Skip to content

feat: Keyboard shortcut handler#9929

Open
snowystinger wants to merge 23 commits into
mainfrom
keyboard-shortcut-handler
Open

feat: Keyboard shortcut handler#9929
snowystinger wants to merge 23 commits into
mainfrom
keyboard-shortcut-handler

Conversation

@snowystinger
Copy link
Copy Markdown
Member

@snowystinger snowystinger commented Apr 15, 2026

Closes

Aiming to help us solve the issue of event leaks, this will allow us more fine grained control of what useKeyboard blocks/prevents default on in a more declarative way.

New behaviour is opt in through shortcuts in useKeyboard. This will preventDefault and stopPropagation if the associated function in the map returns true. Or it will let the event pass without altering it if false is returned. In addition, an object can be returned to alter that behaviour in whatever way necessary. Given that all key combinations are exclusive matches, this should make it more obvious what events are continuing or not.

✅ Pull Request Checklist:

  • Included link to corresponding React Spectrum GitHub Issue.
  • Added/updated unit tests and storybook for this change (for new code or code which already has tests).
  • Filled out test instructions.
  • Updated documentation (if it already exists for this component).
  • Looked at the Accessibility Practices for this feature - Aria Practices

📝 Test Instructions:

🧢 Your Project:

@github-actions github-actions Bot added the RAC label Apr 15, 2026
@snowystinger snowystinger changed the title Keyboard shortcut handler [WIP]: Keyboard shortcut handler Apr 15, 2026
@rspbot
Copy link
Copy Markdown

rspbot commented Apr 30, 2026

@github-actions github-actions Bot added the S2 label May 1, 2026
@rspbot
Copy link
Copy Markdown

rspbot commented May 1, 2026

@snowystinger snowystinger marked this pull request as ready for review May 1, 2026 06:37
@rspbot
Copy link
Copy Markdown

rspbot commented May 1, 2026

@rspbot
Copy link
Copy Markdown

rspbot commented May 1, 2026


const isSelected = selectedKey === key;

let onKeyDown = (event) => {
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.

This seems just designed to skip arrow down and up, i think it'd be better to do this in the keyboard delegate or just allow it, i'm not sure why we wouldn't, there could be vertical steplists and it's a nice non-rtl dependent set of keys.

Just removing it for now as it'd make debugging difficult anyways

@rspbot
Copy link
Copy Markdown

rspbot commented May 4, 2026

@snowystinger snowystinger changed the title [WIP]: Keyboard shortcut handler feat: Keyboard shortcut handler May 4, 2026
# Conflicts:
#	packages/react-aria-components/test/Calendar.test.js
#	packages/react-aria-components/test/RangeCalendar.test.tsx
#	packages/react-aria/src/actiongroup/useActionGroup.ts
#	packages/react-aria/src/calendar/useCalendarGrid.ts
#	packages/react-aria/src/color/useColorArea.ts
#	packages/react-aria/src/combobox/useComboBox.ts
#	packages/react-aria/src/datepicker/useDateField.ts
#	packages/react-aria/src/datepicker/useDateSegment.ts
#	packages/react-aria/src/interactions/useKeyboard.ts
#	packages/react-aria/src/menu/useMenuItem.ts
#	packages/react-aria/src/menu/useMenuTrigger.ts
#	packages/react-aria/src/menu/useSubmenuTrigger.ts
#	packages/react-aria/src/numberfield/useNumberField.ts
#	packages/react-aria/src/overlays/useOverlay.ts
#	packages/react-aria/src/radio/useRadioGroup.ts
#	packages/react-aria/src/searchfield/useSearchField.ts
#	packages/react-aria/src/select/useSelect.ts
#	packages/react-aria/src/selection/useSelectableCollection.ts
#	packages/react-aria/src/spinbutton/useSpinButton.ts
#	packages/react-aria/src/steplist/useStepListItem.ts
#	packages/react-aria/src/table/useTableColumnResize.ts
#	packages/react-aria/test/combobox/useComboBox.test.js
@rspbot
Copy link
Copy Markdown

rspbot commented May 14, 2026

Comment thread packages/react-aria/src/interactions/createKeyboardShortcutHandler.ts Outdated
'ctrl',
'control',
'meta',
'mod', // OS dependent - Cmd on Mac, Ctrl on Windows/Linux
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.

different name? just CMD?

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.

Leaving alone for the moment, needs more opinions

return;
}
shortcutHandler(e);
props.onKeyDown?.(e);
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.

