From ea260713aa9226f601e3bbc6eb33e089a0461704 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Mon, 2 Mar 2026 19:28:13 -0600 Subject: [PATCH 1/6] docs(ActionBar): add TreeView example to ActionBar docs (#9718) * docs(ActionBar): add TreeView example to ActionBar docs * remove text from actionbar buttons to avoid overflow. --------- Co-authored-by: Daniel Lu --- packages/dev/s2-docs/pages/s2/ActionBar.mdx | 79 +++++++++++++++++---- 1 file changed, 65 insertions(+), 14 deletions(-) diff --git a/packages/dev/s2-docs/pages/s2/ActionBar.mdx b/packages/dev/s2-docs/pages/s2/ActionBar.mdx index 9d525fdbf71..a85707eac5c 100644 --- a/packages/dev/s2-docs/pages/s2/ActionBar.mdx +++ b/packages/dev/s2-docs/pages/s2/ActionBar.mdx @@ -10,11 +10,11 @@ export const description = 'Used when a user needs to perform actions on one or {docs.exports.ActionBar.description} - + ```tsx render docs={docs.exports.ActionBar} links={docs.links} props={['isEmphasized']} type="s2" wide "use client"; -import {ActionBar, ActionButton, TableView, TableHeader, TableBody, Column, Row, Cell, Text} from '@react-spectrum/s2'; +import {ActionBar, ActionButton, TableView, TableHeader, TableBody, Column, Row, Cell} from '@react-spectrum/s2'; import Edit from '@react-spectrum/s2/icons/Edit'; import Copy from '@react-spectrum/s2/icons/Copy'; import Delete from '@react-spectrum/s2/icons/Delete'; @@ -37,17 +37,14 @@ function Example(props) { renderActionBar={(selectedKeys) => ( /*- begin focus -*/ - alert('Edit action')}> + alert('Edit action')}> - Edit - alert('Copy action')}> + alert('Copy action')}> - Copy - alert('Delete action')}> + alert('Delete action')}> - Delete /*- end focus -*/ @@ -97,17 +94,14 @@ function Example(props) { renderActionBar={(selectedKeys) => ( /*- begin focus -*/ - alert('Edit action')}> + alert('Edit action')}> - Edit - alert('Copy action')}> + alert('Copy action')}> - Copy - alert('Delete action')}> + alert('Delete action')}> - Delete /*- end focus -*/ @@ -123,6 +117,63 @@ function Example(props) { } ``` +```tsx render docs={docs.exports.ActionBar} links={docs.links} props={['isEmphasized']} type="s2" wide +"use client"; +import {ActionBar, ActionButton, TreeView, TreeViewItem, TreeViewItemContent} from '@react-spectrum/s2'; +import Edit from '@react-spectrum/s2/icons/Edit'; +import Copy from '@react-spectrum/s2/icons/Copy'; +import Delete from '@react-spectrum/s2/icons/Delete'; +import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; + +function Example(props) { + return ( + ( + /*- begin focus -*/ + + alert('Edit action')}> + + + alert('Copy action')}> + + + alert('Delete action')}> + + + + /*- end focus -*/ + )}> + + Documents + + Project + + Weekly Report + + + Budget + + + + + Photos + + Image 1 + + + Image 2 + + + + ); +} +``` + ## API From cf8473575fd2fb169069256360a19e5e6ce70088 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Mon, 2 Mar 2026 17:48:07 -0800 Subject: [PATCH 2/6] fix: prevent calling focus on elements within template elements (#9712) * fix: prevent calling focus on elements within template elements * make check in focusSafely more robust and repalce dialog with div we can use a div instead because popovers render as dialogs anyways and this sidesteps the autofocus behavior of useDialog --- .../@react-aria/interactions/src/focusSafely.ts | 4 ++++ .../@react-spectrum/s2/src/ContextualHelp.tsx | 16 +++++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/@react-aria/interactions/src/focusSafely.ts b/packages/@react-aria/interactions/src/focusSafely.ts index cd171d34950..8164ad90893 100644 --- a/packages/@react-aria/interactions/src/focusSafely.ts +++ b/packages/@react-aria/interactions/src/focusSafely.ts @@ -24,6 +24,10 @@ import {getInteractionModality} from './useFocusVisible'; * as page scrolling and screen reader issues with CSS transitions. */ export function focusSafely(element: FocusableElement): void { + if (!element.isConnected) { + return; + } + // If the user is interacting with a virtual cursor, e.g. screen reader, then // wait until after any animated transitions that are currently occurring on // the page before shifting focus. This avoids issues with VoiceOver on iOS diff --git a/packages/@react-spectrum/s2/src/ContextualHelp.tsx b/packages/@react-spectrum/s2/src/ContextualHelp.tsx index 6d445704021..8f9e20eb039 100644 --- a/packages/@react-spectrum/s2/src/ContextualHelp.tsx +++ b/packages/@react-spectrum/s2/src/ContextualHelp.tsx @@ -1,11 +1,11 @@ import {ActionButton} from './ActionButton'; import {AriaLabelingProps, DOMProps, FocusableRef, FocusableRefValue} from '@react-types/shared'; import {ContentContext, FooterContext, HeadingContext, TextContext as SpectrumTextContext} from './Content'; -import {ContextValue, DEFAULT_SLOT, Provider, Dialog as RACDialog, TextContext} from 'react-aria-components'; +import {ContextValue, DEFAULT_SLOT, Provider, TextContext} from 'react-aria-components'; import {createContext, forwardRef, ReactNode} from 'react'; import {dialogInner} from './Dialog'; import {DialogTrigger, DialogTriggerProps} from './DialogTrigger'; -import {filterDOMProps, mergeProps, useLabels, useSlotId} from '@react-aria/utils'; +import {filterDOMProps, mergeProps, useId, useLabels} from '@react-aria/utils'; import HelpIcon from '../s2wf-icons/S2_Icon_HelpCircle_20_N.svg'; import InfoIcon from '../s2wf-icons/S2_Icon_InfoCircle_20_N.svg'; // @ts-ignore @@ -44,16 +44,18 @@ const headingStyles = style({ */ export function ContextualHelpPopover(props: ContextualHelpPopoverProps) { let {children, ...popoverProps} = props; - let titleId = useSlotId(); + let titleId = useId(); + return (
- {children} - +
); From 98d99cbe29e313962a8592954cb06846576e95fc Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Mon, 2 Mar 2026 20:08:17 -0600 Subject: [PATCH 3/6] fix(TableView): updated disabled behavior to match ListView/TreeView (#9720) * fix(TableView): hide row selection checkbox if disabled (for all disabledBehaviors). * use disabled text color for disabledBehavior="all" --- packages/@react-spectrum/s2/src/TableView.tsx | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index 85a9d79c19c..fac45b68db6 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -947,21 +947,13 @@ const commonCellStyles = { borderXWidth: 0, borderStyle: 'solid', position: 'relative', - color: { - default: 'gray-800', - forcedColors: 'ButtonText' - }, + color: '--rowTextColor', outlineStyle: 'none', paddingX: 16 // table-edge-to-content } as const; const cell = style({ ...commonCellStyles, - color: { - default: baseColor('neutral-subdued'), - isSelected: baseColor('neutral'), - forcedColors: 'ButtonText' - }, paddingY: centerPadding(), minHeight: { default: 40, @@ -1398,6 +1390,16 @@ const rowBackgroundColor = { } } as const; +const rowTextColor = { + default: baseColor('neutral-subdued'), + isSelected: baseColor('neutral'), + isDisabled: { + default: 'disabled', + forcedColors: 'GrayText' + }, + forcedColors: 'ButtonText' +} as const; + const row = style({ height: 'full', position: 'relative', @@ -1407,6 +1409,10 @@ const row = style({ type: 'backgroundColor', value: rowBackgroundColor }, + '--rowTextColor': { + type: 'color', + value: rowTextColor + }, '--rowFocusIndicatorColor': { type: 'outlineColor', value: { @@ -1451,6 +1457,13 @@ const row = style({ forcedColorAdjust: 'none' }); +const selectionCheckbox = style({ + visibility: { + default: 'visible', + ':is([slot="selection"][data-disabled="true"])': 'hidden' + } +}); + export interface RowProps extends Pick, 'id' | 'columns' | 'isDisabled' | 'onAction' | 'children' | 'textValue' | 'dependencies' | keyof GlobalDOMAttributes>, LinkDOMProps {} /** @@ -1477,7 +1490,7 @@ export const Row = /*#__PURE__*/ (forwardRef as forwardRefType)(function Row - + )} From 2fd0040fc37eb7c1d5361da9b6be81db5af582f1 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Mon, 2 Mar 2026 20:12:42 -0600 Subject: [PATCH 4/6] chore: update example apps with ListView and Unavailable MenuItem examples (#9722) * remove extra aria-label from docs example * add ListView to example apps * add unavailable menu item example to docs * enable verdaccio * update ListView slots example to prevent scrolling on mobile. * Revert "enable verdaccio" This reverts commit b6c1751d45ad1700b1a5c912c5b7d0408330c386. --- examples/s2-next-macros/src/app/page.tsx | 32 ++++++++++++++++++++-- examples/s2-parcel-example/src/App.js | 32 ++++++++++++++++++++-- examples/s2-webpack-5-example/src/App.js | 32 ++++++++++++++++++++-- packages/dev/s2-docs/pages/s2/ListView.mdx | 5 ++-- 4 files changed, 92 insertions(+), 9 deletions(-) diff --git a/examples/s2-next-macros/src/app/page.tsx b/examples/s2-next-macros/src/app/page.tsx index 44456c9bf0e..536b5a2cb78 100644 --- a/examples/s2-next-macros/src/app/page.tsx +++ b/examples/s2-next-macros/src/app/page.tsx @@ -23,9 +23,13 @@ import { ButtonGroup, Cell, Column, + Content, + ContextualHelpPopover, Divider, Heading, LinkButton, + ListView, + ListViewItem, Menu, MenuItem, MenuTrigger, @@ -43,7 +47,8 @@ import { ToggleButtonGroup, TreeView, TreeViewItem, - TreeViewItemContent + TreeViewItemContent, + UnavailableMenuItemTrigger } from "@react-spectrum/s2"; import Edit from "@react-spectrum/s2/icons/Edit"; import FileTxt from "@react-spectrum/s2/icons/FileText"; @@ -170,7 +175,13 @@ function App() { SMS - Delete + + Delete + + Permission required + Contact your administrator for permissions to delete. + + @@ -184,6 +195,23 @@ function App() { Paste + + + Adobe Photoshop + Image editing software + + + Adobe XD + UI/UX design tool + + + Adobe InDesign + Desktop publishing + + SMS - Delete + + Delete + + Permission required + Contact your administrator for permissions to delete. + + @@ -182,6 +193,23 @@ function App() { Paste + + + Adobe Photoshop + Image editing software + + + Adobe XD + UI/UX design tool + + + Adobe InDesign + Desktop publishing + + SMS - Delete + + Delete + + Permission required + Contact your administrator for permissions to delete. + + @@ -182,6 +193,23 @@ function App() { Paste + + + Adobe Photoshop + Image editing software + + + Adobe XD + UI/UX design tool + + + Adobe InDesign + Desktop publishing + + @@ -89,7 +88,7 @@ let images = [ ///- end collapse -///
- + {item => ( {/*- begin highlight -*/} @@ -108,7 +107,7 @@ let images = [ )} - + {item => ( {/*- begin highlight -*/} From 7b9da59467484d6973ea707104027900b96d58c3 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Mon, 2 Mar 2026 18:20:58 -0800 Subject: [PATCH 5/6] fix: Tree keyboard drag and drop should skip content nodes (#9724) --- .../dnd/src/DropTargetKeyboardNavigation.ts | 22 ++++++---- .../DropTargetKeyboardNavigation.test.tsx | 42 ++++++++++++++++++- 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/packages/@react-aria/dnd/src/DropTargetKeyboardNavigation.ts b/packages/@react-aria/dnd/src/DropTargetKeyboardNavigation.ts index 7fbec801b48..d10c797d525 100644 --- a/packages/@react-aria/dnd/src/DropTargetKeyboardNavigation.ts +++ b/packages/@react-aria/dnd/src/DropTargetKeyboardNavigation.ts @@ -58,12 +58,7 @@ function nextDropTarget( } else { nextKey = keyboardDelegate.getKeyBelow?.(target.key); } - let nextCollectionKey = collection.getKeyAfter(target.key); - let nextCollectionNode = nextCollectionKey && collection.getItem(nextCollectionKey); - while (nextCollectionNode && nextCollectionNode.type === 'content') { - nextCollectionKey = nextCollectionKey ? collection.getKeyAfter(nextCollectionKey) : null; - nextCollectionNode = nextCollectionKey && collection.getItem(nextCollectionKey); - } + let nextCollectionKey = getNextItem(collection, target.key, key => collection.getKeyAfter(key)); // If the keyboard delegate did not move to the next key in the collection, // jump to that key with the same drop position. Otherwise, try the other @@ -191,7 +186,7 @@ function previousDropTarget( } else { prevKey = keyboardDelegate.getKeyAbove?.(target.key); } - let prevCollectionKey = collection.getKeyBefore(target.key); + let prevCollectionKey = getNextItem(collection, target.key, key => collection.getKeyBefore(key)); // If the keyboard delegate did not move to the next key in the collection, // jump to that key with the same drop position. Otherwise, try the other @@ -263,7 +258,7 @@ function getLastChild(collection: Collection>, key: Key): DropTarg // getChildNodes still returns child tree items even when the item is collapsed. // Checking if the next item has a greater level is a silly way to determine if the item is expanded. let targetNode = collection.getItem(key); - let nextKey = collection.getKeyAfter(key); + let nextKey = getNextItem(collection, key, key => collection.getKeyAfter(key)); let nextNode = nextKey != null ? collection.getItem(nextKey) : null; if (targetNode && nextNode && nextNode.level > targetNode.level) { let children = getChildNodes(targetNode, collection); @@ -285,3 +280,14 @@ function getLastChild(collection: Collection>, key: Key): DropTarg return null; } + +// Find the next or previous item in a collection, skipping over other types of nodes (e.g. content). +function getNextItem(collection: Collection>, key: Key, getNextKey: (key: Key) => Key | null): Key | null { + let nextCollectionKey = getNextKey(key); + let nextCollectionNode = nextCollectionKey != null ? collection.getItem(nextCollectionKey) : null; + while (nextCollectionNode && nextCollectionNode.type !== 'item') { + nextCollectionKey = getNextKey(nextCollectionNode.key); + nextCollectionNode = nextCollectionKey != null ? collection.getItem(nextCollectionKey) : null; + } + return nextCollectionKey; +} diff --git a/packages/@react-aria/dnd/test/DropTargetKeyboardNavigation.test.tsx b/packages/@react-aria/dnd/test/DropTargetKeyboardNavigation.test.tsx index bbf7a1c09dc..1fddd436602 100644 --- a/packages/@react-aria/dnd/test/DropTargetKeyboardNavigation.test.tsx +++ b/packages/@react-aria/dnd/test/DropTargetKeyboardNavigation.test.tsx @@ -48,7 +48,7 @@ class TestCollection implements Collection> { constructor(items: Item[]) { this.map = new Map>(); let visitItem = (item: Item, index: number, level = 0, parentKey: string | null = null): Node => { - let childNodes = item.childItems ? visitItems(item.childItems, level + 1, item.id) : []; + let childNodes = visitItems(item.childItems ?? [], level + 1, item.id); return { type: 'item', key: item.id, @@ -67,6 +67,24 @@ class TestCollection implements Collection> { let last: Node | null = null; let index = 0; let nodes: Node[] = []; + if (parentKey != null) { + let contentNode = { + type: 'content', + key: parentKey + '-content', + value: null, + level: level, + hasChildNodes: false, + childNodes: [], + rendered: null, + textValue: '', + index: index++, + parentKey + }; + this.map.set(contentNode.key, contentNode); + nodes.push(contentNode); + last = contentNode; + } + for (let item of items) { let node = visitItem(item, index, level, parentKey); this.map.set(item.id, node); @@ -236,25 +254,45 @@ describe('drop target keyboard navigation', () => { let expectedKeys = [ 'projects', + 'projects-content', 'project-1', + 'project-1-content', 'project-2', + 'project-2-content', 'project-2A', + 'project-2A-content', 'project-2B', + 'project-2B-content', 'project-2C', + 'project-2C-content', 'project-3', + 'project-3-content', 'project-4', + 'project-4-content', 'project-5', + 'project-5-content', 'project-5A', + 'project-5A-content', 'project-5B', + 'project-5B-content', 'project-5C', + 'project-5C-content', 'reports', + 'reports-content', 'reports-1', + 'reports-1-content', 'reports-1A', + 'reports-1A-content', 'reports-1AB', + 'reports-1AB-content', 'reports-1ABC', + 'reports-1ABC-content', 'reports-1B', + 'reports-1B-content', 'reports-1C', - 'reports-2' + 'reports-1C-content', + 'reports-2', + 'reports-2-content' ]; expect(nextKeys).toEqual(expectedKeys); From cdbfe4816963758432b4c3bcef436bb72e8a08e3 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Mon, 2 Mar 2026 18:26:48 -0800 Subject: [PATCH 6/6] chore: fixing defaultSelectedKey type consistency and removing unneeded import (#9725) * chore: make defaultSelectedKey consistent with selectedKey * chore: dont import isLocalTimeZoneOverridden * omit onKeydown/up from combobox item since virtual focus --- packages/@internationalized/date/src/index.ts | 1 - packages/@internationalized/date/tests/queries.test.js | 3 ++- packages/@react-spectrum/s2/src/ComboBox.tsx | 2 +- packages/@react-types/combobox/src/index.d.ts | 2 +- packages/@react-types/select/src/index.d.ts | 2 +- packages/@react-types/shared/src/selection.d.ts | 2 +- packages/@react-types/tabs/src/index.d.ts | 8 ++++++-- 7 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/@internationalized/date/src/index.ts b/packages/@internationalized/date/src/index.ts index 02752a6045d..5d373c842f1 100644 --- a/packages/@internationalized/date/src/index.ts +++ b/packages/@internationalized/date/src/index.ts @@ -66,7 +66,6 @@ export { getLocalTimeZone, setLocalTimeZone, resetLocalTimeZone, - isLocalTimeZoneOverridden, startOfMonth, startOfWeek, startOfYear, diff --git a/packages/@internationalized/date/tests/queries.test.js b/packages/@internationalized/date/tests/queries.test.js index a80166ed0db..cb9f5b553ca 100644 --- a/packages/@internationalized/date/tests/queries.test.js +++ b/packages/@internationalized/date/tests/queries.test.js @@ -25,7 +25,6 @@ import { isEqualMonth, isEqualYear, IslamicUmalquraCalendar, - isLocalTimeZoneOverridden, isSameDay, isSameMonth, isSameYear, @@ -40,6 +39,8 @@ import { startOfYear, ZonedDateTime } from '..'; +import {isLocalTimeZoneOverridden} from '../src/queries'; + describe('queries', function () { describe('isSameDay', function () { diff --git a/packages/@react-spectrum/s2/src/ComboBox.tsx b/packages/@react-spectrum/s2/src/ComboBox.tsx index 51247090172..32bbeb3feb8 100644 --- a/packages/@react-spectrum/s2/src/ComboBox.tsx +++ b/packages/@react-spectrum/s2/src/ComboBox.tsx @@ -369,7 +369,7 @@ export const ComboBox = /*#__PURE__*/ (forwardRef as forwardRefType)(function Co ); }); -export interface ComboBoxItemProps extends Omit, StyleProps { +export interface ComboBoxItemProps extends Omit, StyleProps { children: ReactNode } diff --git a/packages/@react-types/combobox/src/index.d.ts b/packages/@react-types/combobox/src/index.d.ts index 3eefa51123e..7a00ecf44f6 100644 --- a/packages/@react-types/combobox/src/index.d.ts +++ b/packages/@react-types/combobox/src/index.d.ts @@ -71,7 +71,7 @@ export interface ComboBoxProps extends Co * The initial selected key in the collection (uncontrolled). * @deprecated */ - defaultSelectedKey?: Key, + defaultSelectedKey?: Key | null, /** * Handler that is called when the selection changes. * @deprecated diff --git a/packages/@react-types/select/src/index.d.ts b/packages/@react-types/select/src/index.d.ts index e30db1813cc..edb951aab63 100644 --- a/packages/@react-types/select/src/index.d.ts +++ b/packages/@react-types/select/src/index.d.ts @@ -51,7 +51,7 @@ export interface SelectProps extends Coll * The initial selected key in the collection (uncontrolled). * @deprecated */ - defaultSelectedKey?: Key, + defaultSelectedKey?: Key | null, /** * Handler that is called when the selection changes. * @deprecated diff --git a/packages/@react-types/shared/src/selection.d.ts b/packages/@react-types/shared/src/selection.d.ts index e59aaf3e435..7327677b8a7 100644 --- a/packages/@react-types/shared/src/selection.d.ts +++ b/packages/@react-types/shared/src/selection.d.ts @@ -18,7 +18,7 @@ export interface SingleSelection { /** The currently selected key in the collection (controlled). */ selectedKey?: Key | null, /** The initial selected key in the collection (uncontrolled). */ - defaultSelectedKey?: Key, + defaultSelectedKey?: Key | null, /** Handler that is called when the selection changes. */ onSelectionChange?: (key: Key | null) => void } diff --git a/packages/@react-types/tabs/src/index.d.ts b/packages/@react-types/tabs/src/index.d.ts index 4f17abe0e3c..6dbac3214ba 100644 --- a/packages/@react-types/tabs/src/index.d.ts +++ b/packages/@react-types/tabs/src/index.d.ts @@ -30,7 +30,7 @@ export interface AriaTabProps extends AriaLabelingProps { shouldSelectOnPressUp?: boolean } -export interface TabListProps extends CollectionBase, Omit { +export interface TabListProps extends CollectionBase, Omit { /** * Whether the TabList is disabled. * Shows that a selection exists, but is not available in that circumstance. @@ -38,6 +38,8 @@ export interface TabListProps extends CollectionBase, Omit void } @@ -62,7 +64,7 @@ export interface AriaTabPanelProps extends Omit, AriaLabelingPro id?: Key } -export interface SpectrumTabsProps extends AriaTabListBase, Omit, DOMProps, StyleProps { +export interface SpectrumTabsProps extends AriaTabListBase, Omit, DOMProps, StyleProps { /** The children of the `` element. Should include `` and `` elements. */ children: ReactNode, /** The item objects for each tab, for dynamic collections. */ @@ -79,6 +81,8 @@ export interface SpectrumTabsProps extends AriaTabListBase, Omit void }