Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
30f302e
prototype
snowystinger Mar 18, 2026
a564b71
Change over all useSelectableCollection to use the new shortcuts syntax
snowystinger Apr 13, 2026
a891c1e
try making object more readable
snowystinger Apr 13, 2026
cafa642
share types
snowystinger Apr 13, 2026
44e5de7
simplify and add tests, move to interactions. fix outstanding bug
snowystinger Apr 30, 2026
f29a6ec
Merge branch 'main' into keyboard-shortcut-handler
snowystinger Apr 30, 2026
03bfab7
fix react 16 tests
snowystinger Apr 30, 2026
8791d64
Starting converting everything to use shortcuts
snowystinger May 1, 2026
a0a257f
fix all of the lint
snowystinger May 1, 2026
07e8ea6
fix: calendar tests
snowystinger May 1, 2026
17ed6c6
Merge branch 'main' into keyboard-shortcut-handler
snowystinger May 1, 2026
20c5c3d
convert calendar and date picker
snowystinger May 1, 2026
276e625
actiongroup and datesegment and lint
snowystinger May 1, 2026
ed6fd4b
simplify code
snowystinger May 1, 2026
43f94ff
fix lint
snowystinger May 1, 2026
14cc456
fix ts that I don't think i broke
snowystinger May 1, 2026
fcbf35c
add combobox
snowystinger May 1, 2026
8cdc93e
simplify some code
snowystinger May 4, 2026
0990e4e
fix lint
snowystinger May 4, 2026
c87a33b
Merge branch 'main' into keyboard-shortcut-handler
snowystinger May 14, 2026
86caca9
Merge branch 'main' into keyboard-shortcut-handler
snowystinger May 19, 2026
8e99c00
Remove "Sel" expansion
snowystinger May 22, 2026
59ed398
change to void return
snowystinger May 22, 2026
90490c4
Merge branch 'main' into keyboard-shortcut-handler
snowystinger May 26, 2026
ae94254
fix ts
snowystinger May 26, 2026
b8520b3
update everything to the void signature
snowystinger May 29, 2026
9a9a0c2
review comments
snowystinger May 31, 2026
257b1c8
Merge branch 'main' into keyboard-shortcut-handler
snowystinger Jun 9, 2026
73a0c69
remove stale todo
snowystinger Jun 9, 2026
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
5 changes: 2 additions & 3 deletions packages/@adobe/react-spectrum/src/actionbar/ActionBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,8 @@ function ActionBarInner<T>(props: ActionBarInnerProps<T>, ref: Ref<HTMLDivElemen
}

let {keyboardProps} = useKeyboard({
onKeyDown(e) {
if (e.key === 'Escape') {
e.preventDefault();
shortcuts: {
Escape: () => {
onClearSelection();
}
}
Expand Down
7 changes: 2 additions & 5 deletions packages/@react-spectrum/s2/src/ActionBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,12 +167,9 @@ const ActionBarInner = forwardRef(function ActionBarInner(
});

let {keyboardProps} = useKeyboard({
onKeyDown(e) {
if (e.key === 'Escape') {
e.preventDefault();
shortcuts: {
Escape: () => {
onClearSelection?.();
} else {
e.continuePropagation();
}
}
});
Expand Down
37 changes: 37 additions & 0 deletions packages/react-aria-components/test/ListBox.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2396,3 +2396,40 @@ describe('ListBox', () => {
});
}
});

describe('keyboard modifier keys', () => {
let user;
let platformMock;
beforeAll(() => {
user = userEvent.setup({delay: null, pointerMap});
});
// selectionMode: 'none', 'single', 'multiple'
// selectionBehavior: 'toggle', 'replace'
// platform: 'mac', 'windows'

// modifier key: 'alt', 'ctrl', 'meta', 'shift'
// key: 'arrow-up', 'arrow-down', 'arrow-left', 'arrow-right', 'home', 'end', 'page-up', 'page-down', 'enter', 'space', 'tab'
// expected behavior: 'navigate', 'select', 'toggle', 'replace'
describe('mac', () => {
beforeAll(() => {
platformMock = jest.spyOn(navigator, 'platform', 'get').mockImplementation(() => 'Mac');
});
afterAll(() => {
platformMock.mockRestore();
});
it('should not navigate when using unsupported modifier keys', async () => {
let {getByRole} = renderListbox({selectionMode: 'none'});
await user.tab();
let listbox = getByRole('listbox');
let options = within(listbox).getAllByRole('option');
await user.keyboard('{ArrowDown}');
expect(document.activeElement).toBe(options[1]);
await user.keyboard('{Meta>}{ArrowDown}{/Meta}');
expect(document.activeElement).toBe(options[1]);
await user.keyboard('{Meta>}{ArrowUp}{/Meta}');
expect(document.activeElement).toBe(options[1]);
await user.keyboard('{Control>}{Home}{/Control}');
expect(document.activeElement).toBe(options[1]);
});
});
});
40 changes: 18 additions & 22 deletions packages/react-aria/src/actiongroup/useActionGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ import {
} from '@react-types/shared';
import {createFocusManager} from '../focus/FocusScope';
import {filterDOMProps} from '../utils/filterDOMProps';
import {getEventTarget, nodeContains} from '../utils/shadowdom/DOMFunctions';
import {KeyboardEventHandler, useState} from 'react';
import {ListState} from 'react-stately/useListState';
import {useKeyboard} from '../interactions/useKeyboard';
import {useLayoutEffect} from '../utils/useLayoutEffect';
import {useLocale} from '../i18n/I18nProvider';
import {useState} from 'react';

const BUTTON_GROUP_ROLES = {
none: 'toolbar',
Expand Down Expand Up @@ -91,34 +91,30 @@ export function useActionGroup<T>(
let {direction} = useLocale();
let focusManager = createFocusManager(ref);
let flipDirection = direction === 'rtl' && orientation === 'horizontal';
let onKeyDown: KeyboardEventHandler = e => {
if (!nodeContains(e.currentTarget, getEventTarget(e))) {
return;
}

switch (e.key) {
case 'ArrowRight':
case 'ArrowDown':
e.preventDefault();
e.stopPropagation();
if (e.key === 'ArrowRight' && flipDirection) {
let {keyboardProps} = useKeyboard({
shortcuts: {
ArrowRight: () => {
if (flipDirection) {
focusManager.focusPrevious({wrap: true});
} else {
focusManager.focusNext({wrap: true});
}
break;
case 'ArrowLeft':
case 'ArrowUp':
e.preventDefault();
e.stopPropagation();
if (e.key === 'ArrowLeft' && flipDirection) {
},
ArrowDown: () => {
focusManager.focusNext({wrap: true});
},
ArrowLeft: () => {
if (flipDirection) {
focusManager.focusNext({wrap: true});
} else {
focusManager.focusPrevious({wrap: true});
}
break;
},
ArrowUp: () => {
focusManager.focusPrevious({wrap: true});
}
}
};
});

let role: string | undefined = BUTTON_GROUP_ROLES[state.selectionManager.selectionMode];
if (isInToolbar && role === 'toolbar') {
Expand All @@ -130,7 +126,7 @@ export function useActionGroup<T>(
role,
'aria-orientation': role === 'toolbar' ? orientation : undefined,
'aria-disabled': isDisabled,
onKeyDown
...keyboardProps
}
};
}
84 changes: 38 additions & 46 deletions packages/react-aria/src/calendar/useCalendarGrid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ import {CalendarDate, startOfWeek, today} from '@internationalized/date';
import {CalendarSelectionMode, CalendarState} from 'react-stately/useCalendarState';
import {DOMAttributes} from '@react-types/shared';
import {hookData, useVisibleRangeDescription} from './utils';
import {KeyboardEvent, useMemo} from 'react';
import {mergeProps} from '../utils/mergeProps';
import {RangeCalendarState} from 'react-stately/useRangeCalendarState';
import {useDateFormatter} from '../i18n/useDateFormatter';
import {useKeyboard} from '../interactions/useKeyboard';
import {useLabels} from '../utils/useLabels';
import {useLocale} from '../i18n/I18nProvider';
import {useMemo} from 'react';

export interface AriaCalendarGridProps {
/**
Expand Down Expand Up @@ -78,70 +79,61 @@ export function useCalendarGrid(

let {direction} = useLocale();

let onKeyDown = (e: KeyboardEvent) => {
switch (e.key) {
case 'Enter':
case ' ':
e.preventDefault();
let {keyboardProps} = useKeyboard({
shortcuts: {
Enter: () => {
state.selectFocusedDate();
break;
case 'PageUp':
e.preventDefault();
e.stopPropagation();
state.focusPreviousSection(e.shiftKey);
break;
case 'PageDown':
e.preventDefault();
e.stopPropagation();
state.focusNextSection(e.shiftKey);
break;
case 'End':
e.preventDefault();
e.stopPropagation();
},
' ': () => {
state.selectFocusedDate();
},
PageUp: () => {
state.focusPreviousSection();
},
'Shift+PageUp': () => {
state.focusPreviousSection(true);
},
PageDown: () => {
state.focusNextSection();
},
'Shift+PageDown': () => {
state.focusNextSection(true);
},
End: () => {
state.focusSectionEnd();
break;
case 'Home':
e.preventDefault();
e.stopPropagation();
},
Home: () => {
state.focusSectionStart();
break;
case 'ArrowLeft':
e.preventDefault();
e.stopPropagation();
},
ArrowLeft: () => {
if (direction === 'rtl') {
state.focusNextDay();
} else {
state.focusPreviousDay();
}
break;
case 'ArrowUp':
e.preventDefault();
e.stopPropagation();
},
ArrowUp: () => {
state.focusPreviousRow();
break;
case 'ArrowRight':
e.preventDefault();
e.stopPropagation();
},
ArrowRight: () => {
if (direction === 'rtl') {
state.focusPreviousDay();
} else {
state.focusNextDay();
}
break;
case 'ArrowDown':
e.preventDefault();
e.stopPropagation();
},
ArrowDown: () => {
state.focusNextRow();
break;
case 'Escape':
},
Escape: () => {
// Cancel the selection.
if ('setAnchorDate' in state) {
e.preventDefault();
state.setAnchorDate(null);
}
break;
return false; // TODO: is this really correct? or should it return true when we cancel and only propagate if there's nothing to do

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we return true inside the if statement? Was there a test that broke?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, react-spectrum/test/datepicker/DateRangePicker.test.js:439:28

}
}
};
});

let visibleRangeDescription = useVisibleRangeDescription(
startDate,
Expand Down Expand Up @@ -182,7 +174,7 @@ export function useCalendarGrid(
'aria-disabled': state.isDisabled || undefined,
'aria-multiselectable':
'highlightedRange' in state || state.selectionMode === 'multiple' || undefined,
onKeyDown,
...keyboardProps,
onFocus: () => state.setFocused(true),
onBlur: () => state.setFocused(false)
}),
Expand Down
88 changes: 49 additions & 39 deletions packages/react-aria/src/color/useColorArea.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,46 +111,56 @@ export function useColorArea(props: AriaColorAreaOptions, state: ColorAreaState)

let currentPosition = useRef<{x: number; y: number} | null>(null);

let keyboardUpdate = (cb, inputRef: RefObject<HTMLInputElement | null>, input: 'x' | 'y') => {
state.setDragging(true);
setValueChangedViaKeyboard(true);
cb();
state.setDragging(false);
focusInput(inputRef);
setFocusedInput(input);
};

let {keyboardProps} = useKeyboard({
onKeyDown(e) {
// these are the cases that useMove doesn't handle
if (!/^(PageUp|PageDown|Home|End)$/.test(e.key)) {
e.continuePropagation();
return;
}
// same handling as useMove, don't need to stop propagation, useKeyboard will do that for us
e.preventDefault();
// remember to set this and unset it so that onChangeEnd is fired
state.setDragging(true);
setValueChangedViaKeyboard(true);
let dir;
switch (e.key) {
case 'PageUp':
state.incrementY(state.yChannelPageStep);
dir = 'y';
break;
case 'PageDown':
state.decrementY(state.yChannelPageStep);
dir = 'y';
break;
case 'Home':
direction === 'rtl'
? state.incrementX(state.xChannelPageStep)
: state.decrementX(state.xChannelPageStep);
dir = 'x';
break;
case 'End':
direction === 'rtl'
? state.decrementX(state.xChannelPageStep)
: state.incrementX(state.xChannelPageStep);
dir = 'x';
break;
}
state.setDragging(false);
if (dir) {
let input = dir === 'x' ? inputXRef : inputYRef;
focusInput(input);
setFocusedInput(dir);
shortcuts: {
PageUp: () => {
return keyboardUpdate(
() => {
state.incrementY(state.yChannelPageStep);
},
inputYRef,
'y'
);
},
PageDown: () => {
return keyboardUpdate(
() => {
state.decrementY(state.yChannelPageStep);
},
inputYRef,
'y'
);
},
Home: () => {
return keyboardUpdate(
() => {
direction === 'rtl'
? state.incrementX(state.xChannelPageStep)
: state.decrementX(state.xChannelPageStep);
},
inputXRef,
'x'
);
},
End: () => {
return keyboardUpdate(
() => {
direction === 'rtl'
? state.decrementX(state.xChannelPageStep)
: state.incrementX(state.xChannelPageStep);
},
inputXRef,
'x'
);
}
}
});
Expand Down
Loading