call first and allow users to cancel our behaviour?

Copy link
Copy Markdown
Member Author

@snowystinger snowystinger May 22, 2026

Choose a reason for hiding this comment

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

Can't do it based on preventDefault, we break that too much right now. We could address that, but it'd make the PR harder.

It's also a little odd to use preventDefault to handle this, preventDefault for the browser works at any and every level, the event's default takes place after the bubble phase is complete. If someone preventing default on our actions wanted to do it at some other level, they couldn't. It must be on the same element as the handler that defined the action.

If we created a special (preventDefaultSelection/preventDefaultNavigation), would we do it individually per handler like that? or just a preventAnyRACDefault?

who has the final say on continue propagation?

Maybe it'd be better to expose shortcuts as a prop on everything using useKeyboard. We could merge the shortcut objects. Then we could merge the results and go with the most limited returns.
We could allow an extra property on shortcuts that specifies how the merge should occur, so people could completely prevent us from running by using the merge strategy "replace" or something like that?

import {MergeStrategySymbol} from 'wherever';
<Button shortcuts={{
  'Enter': (e) => {
    console.log('just doing my own thing');
    return {preventDefault: true, continuePropagation: false};
  },
  [MergeStrategySymbol]: 'replace'
  }} onPress={() => console.log('pressed')} />Just click</Button>

// Pressing Enter just does the console log, none of our handlers run, so onPress console log never happens

import {MergeStrategySymbol} from 'wherever';
<Button shortcuts={{
  'Enter': (e) => {
    console.log('just doing my own thing');
    return {preventDefault: true, continuePropagation: true};
  },
  [MergeStrategySymbol]: 'merge'
  }} onPress={() => console.log('pressed')} />Just click</Button>

// Pressing Enter calls their handler first, then our onPress is called. Even though they set continuePropagation to true, our handler sets it false, which is more restrictive and we don't continue propagation

