From 8a1ff6c4ba62659848f634a0f2714414ba9eb610 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Thu, 15 Jan 2026 18:15:34 -0600 Subject: [PATCH 1/5] fix 'ResizeObserver loop completed with undelivered notifications' error when overlays get resized (#7742) Co-authored-by: Devon Govett --- packages/@react-aria/utils/src/useResizeObserver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-aria/utils/src/useResizeObserver.ts b/packages/@react-aria/utils/src/useResizeObserver.ts index ab0a1d5ae3c..0d33aa60a2d 100644 --- a/packages/@react-aria/utils/src/useResizeObserver.ts +++ b/packages/@react-aria/utils/src/useResizeObserver.ts @@ -37,7 +37,7 @@ export function useResizeObserver(options: useResizeObserverO return; } - onResizeEvent(); + requestAnimationFrame(() => onResizeEvent()); }); resizeObserverInstance.observe(element, {box}); From 4a815c3efb1ec007b05463d5aafc80565b6f9f67 Mon Sep 17 00:00:00 2001 From: Damian Pieczynski Date: Fri, 16 Jan 2026 01:25:08 +0100 Subject: [PATCH 2/5] fix: offset calculation to handle transformed elements with translateY or translateX in scrollIntoView (#7717) * fix: offset calculation to handle transformed elements with translateY or translateX * add story * fix lint --------- Co-authored-by: Reid Barber Co-authored-by: Devon Govett --- .../@react-aria/utils/src/scrollIntoView.ts | 29 ++++------ .../stories/GridList.stories.tsx | 55 ++++++++++++++++++- 2 files changed, 66 insertions(+), 18 deletions(-) diff --git a/packages/@react-aria/utils/src/scrollIntoView.ts b/packages/@react-aria/utils/src/scrollIntoView.ts index 57c1a09fefe..92e93a53785 100644 --- a/packages/@react-aria/utils/src/scrollIntoView.ts +++ b/packages/@react-aria/utils/src/scrollIntoView.ts @@ -73,23 +73,18 @@ export function scrollIntoView(scrollView: HTMLElement, element: HTMLElement): v * offsetLeft or offsetTop through intervening offsetParents. */ function relativeOffset(ancestor: HTMLElement, child: HTMLElement, axis: 'left'|'top') { - const prop = axis === 'left' ? 'offsetLeft' : 'offsetTop'; - let sum = 0; - while (child.offsetParent) { - sum += child[prop]; - if (child.offsetParent === ancestor) { - // Stop once we have found the ancestor we are interested in. - break; - } else if (child.offsetParent.contains(ancestor)) { - // If the ancestor is not `position:relative`, then we stop at - // _its_ offset parent, and we subtract off _its_ offset, so that - // we end up with the proper offset from child to ancestor. - sum -= ancestor[prop]; - break; - } - child = child.offsetParent as HTMLElement; - } - return sum; + let childRect = child.getBoundingClientRect(); + let ancestorRect = ancestor.getBoundingClientRect(); + + let viewportOffset = axis === 'left' + ? childRect.left - ancestorRect.left + : childRect.top - ancestorRect.top; + + let scrollAdjustment = axis === 'left' + ? ancestor.scrollLeft + : ancestor.scrollTop; + + return viewportOffset + scrollAdjustment; } /** diff --git a/packages/react-aria-components/stories/GridList.stories.tsx b/packages/react-aria-components/stories/GridList.stories.tsx index b6f0174a509..c5fc4e26059 100644 --- a/packages/react-aria-components/stories/GridList.stories.tsx +++ b/packages/react-aria-components/stories/GridList.stories.tsx @@ -625,6 +625,60 @@ function GridListInModalPickerRender(props: ModalOverlayProps): JSX.Element { ); } +export function GridListScrollIntoView() { + let items: {id: number, name: string}[] = []; + for (let i = 0; i < 100; i++) { + items.push({id: i, name: `Item ${i}`}); + } + + let list = useListData({ + initialItems: items + }); + + const getElement = (id: number) => document.querySelector(`[data-key="${id}"]`) as HTMLElement; + + const rowHeight = 25; + + return ( + <> +
+ + {item => ( + + {item.name} + )} + +
+ + + + ); +} + export let GridListInModalPicker: StoryObj = { render: (args) => , parameters: { @@ -635,4 +689,3 @@ export let GridListInModalPicker: StoryObj = } } }; - From e557afddc13032e39ffa288dd5506c0ffbf8a498 Mon Sep 17 00:00:00 2001 From: Damian Stasik <920747+damianstasik@users.noreply.github.com> Date: Fri, 16 Jan 2026 01:25:18 +0100 Subject: [PATCH 3/5] Expose focus events' types on Collection Items (#8231) * Expose focus events types on MenuItem * add test * fix test * fix test, add tabs, add taggroup * fix lint --------- Co-authored-by: Robert Snow --- packages/@react-aria/menu/src/useMenuItem.ts | 6 ++--- packages/@react-aria/tabs/src/useTab.ts | 1 + packages/@react-aria/tag/src/useTag.ts | 1 + packages/react-aria-components/src/Menu.tsx | 4 ++-- packages/react-aria-components/src/Tabs.tsx | 12 +++++----- .../react-aria-components/src/TagGroup.tsx | 4 ++-- .../react-aria-components/test/Menu.test.tsx | 23 +++++++++++++++++-- .../react-aria-components/test/Tabs.test.js | 16 +++++++++++-- .../test/TagGroup.test.js | 14 ++++++++++- 9 files changed, 63 insertions(+), 18 deletions(-) diff --git a/packages/@react-aria/menu/src/useMenuItem.ts b/packages/@react-aria/menu/src/useMenuItem.ts index 58881c84064..a8302c8c745 100644 --- a/packages/@react-aria/menu/src/useMenuItem.ts +++ b/packages/@react-aria/menu/src/useMenuItem.ts @@ -13,7 +13,7 @@ import {DOMAttributes, DOMProps, FocusableElement, FocusEvents, HoverEvents, Key, KeyboardEvents, PressEvent, PressEvents, RefObject} from '@react-types/shared'; import {filterDOMProps, handleLinkClick, mergeProps, useLinkProps, useRouter, useSlotId} from '@react-aria/utils'; import {getItemCount} from '@react-stately/collections'; -import {isFocusVisible, useFocus, useHover, useKeyboard, usePress} from '@react-aria/interactions'; +import {isFocusVisible, useFocusable, useHover, useKeyboard, usePress} from '@react-aria/interactions'; import {menuData} from './utils'; import {MouseEvent, useRef} from 'react'; import {SelectionManager} from '@react-stately/selection'; @@ -307,7 +307,7 @@ export function useMenuItem(props: AriaMenuItemProps, state: TreeState, re onKeyUp }); - let {focusProps} = useFocus({onBlur, onFocus, onFocusChange}); + let {focusableProps} = useFocusable({onBlur, onFocus, onFocusChange}, ref); let domProps = filterDOMProps(item?.props); delete domProps.id; let linkProps = useLinkProps(item?.props); @@ -324,7 +324,7 @@ export function useMenuItem(props: AriaMenuItemProps, state: TreeState, re pressProps, hoverProps, keyboardProps, - focusProps, + focusableProps, // Prevent DOM focus from moving on mouse down when using virtual focus or this is a submenu/subdialog trigger. data.shouldUseVirtualFocus || isTrigger ? {onMouseDown: e => e.preventDefault()} : undefined, isDisabled ? undefined : {onClick} diff --git a/packages/@react-aria/tabs/src/useTab.ts b/packages/@react-aria/tabs/src/useTab.ts index 57a079eb42d..21f2a9e5f2e 100644 --- a/packages/@react-aria/tabs/src/useTab.ts +++ b/packages/@react-aria/tabs/src/useTab.ts @@ -62,6 +62,7 @@ export function useTab( delete domProps.id; let linkProps = useLinkProps(item?.props); let {focusableProps} = useFocusable({ + ...item?.props, isDisabled }, ref); diff --git a/packages/@react-aria/tag/src/useTag.ts b/packages/@react-aria/tag/src/useTag.ts index 9a084031ff6..7fddd3a956c 100644 --- a/packages/@react-aria/tag/src/useTag.ts +++ b/packages/@react-aria/tag/src/useTag.ts @@ -92,6 +92,7 @@ export function useTag(props: AriaTagProps, state: ListState, ref: RefO let domProps = filterDOMProps(item.props); let linkProps = useSyntheticLinkProps(item.props); let {focusableProps} = useFocusable({ + ...item.props, isDisabled }, ref); diff --git a/packages/react-aria-components/src/Menu.tsx b/packages/react-aria-components/src/Menu.tsx index 6aef1a7d20e..5718c4841fb 100644 --- a/packages/react-aria-components/src/Menu.tsx +++ b/packages/react-aria-components/src/Menu.tsx @@ -29,7 +29,7 @@ import { import {CollectionProps, CollectionRendererContext, ItemRenderProps, SectionContext, SectionProps, usePersistedKeys} from './Collection'; import {FieldInputContext, SelectableCollectionContext, SelectableCollectionContextValue} from './RSPContexts'; import {filterDOMProps, useObjectRef, useResizeObserver} from '@react-aria/utils'; -import {FocusStrategy, forwardRefType, GlobalDOMAttributes, HoverEvents, Key, LinkDOMProps, MultipleSelection, PressEvents} from '@react-types/shared'; +import {FocusEvents, FocusStrategy, forwardRefType, GlobalDOMAttributes, HoverEvents, Key, LinkDOMProps, MultipleSelection, PressEvents} from '@react-types/shared'; import {HeaderContext} from './Header'; import {KeyboardContext} from './Keyboard'; import {MultipleSelectionState, SelectionManager, useMultipleSelectionState} from '@react-stately/selection'; @@ -394,7 +394,7 @@ export interface MenuItemRenderProps extends ItemRenderProps { isOpen: boolean } -export interface MenuItemProps extends RenderProps, LinkDOMProps, HoverEvents, PressEvents, Omit, 'onClick'> { +export interface MenuItemProps extends RenderProps, LinkDOMProps, HoverEvents, FocusEvents, PressEvents, Omit, 'onClick'> { /** * The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the element. A function may be provided to compute the class based on component state. * @default 'react-aria-MenuItem' diff --git a/packages/react-aria-components/src/Tabs.tsx b/packages/react-aria-components/src/Tabs.tsx index dfa2b9fa381..98626632a50 100644 --- a/packages/react-aria-components/src/Tabs.tsx +++ b/packages/react-aria-components/src/Tabs.tsx @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {AriaLabelingProps, forwardRefType, GlobalDOMAttributes, HoverEvents, Key, LinkDOMProps, PressEvents, RefObject} from '@react-types/shared'; +import {AriaLabelingProps, FocusEvents, forwardRefType, GlobalDOMAttributes, HoverEvents, Key, LinkDOMProps, PressEvents, RefObject} from '@react-types/shared'; import {AriaTabListProps, AriaTabPanelProps, mergeProps, Orientation, useFocusRing, useHover, useTab, useTabList, useTabPanel} from 'react-aria'; import {ClassNameOrFunction, ContextValue, Provider, RenderProps, SlotProps, StyleProps, StyleRenderProps, useContextProps, useRenderProps, useSlottedContext} from './utils'; import {Collection, CollectionBuilder, CollectionNode, createHideableComponent, createLeafComponent} from '@react-aria/collections'; @@ -57,7 +57,7 @@ export interface TabListRenderProps { state: TabListState } -export interface TabProps extends RenderProps, AriaLabelingProps, LinkDOMProps, HoverEvents, PressEvents, Omit, 'onClick'> { +export interface TabProps extends RenderProps, AriaLabelingProps, LinkDOMProps, HoverEvents, FocusEvents, PressEvents, Omit, 'onClick'> { /** * The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the element. A function may be provided to compute the class based on component state. * @default 'react-aria-Tab' @@ -354,7 +354,7 @@ export const TabPanels = /*#__PURE__*/ createHideableComponent(function TabPanel if (hasTransition.current == null) { hasTransition.current = /width|height|all/.test(window.getComputedStyle(el).transition); } - + if (hasTransition.current && selectedKeyRef.current != null && selectedKeyRef.current !== state.selectedKey) { // Measure auto size. el.style.setProperty('--tab-panel-width', 'auto'); @@ -365,7 +365,7 @@ export const TabPanels = /*#__PURE__*/ createHideableComponent(function TabPanel // Revert to previous size. el.style.setProperty('--tab-panel-width', prevSize.current.width + 'px'); el.style.setProperty('--tab-panel-height', prevSize.current.height + 'px'); - + // Force style re-calculation to trigger animations. window.getComputedStyle(el).height; @@ -382,7 +382,7 @@ export const TabPanels = /*#__PURE__*/ createHideableComponent(function TabPanel .catch(() => {}); } } - + selectedKeyRef.current = state.selectedKey; }, [ref, state.selectedKey]); @@ -398,7 +398,7 @@ export const TabPanels = /*#__PURE__*/ createHideableComponent(function TabPanel delete DOMProps.id; return ( -
diff --git a/packages/react-aria-components/src/TagGroup.tsx b/packages/react-aria-components/src/TagGroup.tsx index 0e63c0184f7..1497ab16841 100644 --- a/packages/react-aria-components/src/TagGroup.tsx +++ b/packages/react-aria-components/src/TagGroup.tsx @@ -27,7 +27,7 @@ import { import {Collection, CollectionBuilder, createLeafComponent, ItemNode} from '@react-aria/collections'; import {CollectionProps, CollectionRendererContext, DefaultCollectionRenderer, ItemRenderProps, usePersistedKeys} from './Collection'; import {filterDOMProps, mergeProps, useObjectRef} from '@react-aria/utils'; -import {forwardRefType, GlobalDOMAttributes, HoverEvents, Key, LinkDOMProps, PressEvents, RefObject} from '@react-types/shared'; +import {FocusEvents, forwardRefType, GlobalDOMAttributes, HoverEvents, Key, LinkDOMProps, PressEvents, RefObject} from '@react-types/shared'; import {LabelContext} from './Label'; import {ListState, Node, UNSTABLE_useFilteredListState, useListState} from 'react-stately'; import {ListStateContext} from './ListBox'; @@ -219,7 +219,7 @@ export interface TagRenderProps extends Omit, LinkDOMProps, HoverEvents, PressEvents, Omit, 'onClick'> { +export interface TagProps extends RenderProps, LinkDOMProps, HoverEvents, FocusEvents, PressEvents, Omit, 'onClick'> { /** * The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the element. A function may be provided to compute the class based on component state. * @default 'react-aria-Tag' diff --git a/packages/react-aria-components/test/Menu.test.tsx b/packages/react-aria-components/test/Menu.test.tsx index fb0e8f85eb2..1dc23a6a8c3 100644 --- a/packages/react-aria-components/test/Menu.test.tsx +++ b/packages/react-aria-components/test/Menu.test.tsx @@ -334,13 +334,16 @@ describe('Menu', () => { }); it('should support section-level selection', async () => { + let onFocus = jest.fn(); + let onFocusChange = jest.fn(); + let onBlur = jest.fn(); function Example() { let [veggies, setVeggies] = useState(new Set(['lettuce'])); let [protein, setProtein] = useState(new Set(['ham'])); return ( - Lettuce + Lettuce Tomato Onion @@ -378,12 +381,28 @@ describe('Menu', () => { expect(radios[1]).toHaveAttribute('aria-checked', 'true'); expect(radios[2]).toHaveAttribute('aria-checked', 'false'); - act(() => checkboxes[0].focus()); + onFocus.mockClear(); + onFocusChange.mockClear(); + onBlur.mockClear(); + await user.keyboard('{ArrowUp}'); + await user.keyboard('{ArrowUp}'); + await user.keyboard('{ArrowUp}'); + await user.keyboard('{ArrowUp}'); + expect(document.activeElement).toBe(checkboxes[0]); + expect(onFocus).toHaveBeenCalledTimes(1); + expect(onFocusChange).toHaveBeenCalledTimes(1); + expect(onBlur).toHaveBeenCalledTimes(0); + onFocus.mockClear(); + onFocusChange.mockClear(); + onBlur.mockClear(); let sequence = checkboxes.slice(1).concat(radios); for (let item of sequence) { await user.keyboard('{ArrowDown}'); expect(document.activeElement).toBe(item); } + expect(onFocus).toHaveBeenCalledTimes(0); + expect(onFocusChange).toHaveBeenCalledTimes(1); + expect(onBlur).toHaveBeenCalledTimes(1); }); it('should prevent Esc from clearing selection if escapeKeyBehavior is "none"', async () => { diff --git a/packages/react-aria-components/test/Tabs.test.js b/packages/react-aria-components/test/Tabs.test.js index d05350f4267..41166224d26 100644 --- a/packages/react-aria-components/test/Tabs.test.js +++ b/packages/react-aria-components/test/Tabs.test.js @@ -164,7 +164,10 @@ describe('Tabs', () => { }); it('should support focus ring', async () => { - let {getAllByRole} = renderTabs({}, {}, {className: ({isFocusVisible}) => isFocusVisible ? 'focus' : ''}); + let onFocus = jest.fn(); + let onFocusChange = jest.fn(); + let onBlur = jest.fn(); + let {getAllByRole} = renderTabs({}, {}, {className: ({isFocusVisible}) => isFocusVisible ? 'focus' : '', onFocus, onFocusChange, onBlur}); let tab = getAllByRole('tab')[0]; expect(tab).not.toHaveAttribute('data-focus-visible'); @@ -175,10 +178,19 @@ describe('Tabs', () => { expect(tab).toHaveAttribute('data-focus-visible', 'true'); expect(tab).toHaveAttribute('data-focused', 'true'); expect(tab).toHaveClass('focus'); + expect(onFocus).toHaveBeenCalledTimes(1); + expect(onFocusChange).toHaveBeenCalledTimes(1); + expect(onBlur).not.toHaveBeenCalled(); + onFocus.mockClear(); + onFocusChange.mockClear(); + onBlur.mockClear(); await user.tab(); expect(tab).not.toHaveAttribute('data-focus-visible'); expect(tab).not.toHaveClass('focus'); + expect(onFocus).not.toHaveBeenCalled(); + expect(onFocusChange).toHaveBeenCalledTimes(1); + expect(onBlur).toHaveBeenCalledTimes(1); }); it('should support press state', async () => { @@ -634,7 +646,7 @@ describe('Tabs', () => { let {getByRole} = renderTabs({keyboardActivation: 'manual'}, {}, {onPressStart, onPressEnd, onPress, onClick}); let tester = testUtilUser.createTester('Tabs', {root: getByRole('tablist')}); await tester.triggerTab({tab: 1, interactionType, manualActivation: true}); - + expect(onPressStart).toHaveBeenCalledTimes(1); expect(onPressEnd).toHaveBeenCalledTimes(1); expect(onPress).toHaveBeenCalledTimes(1); diff --git a/packages/react-aria-components/test/TagGroup.test.js b/packages/react-aria-components/test/TagGroup.test.js index adc4f07135b..ae058436d54 100644 --- a/packages/react-aria-components/test/TagGroup.test.js +++ b/packages/react-aria-components/test/TagGroup.test.js @@ -161,7 +161,10 @@ describe('TagGroup', () => { }); it('should support focus ring', async () => { - let {getAllByRole} = renderTagGroup({selectionMode: 'multiple'}, {}, {className: ({isFocusVisible}) => isFocusVisible ? 'focus' : ''}); + let onFocus = jest.fn(); + let onFocusChange = jest.fn(); + let onBlur = jest.fn(); + let {getAllByRole} = renderTagGroup({selectionMode: 'multiple'}, {}, {className: ({isFocusVisible}) => isFocusVisible ? 'focus' : '', onFocus, onFocusChange, onBlur}); let row = getAllByRole('row')[0]; expect(row).not.toHaveAttribute('data-focus-visible'); @@ -171,11 +174,20 @@ describe('TagGroup', () => { expect(document.activeElement).toBe(row); expect(row).toHaveAttribute('data-focus-visible', 'true'); expect(row).toHaveClass('focus'); + expect(onFocus).toHaveBeenCalledTimes(1); + expect(onFocusChange).toHaveBeenCalledTimes(1); + expect(onBlur).not.toHaveBeenCalled(); + onFocus.mockClear(); + onFocusChange.mockClear(); + onBlur.mockClear(); fireEvent.keyDown(row, {key: 'ArrowDown'}); fireEvent.keyUp(row, {key: 'ArrowDown'}); expect(row).not.toHaveAttribute('data-focus-visible'); expect(row).not.toHaveClass('focus'); + expect(onFocus).toHaveBeenCalledTimes(1); + expect(onFocusChange).toHaveBeenCalledTimes(2); // once for each tag + expect(onBlur).toHaveBeenCalledTimes(1); }); it('should support press state', async () => { From 732355bcc8d6ad8d0336c2b0f3b23569dd387c23 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Thu, 15 Jan 2026 18:27:22 -0600 Subject: [PATCH 4/5] add key to PressEvent (#9491) --- .../@react-aria/interactions/src/usePress.ts | 8 +- .../interactions/test/usePress.test.js | 129 ++++++++++++------ .../selection/src/useSelectableItem.ts | 18 ++- packages/@react-types/shared/src/events.d.ts | 5 + 4 files changed, 105 insertions(+), 55 deletions(-) diff --git a/packages/@react-aria/interactions/src/usePress.ts b/packages/@react-aria/interactions/src/usePress.ts index b9caaec7f6b..deea828c2a3 100644 --- a/packages/@react-aria/interactions/src/usePress.ts +++ b/packages/@react-aria/interactions/src/usePress.ts @@ -84,7 +84,8 @@ interface EventBase { altKey: boolean, clientX?: number, clientY?: number, - targetTouches?: Array<{clientX?: number, clientY?: number}> + targetTouches?: Array<{clientX?: number, clientY?: number}>, + key?: string } export interface PressResult { @@ -117,6 +118,7 @@ class PressEvent implements IPressEvent { altKey: boolean; x: number; y: number; + key: string | undefined; #shouldStopPropagation = true; constructor(type: IPressEvent['type'], pointerType: PointerType, originalEvent: EventBase, state?: PressState) { @@ -146,6 +148,7 @@ class PressEvent implements IPressEvent { this.altKey = originalEvent.altKey; this.x = x; this.y = y; + this.key = originalEvent.key; } continuePropagation() { @@ -983,7 +986,8 @@ function createEvent(target: FocusableElement, e: EventBase): EventBase { metaKey: e.metaKey, altKey: e.altKey, clientX, - clientY + clientY, + key: e.key }; } diff --git a/packages/@react-aria/interactions/test/usePress.test.js b/packages/@react-aria/interactions/test/usePress.test.js index 4d4af84ba29..48647ac57a1 100644 --- a/packages/@react-aria/interactions/test/usePress.test.js +++ b/packages/@react-aria/interactions/test/usePress.test.js @@ -2276,7 +2276,8 @@ describe('usePress', function () { shiftKey: false, altKey: false, x: 0, - y: 0 + y: 0, + key: ' ' }, { type: 'presschange', @@ -2291,7 +2292,8 @@ describe('usePress', function () { shiftKey: false, altKey: false, x: 0, - y: 0 + y: 0, + key: ' ' }, { type: 'pressend', @@ -2302,7 +2304,8 @@ describe('usePress', function () { shiftKey: false, altKey: false, x: 0, - y: 0 + y: 0, + key: ' ' }, { type: 'presschange', @@ -2317,7 +2320,8 @@ describe('usePress', function () { shiftKey: false, altKey: false, x: 0, - y: 0 + y: 0, + key: ' ' }, { type: 'click', @@ -2366,7 +2370,8 @@ describe('usePress', function () { shiftKey: false, altKey: false, x: 0, - y: 0 + y: 0, + key: 'Enter' }, { type: 'presschange', @@ -2381,7 +2386,8 @@ describe('usePress', function () { shiftKey: false, altKey: false, x: 0, - y: 0 + y: 0, + key: 'Enter' }, { type: 'pressend', @@ -2392,7 +2398,8 @@ describe('usePress', function () { shiftKey: false, altKey: false, x: 0, - y: 0 + y: 0, + key: 'Enter' }, { type: 'presschange', @@ -2407,7 +2414,8 @@ describe('usePress', function () { shiftKey: false, altKey: false, x: 0, - y: 0 + y: 0, + key: 'Enter' }, { type: 'click' @@ -2450,7 +2458,8 @@ describe('usePress', function () { shiftKey: false, altKey: false, x: 0, - y: 0 + y: 0, + key: 'Enter' }, { type: 'presschange', @@ -2465,7 +2474,8 @@ describe('usePress', function () { shiftKey: false, altKey: false, x: 0, - y: 0 + y: 0, + key: 'Enter' }, { type: 'pressend', @@ -2476,7 +2486,8 @@ describe('usePress', function () { shiftKey: false, altKey: false, x: 0, - y: 0 + y: 0, + key: 'Enter' }, { type: 'presschange', @@ -2491,7 +2502,8 @@ describe('usePress', function () { shiftKey: false, altKey: false, x: 0, - y: 0 + y: 0, + key: 'Enter' }, { type: 'click' @@ -2531,7 +2543,8 @@ describe('usePress', function () { shiftKey: false, altKey: false, x: 0, - y: 0 + y: 0, + key: ' ' }, { type: 'presschange', @@ -2546,7 +2559,8 @@ describe('usePress', function () { shiftKey: false, altKey: false, x: 0, - y: 0 + y: 0, + key: ' ' }, { type: 'pressend', @@ -2557,7 +2571,8 @@ describe('usePress', function () { shiftKey: false, altKey: false, x: 0, - y: 0 + y: 0, + key: ' ' }, { type: 'presschange', @@ -2572,7 +2587,8 @@ describe('usePress', function () { shiftKey: false, altKey: false, x: 0, - y: 0 + y: 0, + key: ' ' }, { type: 'click' @@ -2607,7 +2623,8 @@ describe('usePress', function () { shiftKey: true, altKey: false, x: 0, - y: 0 + y: 0, + key: ' ' }, { type: 'presschange', @@ -2622,7 +2639,8 @@ describe('usePress', function () { shiftKey: false, altKey: false, x: 0, - y: 0 + y: 0, + key: ' ' }, { type: 'pressend', @@ -2633,7 +2651,8 @@ describe('usePress', function () { shiftKey: false, altKey: false, x: 0, - y: 0 + y: 0, + key: ' ' }, { type: 'presschange', @@ -2648,7 +2667,8 @@ describe('usePress', function () { shiftKey: false, altKey: false, x: 0, - y: 0 + y: 0, + key: ' ' }, { type: 'click', @@ -2694,7 +2714,8 @@ describe('usePress', function () { shiftKey: false, altKey: false, x: 0, - y: 0 + y: 0, + key: 'Enter' }, { type: 'presschange', @@ -2709,7 +2730,8 @@ describe('usePress', function () { shiftKey: false, altKey: false, x: 0, - y: 0 + y: 0, + key: 'Enter' }, { type: 'pressend', @@ -2720,7 +2742,8 @@ describe('usePress', function () { shiftKey: false, altKey: false, x: 0, - y: 0 + y: 0, + key: 'Enter' }, { type: 'presschange', @@ -2735,7 +2758,8 @@ describe('usePress', function () { shiftKey: false, altKey: false, x: 0, - y: 0 + y: 0, + key: 'Enter' }, { type: 'click', @@ -2779,7 +2803,8 @@ describe('usePress', function () { shiftKey: false, altKey: false, x: 0, - y: 0 + y: 0, + key: ' ' }, { type: 'presschange', @@ -2794,7 +2819,8 @@ describe('usePress', function () { shiftKey: false, altKey: false, x: 0, - y: 0 + y: 0, + key: ' ' }, { type: 'presschange', @@ -2811,7 +2837,8 @@ describe('usePress', function () { shiftKey: false, altKey: false, x: 0, - y: 0 + y: 0, + key: ' ' }, { type: 'presschange', @@ -2826,7 +2853,8 @@ describe('usePress', function () { shiftKey: false, altKey: false, x: 0, - y: 0 + y: 0, + key: ' ' }, { type: 'pressend', @@ -2837,7 +2865,8 @@ describe('usePress', function () { shiftKey: false, altKey: false, x: 0, - y: 0 + y: 0, + key: ' ' }, { type: 'presschange', @@ -2852,7 +2881,8 @@ describe('usePress', function () { shiftKey: false, altKey: false, x: 0, - y: 0 + y: 0, + key: ' ' }, { type: 'click', @@ -2915,7 +2945,8 @@ describe('usePress', function () { shiftKey: false, altKey: false, x: 0, - y: 0 + y: 0, + key: ' ' }, { type: 'presschange', @@ -2935,7 +2966,8 @@ describe('usePress', function () { shiftKey: false, altKey: false, x: 0, - y: 0 + y: 0, + key: ' ' }, { type: 'presschange', @@ -2950,7 +2982,8 @@ describe('usePress', function () { shiftKey: false, altKey: false, x: 0, - y: 0 + y: 0, + key: ' ' }, { type: 'pressend', @@ -2961,7 +2994,8 @@ describe('usePress', function () { shiftKey: false, altKey: false, x: 0, - y: 0 + y: 0, + key: ' ' }, { type: 'presschange', @@ -2976,7 +3010,8 @@ describe('usePress', function () { shiftKey: false, altKey: false, x: 0, - y: 0 + y: 0, + key: ' ' }, { type: 'click', @@ -3726,7 +3761,8 @@ describe('usePress', function () { shiftKey: false, altKey: false, x: 0, - y: 0 + y: 0, + key: ' ' }, { type: 'presschange', @@ -3741,7 +3777,8 @@ describe('usePress', function () { shiftKey: false, altKey: false, x: 0, - y: 0 + y: 0, + key: ' ' }, { type: 'pressend', @@ -3752,7 +3789,8 @@ describe('usePress', function () { shiftKey: false, altKey: false, x: 0, - y: 0 + y: 0, + key: ' ' }, { type: 'presschange', @@ -3767,7 +3805,8 @@ describe('usePress', function () { shiftKey: false, altKey: false, x: 0, - y: 0 + y: 0, + key: ' ' } ]); }); @@ -4827,7 +4866,8 @@ describe('coordinates', () => { shiftKey: false, altKey: false, x: 50, - y: 50 + y: 50, + key: ' ' }, { type: 'presschange', @@ -4842,7 +4882,8 @@ describe('coordinates', () => { shiftKey: false, altKey: false, x: 50, - y: 50 + y: 50, + key: ' ' }, { type: 'pressend', @@ -4853,7 +4894,8 @@ describe('coordinates', () => { shiftKey: false, altKey: false, x: 50, - y: 50 + y: 50, + key: ' ' }, { type: 'presschange', @@ -4868,7 +4910,8 @@ describe('coordinates', () => { shiftKey: false, altKey: false, x: 50, - y: 50 + y: 50, + key: ' ' } ]); }); diff --git a/packages/@react-aria/selection/src/useSelectableItem.ts b/packages/@react-aria/selection/src/useSelectableItem.ts index cd6107a769b..1a307bf9790 100644 --- a/packages/@react-aria/selection/src/useSelectableItem.ts +++ b/packages/@react-aria/selection/src/useSelectableItem.ts @@ -246,7 +246,7 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte itemPressProps.onPressStart = (e) => { modality.current = e.pointerType; longPressEnabledOnPressStart.current = longPressEnabled; - if (e.pointerType === 'keyboard' && (!hasAction || isSelectionKey())) { + if (e.pointerType === 'keyboard' && (!hasAction || isSelectionKey(e.key))) { onSelect(e); } }; @@ -256,7 +256,7 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte if (!allowsDifferentPressOrigin) { itemPressProps.onPress = (e) => { if (hasPrimaryAction || (hasSecondaryAction && e.pointerType !== 'mouse')) { - if (e.pointerType === 'keyboard' && !isActionKey()) { + if (e.pointerType === 'keyboard' && !isActionKey(e.key)) { return; } @@ -290,7 +290,7 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte if ( allowsSelection && ( (e.pointerType === 'mouse' && !hasPrimaryAction) || - (e.pointerType === 'keyboard' && (!allowsActions || isSelectionKey())) + (e.pointerType === 'keyboard' && (!allowsActions || isSelectionKey(e.key))) ) ) { onSelect(e); @@ -305,7 +305,7 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte e.pointerType === 'touch' || e.pointerType === 'pen' || e.pointerType === 'virtual' || - (e.pointerType === 'keyboard' && hasAction && isActionKey()) || + (e.pointerType === 'keyboard' && hasAction && isActionKey(e.key)) || (e.pointerType === 'mouse' && hadPrimaryActionOnPressStart.current) ) { if (hasAction) { @@ -407,12 +407,10 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte }; } -function isActionKey() { - let event = window.event as KeyboardEvent; - return event?.key === 'Enter'; +function isActionKey(key: string | undefined) { + return key === 'Enter'; } -function isSelectionKey() { - let event = window.event as KeyboardEvent; - return event?.key === ' ' || event?.code === 'Space'; +function isSelectionKey(key: string | undefined) { + return key === ' '; } diff --git a/packages/@react-types/shared/src/events.d.ts b/packages/@react-types/shared/src/events.d.ts index e54c5e6d864..f9795946ca0 100644 --- a/packages/@react-types/shared/src/events.d.ts +++ b/packages/@react-types/shared/src/events.d.ts @@ -51,6 +51,11 @@ export interface PressEvent { x: number, /** Y position relative to the target. */ y: number, + /** + * The key that triggered the press event, if it was triggered by a keyboard interaction. + * This is useful for differentiating between Space and Enter key presses. + */ + key?: string, /** * By default, press events stop propagation to parent elements. * In cases where a handler decides not to handle a specific event, From 6f39c1df5b5c1a20a6b661f1cdd59b055b886d20 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Thu, 15 Jan 2026 18:31:09 -0600 Subject: [PATCH 5/5] docs: fix duplicate trailer bug (#9489) * remove rsc-html-stream patch * add script to verify fix * add patch * remove script to verify --- patches/@parcel+core+2.16.3.patch | 17 +++++++++ patches/rsc-html-stream+0.0.6.patch | 53 ----------------------------- 2 files changed, 17 insertions(+), 53 deletions(-) create mode 100644 patches/@parcel+core+2.16.3.patch delete mode 100644 patches/rsc-html-stream+0.0.6.patch diff --git a/patches/@parcel+core+2.16.3.patch b/patches/@parcel+core+2.16.3.patch new file mode 100644 index 00000000000..f9828c9cc33 --- /dev/null +++ b/patches/@parcel+core+2.16.3.patch @@ -0,0 +1,17 @@ +diff --git a/node_modules/@parcel/core/lib/requests/WriteBundleRequest.js b/node_modules/@parcel/core/lib/requests/WriteBundleRequest.js +index 1234567..abcdefg 100644 +--- a/node_modules/@parcel/core/lib/requests/WriteBundleRequest.js ++++ b/node_modules/@parcel/core/lib/requests/WriteBundleRequest.js +@@ -240,8 +240,10 @@ function replaceStream(hashRefToNameHash) { + lastMatchI = matchI + HASH_REF_PREFIX_LEN + _constants.HASH_REF_HASH_LEN; + } + } +- boundaryStr = replaced.subarray(replacedLength - BOUNDARY_LENGTH, replacedLength); +- let strUpToBoundary = replaced.subarray(0, replacedLength - BOUNDARY_LENGTH); ++ // Copy buffers to avoid reuse issues - subarray returns a view that can be ++ // corrupted if the underlying buffer is modified before the data is written ++ boundaryStr = Buffer.from(replaced.subarray(replacedLength - BOUNDARY_LENGTH, replacedLength)); ++ let strUpToBoundary = Buffer.from(replaced.subarray(0, replacedLength - BOUNDARY_LENGTH)); + cb(null, strUpToBoundary); + }, + flush(cb) { diff --git a/patches/rsc-html-stream+0.0.6.patch b/patches/rsc-html-stream+0.0.6.patch deleted file mode 100644 index baeca218f20..00000000000 --- a/patches/rsc-html-stream+0.0.6.patch +++ /dev/null @@ -1,53 +0,0 @@ -diff --git a/node_modules/rsc-html-stream/server.js b/node_modules/rsc-html-stream/server.js -index 1234567..abcdefg 100644 ---- a/node_modules/rsc-html-stream/server.js -+++ b/node_modules/rsc-html-stream/server.js -@@ -14,22 +14,22 @@ export function injectRSCPayload(rscStream, options) { - let buffered = []; - let timeout = null; - function flushBufferedChunks(controller) { -+ // Decode all buffered chunks together so we can reliably detect a trailer that -+ // might be split across chunk boundaries. -+ let combined = ''; - for (let chunk of buffered) { -- let buf = decoder.decode(chunk, {stream: true}); -- if (buf.endsWith(trailer)) { -- buf = buf.slice(0, -trailer.length); -- } -- controller.enqueue(encoder.encode(buf)); -+ combined += decoder.decode(chunk, {stream: true}); - } -+ combined += decoder.decode(); - -- let remaining = decoder.decode(); -- if (remaining.length) { -- if (remaining.endsWith(trailer)) { -- remaining = remaining.slice(0, -trailer.length); -- } -- controller.enqueue(encoder.encode(remaining)); -+ if (combined.endsWith(trailer)) { -+ combined = combined.slice(0, -trailer.length); - } - -+ if (combined.length) { -+ controller.enqueue(encoder.encode(combined)); -+ } -+ - buffered.length = 0; - timeout = null; - } -@@ -42,7 +42,13 @@ export function injectRSCPayload(rscStream, options) { - } - - timeout = setTimeout(async () => { -- flushBufferedChunks(controller); -+ try { -+ flushBufferedChunks(controller); -+ } catch (e) { -+ controller.error(e); -+ resolveFlightDataPromise(); -+ return; -+ } - if (!startedRSC) { - startedRSC = true; - writeRSCStream(rscStream, controller, nonce)