Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .changeset/kbd-hotkeys-blank-client-hints-platform.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
'@astryxdesign/core': patch
---

[fix] `mod` hotkeys and `Kbd` resolve to Cmd on macOS again when client hints report a blank platform

`useHotkeys` and `Kbd` both prefer `navigator.userAgentData.platform` and fall
back to `navigator.platform`, but guarded the preference with
`'platform' in uaData`, which is true whenever the key exists at all. A build
reporting `platform: ''` therefore committed to the client-hints branch and got
`false` without ever reaching the fallback, so on macOS every `mod` combo
listened for Ctrl and every `<Kbd>` drew Ctrl. Electron and other embedders
that rewrite the app's user-agent identity ship exactly that. A blank platform
is now treated as unknown and falls through.

@Astro-Han
17 changes: 17 additions & 0 deletions packages/core/src/Kbd/Kbd.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ describe('Kbd', () => {
value: originalPlatform,
configurable: true,
});
delete (navigator as {userAgentData?: unknown}).userAgentData;
});

it('renders a single key', () => {
Expand Down Expand Up @@ -54,6 +55,22 @@ describe('Kbd', () => {
expect(screen.getByText('\u2318')).toBeInTheDocument(); // \u2318
});

it('reads a blank userAgentData.platform as unknown, not as non-Mac', () => {
// Builds that rewrite their client-hints identity expose the key with an
// empty value; navigator.platform is the only surface left that answers.
Object.defineProperty(navigator, 'userAgentData', {
value: {platform: ''},
configurable: true,
});
Object.defineProperty(navigator, 'platform', {
value: 'MacIntel',
configurable: true,
});

render(<Kbd keys="mod" />);
expect(screen.getByText('\u2318')).toBeInTheDocument();
});

it('maps modifier keys to symbols', () => {
render(<Kbd keys="ctrl+alt+shift+k" />);
expect(screen.getByText('\u2303')).toBeInTheDocument(); // \u2303
Expand Down
13 changes: 10 additions & 3 deletions packages/core/src/Kbd/Kbd.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,9 @@ function getServerPlatformSnapshot(): boolean {

/**
* Detects whether the current platform is macOS/iOS.
* Prefers the User-Agent Client Hints API when available (modern Chrome/Edge),
* falls back to navigator.platform (deprecated but universally supported).
* Prefers the User-Agent Client Hints API when it names a platform (modern
* Chrome/Edge), falls back to navigator.platform (deprecated but universally
* supported) when it is absent or blank.
*/
function detectMac(): boolean {
if (typeof navigator === 'undefined') {
Expand All @@ -134,7 +135,13 @@ function detectMac(): boolean {
// Prefer User-Agent Client Hints API (not deprecated)
const uaData = 'userAgentData' in navigator ? navigator.userAgentData : null;
if (uaData && typeof uaData === 'object' && 'platform' in uaData) {
return /mac/i.test((uaData as {platform: string}).platform ?? '');
const uaPlatform = (uaData as {platform?: unknown}).platform;
// A blank platform is no answer, not a negative one. Builds that rewrite
// their client-hints identity ship '', so fall through rather than
// reading it as "not Apple".
if (typeof uaPlatform === 'string' && uaPlatform.trim() !== '') {
return /mac/i.test(uaPlatform);
}
}
// Fallback: navigator.platform (deprecated but still shipped everywhere)
return /Mac|iPhone|iPad|iPod/.test(navigator.platform ?? '');
Expand Down
18 changes: 18 additions & 0 deletions packages/core/src/hooks/useHotkeys.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,24 @@ describe('useHotkeys', () => {
expect(onPress).toHaveBeenCalledTimes(1);
});

it('reads a blank userAgentData.platform as unknown, not as non-Apple', () => {
// Builds that rewrite their client-hints identity expose the key with an
// empty value; navigator.platform is the only surface left that answers.
vi.stubGlobal('navigator', {
userAgentData: {platform: ''},
platform: 'MacIntel',
});
const onPress = vi.fn();
renderHook(() => useHotkeys([{keys: 'mod+k', onPress}]));

press('k', {ctrlKey: true});
expect(onPress).not.toHaveBeenCalled();

const event = press('k', {metaKey: true});
expect(onPress).toHaveBeenCalledTimes(1);
expect(onPress).toHaveBeenCalledWith(event);
});

it('does not fire a bare key when modifiers are held', () => {
stubApplePlatform();
const onPress = vi.fn();
Expand Down
13 changes: 10 additions & 3 deletions packages/core/src/hooks/useHotkeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,9 @@ const KEY_ALIASES: Record<string, string> = {

/**
* Detects whether the current platform is macOS/iOS.
* Prefers the User-Agent Client Hints API when available (modern Chrome/Edge),
* falls back to navigator.platform (deprecated but universally supported).
* Prefers the User-Agent Client Hints API when it names a platform (modern
* Chrome/Edge), falls back to navigator.platform (deprecated but universally
* supported) when it is absent or blank.
* Mirrors the detection used by Kbd so displayed and handled shortcuts agree.
*/
function isApplePlatform(): boolean {
Expand All @@ -83,7 +84,13 @@ function isApplePlatform(): boolean {
}
const uaData = 'userAgentData' in navigator ? navigator.userAgentData : null;
if (uaData && typeof uaData === 'object' && 'platform' in uaData) {
return /mac/i.test((uaData as {platform: string}).platform ?? '');
const uaPlatform = (uaData as {platform?: unknown}).platform;
// A blank platform is no answer, not a negative one. Builds that rewrite
// their client-hints identity ship '', so fall through rather than
// reading it as "not Apple".
if (typeof uaPlatform === 'string' && uaPlatform.trim() !== '') {
return /mac/i.test(uaPlatform);
}
}
return /Mac|iPhone|iPad|iPod/.test(navigator.platform ?? '');
}
Expand Down