let action = map.get(canonical);
let result = action?.(e);
if (typeof result === 'boolean') {
result = {shouldContinuePropagation: !result, shouldPreventDefault: result};
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.

allow return type of void to behave like return true?

Copy link
Copy Markdown
Member Author

@snowystinger snowystinger May 22, 2026

Choose a reason for hiding this comment

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

I don't love this, it's too easy to forget to return something and returning true does a fair amount already, it stops propagation and prevents default. I think it's better to make a choice. Makes it easier to read the handlers as well, you can't just fall through to void/true accidentally

I'll change it though and see if others agree, can always undo it

@rspbot
Copy link
Copy Markdown

rspbot commented May 19, 2026

@rspbot
Copy link
Copy Markdown

rspbot commented May 22, 2026

@rspbot
Copy link
Copy Markdown

rspbot commented May 22, 2026

## API Changes

react-aria-components

/react-aria-components:GridListItemRenderProps

 GridListItemRenderProps {
   allowsDragging?: boolean
   id?: Key
   isDisabled: boolean
   isDragging?: boolean
   isDropTarget?: boolean
   isFocusVisible: boolean
-  isFocusVisibleWithin: boolean
   isFocused: boolean
   isHovered: boolean
   isPressed: boolean
   isSelected: boolean
   selectionMode: SelectionMode
   state: ListState<unknown>
 }

/react-aria-components:CellRenderProps

 CellRenderProps {
   columnIndex?: number | null
   hasChildItems: boolean
   id?: Key
   isDisabled: boolean
   isExpanded: boolean
   isFocusVisible: boolean
-  isFocusVisibleWithinRow: boolean
   isFocused: boolean
   isHovered: boolean
   isPressed: boolean
   isSelected: boolean
   level: number
 }

@react-aria/interactions

/@react-aria/interactions:KeyboardProps

 KeyboardProps {
+  ignorePortalRef?: RefObject<Element | null> | null
   isDisabled?: boolean
   onKeyDown?: (KeyboardEvent) => void
   onKeyUp?: (KeyboardEvent) => void
+  shortcuts?: KeyboardShortcutBindings
 }

@rspbot
Copy link
Copy Markdown

rspbot commented May 22, 2026

Agent Skills Changes

Removed (117)
  • s2/skills/react-spectrum-s2/references/guides/dnd.md
  • s2/skills/react-spectrum-s2/references/guides/test-utils-guidance.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/Autocomplete.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/Breadcrumbs.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/Button.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/Calendar.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/Checkbox.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/CheckboxGroup.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/ColorArea.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/ColorField.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/ColorPicker.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/ColorSlider.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/ColorSwatch.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/ColorSwatchPicker.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/ColorWheel.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/ComboBox.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/DateField.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/DatePicker.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/DateRangePicker.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/Disclosure.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/DisclosureGroup.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/DropZone.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/FileTrigger.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/Form.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/GridList.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/Group.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/Link.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/ListBox.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/Menu.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/Meter.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/Modal.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/NumberField.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/Popover.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/ProgressBar.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/RadioGroup.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/RangeCalendar.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/SearchField.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/Select.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/Separator.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/Slider.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/Switch.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/Table.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/Tabs.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/TagGroup.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/TextField.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/TimeField.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/Toast.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/ToggleButton.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/ToggleButtonGroup.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/Toolbar.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/Tooltip.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/Tree.md
  • s2/skills/react-spectrum-s2/references/react-aria/components/Virtualizer.md
  • s2/skills/react-spectrum-s2/references/react-aria/guides/ai.md
  • s2/skills/react-spectrum-s2/references/react-aria/guides/collections.md
  • s2/skills/react-spectrum-s2/references/react-aria/guides/customization.md
  • s2/skills/react-spectrum-s2/references/react-aria/guides/dnd.md
  • s2/skills/react-spectrum-s2/references/react-aria/guides/forms.md
  • s2/skills/react-spectrum-s2/references/react-aria/guides/frameworks.md
  • s2/skills/react-spectrum-s2/references/react-aria/guides/getting-started.md
  • s2/skills/react-spectrum-s2/references/react-aria/guides/quality.md
  • s2/skills/react-spectrum-s2/references/react-aria/guides/selection.md
  • s2/skills/react-spectrum-s2/references/react-aria/guides/styling.md
  • s2/skills/react-spectrum-s2/references/react-aria/guides/testing.md
  • s2/skills/react-spectrum-s2/references/react-aria/interactions/FocusRing.md
  • s2/skills/react-spectrum-s2/references/react-aria/interactions/FocusScope.md
  • s2/skills/react-spectrum-s2/references/react-aria/interactions/useClipboard.md
  • s2/skills/react-spectrum-s2/references/react-aria/interactions/useDrag.md
  • s2/skills/react-spectrum-s2/references/react-aria/interactions/useDrop.md
  • s2/skills/react-spectrum-s2/references/react-aria/interactions/useFocus.md
  • s2/skills/react-spectrum-s2/references/react-aria/interactions/useFocusRing.md
  • s2/skills/react-spectrum-s2/references/react-aria/interactions/useFocusVisible.md
  • s2/skills/react-spectrum-s2/references/react-aria/interactions/useFocusWithin.md
  • s2/skills/react-spectrum-s2/references/react-aria/interactions/useHover.md
  • s2/skills/react-spectrum-s2/references/react-aria/interactions/useKeyboard.md
  • s2/skills/react-spectrum-s2/references/react-aria/interactions/useLandmark.md
  • s2/skills/react-spectrum-s2/references/react-aria/interactions/useLongPress.md
  • s2/skills/react-spectrum-s2/references/react-aria/interactions/useMove.md
  • s2/skills/react-spectrum-s2/references/react-aria/interactions/usePress.md
  • s2/skills/react-spectrum-s2/references/react-aria/internationalized/date/Calendar.md
  • s2/skills/react-spectrum-s2/references/react-aria/internationalized/date/CalendarDate.md
  • s2/skills/react-spectrum-s2/references/react-aria/internationalized/date/CalendarDateTime.md
  • s2/skills/react-spectrum-s2/references/react-aria/internationalized/date/DateFormatter.md
  • s2/skills/react-spectrum-s2/references/react-aria/internationalized/date/Time.md
  • s2/skills/react-spectrum-s2/references/react-aria/internationalized/date/ZonedDateTime.md
  • s2/skills/react-spectrum-s2/references/react-aria/internationalized/date/index.md
  • s2/skills/react-spectrum-s2/references/react-aria/internationalized/number/NumberFormatter.md
  • s2/skills/react-spectrum-s2/references/react-aria/internationalized/number/NumberParser.md
  • s2/skills/react-spectrum-s2/references/react-aria/internationalized/number/index.md
  • s2/skills/react-spectrum-s2/references/react-aria/llms.txt
  • s2/skills/react-spectrum-s2/references/react-aria/testing/CheckboxGroup/testing.md
  • s2/skills/react-spectrum-s2/references/react-aria/testing/ComboBox/testing.md
  • s2/skills/react-spectrum-s2/references/react-aria/testing/GridList/testing.md
  • s2/skills/react-spectrum-s2/references/react-aria/testing/ListBox/testing.md
  • s2/skills/react-spectrum-s2/references/react-aria/testing/Menu/testing.md
  • s2/skills/react-spectrum-s2/references/react-aria/testing/Modal/testing.md
  • s2/skills/react-spectrum-s2/references/react-aria/testing/Popover/testing.md
  • s2/skills/react-spectrum-s2/references/react-aria/testing/RadioGroup/testing.md
  • s2/skills/react-spectrum-s2/references/react-aria/testing/Select/testing.md
  • s2/skills/react-spectrum-s2/references/react-aria/testing/Table/testing.md
  • s2/skills/react-spectrum-s2/references/react-aria/testing/Tabs/testing.md
  • s2/skills/react-spectrum-s2/references/react-aria/testing/Tree/testing.md
  • s2/skills/react-spectrum-s2/references/react-aria/utilities/I18nProvider.md
  • s2/skills/react-spectrum-s2/references/react-aria/utilities/PortalProvider.md
  • s2/skills/react-spectrum-s2/references/react-aria/utilities/SSRProvider.md
  • s2/skills/react-spectrum-s2/references/react-aria/utilities/VisuallyHidden.md
  • s2/skills/react-spectrum-s2/references/react-aria/utilities/mergeProps.md
  • s2/skills/react-spectrum-s2/references/react-aria/utilities/useCollator.md
  • s2/skills/react-spectrum-s2/references/react-aria/utilities/useDateFormatter.md
  • s2/skills/react-spectrum-s2/references/react-aria/utilities/useField.md
  • s2/skills/react-spectrum-s2/references/react-aria/utilities/useFilter.md
  • s2/skills/react-spectrum-s2/references/react-aria/utilities/useId.md
  • s2/skills/react-spectrum-s2/references/react-aria/utilities/useIsSSR.md
  • s2/skills/react-spectrum-s2/references/react-aria/utilities/useLabel.md
  • s2/skills/react-spectrum-s2/references/react-aria/utilities/useLocale.md
  • s2/skills/react-spectrum-s2/references/react-aria/utilities/useNumberFormatter.md
  • s2/skills/react-spectrum-s2/references/react-aria/utilities/useObjectRef.md
Modified (145)
Install

React Spectrum S2:

npx skills add https://d1pzu54gtk2aed.cloudfront.net/pr/8e99c0067edfeb453014cef3d2a2ee717b51e49f/

React Aria:

npx skills add https://d5iwopk28bdhl.cloudfront.net/pr/8e99c0067edfeb453014cef3d2a2ee717b51e49f/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants