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 + + 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); 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/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-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} - + ); 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 - + )} 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 } 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 diff --git a/packages/dev/s2-docs/pages/s2/ListView.mdx b/packages/dev/s2-docs/pages/s2/ListView.mdx index 79dc2ee87b3..d7848262f11 100644 --- a/packages/dev/s2-docs/pages/s2/ListView.mdx +++ b/packages/dev/s2-docs/pages/s2/ListView.mdx @@ -19,7 +19,6 @@ import {ListView, ListViewItem, Text} from '@react-spectrum/s2'; import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; @@ -89,7 +88,7 @@ let images = [ ///- end collapse -/// - + {item => ( {/*- begin highlight -*/} @@ -108,7 +107,7 @@ let images = [ )} - + {item => ( {/*- begin highlight -*/}