From ecf9415c70080b81f7aad60f1374938d903a9d6b Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Thu, 3 Jul 2025 18:43:26 +1000 Subject: [PATCH 01/60] feat: s2 ListView --- packages/@react-spectrum/s2/src/ListView.tsx | 286 ++++++++++++++++++ packages/@react-spectrum/s2/src/Picker.tsx | 21 -- packages/@react-spectrum/s2/src/TreeView.tsx | 7 +- packages/@react-spectrum/s2/src/index.ts | 4 +- .../s2/stories/ListView.stories.tsx | 179 +++++++++++ .../react-aria-components/src/GridList.tsx | 2 + 6 files changed, 476 insertions(+), 23 deletions(-) create mode 100644 packages/@react-spectrum/s2/src/ListView.tsx create mode 100644 packages/@react-spectrum/s2/stories/ListView.stories.tsx diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx new file mode 100644 index 00000000000..443ac565a89 --- /dev/null +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -0,0 +1,286 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import { + DEFAULT_SLOT, + GridList, + GridListItem, + GridListItemProps, + GridListItemRenderProps, + GridListProps, + GridListRenderProps, + ListLayout, + ListStateContext, + Provider, + useContextProps, + useLocale, + Virtualizer +} from 'react-aria-components'; +import {JSXElementConstructor, ReactElement, ReactNode, createContext, forwardRef, useContext, useRef} from 'react'; +import {ContextValue, SlotProps} from 'react-aria-components'; +import {DOMProps, DOMRef, DOMRefValue} from '@react-types/shared'; +import {control, controlFont, controlSize, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; +import {useSpectrumContextProps} from './useSpectrumContextProps'; +import {baseColor, edgeToText, focusRing, fontRelative, space, style} from '../style' with {type: 'macro'}; +import { useScale } from './utils'; +import { useDOMRef } from '@react-spectrum/utils'; +import { pressScale } from './pressScale'; +import { IconContext } from './Icon'; +import { centerBaseline } from './CenterBaseline'; +import { Text, TextContext } from './Content'; +import { ImageContext } from './Image'; +import { ActionButtonGroupContext } from './ActionButtonGroup'; + +export interface ListViewProps extends GridListProps, DOMProps, StyleProps, ListViewStylesProps, SlotProps { + /** + * Whether to automatically focus the Inline Alert when it first renders. + */ + autoFocus?: boolean +} + +interface ListViewStylesProps { + isQuiet?: boolean +} + +export interface ListViewItemProps extends Omit, StyleProps { + /** + * The contents of the item. + */ + children: ReactNode +} + +interface ListViewRendererContextValue { + renderer?: (item) => ReactElement> +} +const ListViewRendererContext = createContext({}); + +export const ListViewContext = createContext>, DOMRefValue>>(null); + +let InternalListViewContext = createContext<{isQuiet?: boolean}>({}); + +const listView = style({ + ...focusRing(), + outlineOffset: -2, // make certain we are visible inside overflow hidden containers + userSelect: 'none', + minHeight: 0, + minWidth: 0, + width: 'full', + height: 'full', + boxSizing: 'border-box', + overflow: 'auto', + fontSize: controlFont(), + borderRadius: 'default', + borderColor: 'gray-300', + borderWidth: 1, + borderStyle: 'solid' +}, getAllowedOverrides()); + +export const ListView = /*#__PURE__*/ forwardRef(function ListView( + props: ListViewProps, + ref: DOMRef +): ReactNode { + [props, ref] = useSpectrumContextProps(props, ref, ListViewContext); + let {children, isQuiet} = props; + let scale = useScale(); + + let renderer; + if (typeof children === 'function') { + renderer = children; + } + + let domRef = useDOMRef(ref); + + return ( + + + + (props.UNSAFE_className || '') + listView({ + ...renderProps, + isQuiet + }, props.styles)}> + {children} + + + + + ); +}); + +const listitem = style({ + ...focusRing(), + outlineOffset: 0, + columnGap: 0, + paddingX: 0, + paddingBottom: '--labelPadding', + backgroundColor: { + default: 'transparent', + isFocused: baseColor('gray-100').isFocusVisible + }, + color: { + default: baseColor('neutral'), + isDisabled: { + default: 'disabled', + forcedColors: 'GrayText' + } + }, + position: 'relative', + gridColumnStart: 1, + gridColumnEnd: -1, + display: 'grid', + gridTemplateAreas: [ + '. checkmark icon label actions chevron .', + '. . . description actions chevron .' + ], + gridTemplateColumns: [edgeToText(12), 'auto', 'auto', 'minmax(0, 1fr)', 'auto', 'auto', edgeToText(12)], + gridTemplateRows: '1fr auto', + rowGap: { + ':has([slot=description])': space(1) + }, + alignItems: 'baseline', + height: 'full', + textDecoration: 'none', + cursor: { + default: 'default', + isLink: 'pointer' + }, + transition: 'default', + borderColor: { + default: 'gray-300', + forcedColors: 'ButtonBorder' + }, + borderBottomWidth: 1, + borderTopWidth: 0, + borderXWidth: 0, + borderStyle: 'solid', + borderTopRadius: { + default: 'none', + isFirstItem: 'default' + }, + borderBottomRadius: { + default: 'none', + isLastItem: 'default' + } +}, getAllowedOverrides()); + +export let label = style({ + gridArea: 'label', + alignSelf: 'center', + font: controlFont(), + color: 'inherit', + fontWeight: 'medium', + // TODO: token values for padding not defined yet, revisit + marginTop: '--labelPadding' +}); + +export let description = style({ + gridArea: 'description', + alignSelf: 'center', + font: 'ui-sm', + color: { + default: baseColor('neutral-subdued'), + // Ideally this would use the same token as hover, but we don't have access to that here. + // TODO: should we always consider isHovered and isFocused to be the same thing? + isFocused: 'gray-800', + isDisabled: 'disabled' + }, + transition: 'default' +}); + +export let iconCenterWrapper = style({ + display: 'flex', + gridArea: 'icon', + alignSelf: 'center' +}); + +export let icon = style({ + display: 'block', + size: fontRelative(20), + // too small default icon size is wrong, it's like the icons are 1 tshirt size bigger than the rest of the component? check again after typography changes + // reminder, size of WF is applied via font size + marginEnd: 'text-to-visual', + '--iconPrimary': { + type: 'fill', + value: 'currentColor' + } +}); + +let image = style({ + gridArea: 'icon', + gridRowEnd: 'span 2', + marginEnd: 'text-to-visual', + alignSelf: 'center', + borderRadius: 'sm', + height: 'calc(100% - 12px)', + aspectRatio: 'square', + objectFit: 'contain' +}); + +let actionButtonGroup = style({ + gridArea: 'actions', + gridRowEnd: 'span 2', + alignSelf: 'center', + justifySelf: 'end' +}); + +export function ListViewItem(props: ListViewItemProps): ReactNode { + let ref = useRef(null); + let isLink = props.href != null; + let isLinkOut = isLink && props.target === '_blank'; + let {isQuiet} = useContext(InternalListViewContext); + let textValue = props.textValue || (typeof props.children === 'string' ? props.children : undefined); + let {direction} = useLocale(); + return ( + (props.UNSAFE_className || '') + listitem({ + ...renderProps, + isLink, + isQuiet + }, props.styles)}> + {(renderProps) => { + let {children} = props; + return ( + + {typeof children === 'string' ? {children} : children} + + ); + }} + + ); +} diff --git a/packages/@react-spectrum/s2/src/Picker.tsx b/packages/@react-spectrum/s2/src/Picker.tsx index edc7b55383e..e57231ea6bb 100644 --- a/packages/@react-spectrum/s2/src/Picker.tsx +++ b/packages/@react-spectrum/s2/src/Picker.tsx @@ -178,27 +178,6 @@ const quietFocusLine = style({ } }); -export let menu = style({ - outlineStyle: 'none', - display: 'grid', - width: 'full', - gridTemplateColumns: { - size: { - S: [edgeToText(24), 'auto', 'auto', 'minmax(0, 1fr)', 'auto', 'auto', 'auto', edgeToText(24)], - M: [edgeToText(32), 'auto', 'auto', 'minmax(0, 1fr)', 'auto', 'auto', 'auto', edgeToText(32)], - L: [edgeToText(40), 'auto', 'auto', 'minmax(0, 1fr)', 'auto', 'auto', 'auto', edgeToText(40)], - XL: [edgeToText(48), 'auto', 'auto', 'minmax(0, 1fr)', 'auto', 'auto', 'auto', edgeToText(48)] - } - }, - boxSizing: 'border-box', - maxHeight: 'inherit', - overflow: 'auto', - padding: 8, - fontFamily: 'sans', - fontSize: controlFont(), - gridAutoRows: 'min-content' -}); - const invalidBorder = style({ ...controlBorderRadius(), position: 'absolute', diff --git a/packages/@react-spectrum/s2/src/TreeView.tsx b/packages/@react-spectrum/s2/src/TreeView.tsx index 9c6062710a9..2cb632a9726 100644 --- a/packages/@react-spectrum/s2/src/TreeView.tsx +++ b/packages/@react-spectrum/s2/src/TreeView.tsx @@ -15,6 +15,7 @@ import {ActionMenuContext} from './ActionMenu'; import { Button, ButtonContext, + ContextValue, ListLayout, Provider, TreeItemProps as RACTreeItemProps, @@ -30,7 +31,7 @@ import {centerBaseline} from './CenterBaseline'; import {Checkbox} from './Checkbox'; import Chevron from '../ui-icons/Chevron'; import {colorMix, focusRing, fontRelative, lightDark, style} from '../style' with {type: 'macro'}; -import {DOMRef, Key} from '@react-types/shared'; +import {DOMRef, DOMRefValue, Key} from '@react-types/shared'; import {getAllowedOverrides, StylesPropWithHeight, UnsafeStyles} from './style-utils' with {type: 'macro'}; import {IconContext} from './Icon'; import {raw} from '../style/style-macro' with {type: 'macro'}; @@ -39,6 +40,7 @@ import {TextContext} from './Content'; import {useDOMRef} from '@react-spectrum/utils'; import {useLocale} from 'react-aria'; import {useScale} from './utils'; +import { useSpectrumContextProps } from './useSpectrumContextProps'; interface S2TreeProps { // Only detatched is supported right now with the current styles from Spectrum @@ -63,6 +65,8 @@ interface TreeRendererContextValue { } const TreeRendererContext = createContext({}); +export const TreeViewContext = createContext, DOMRefValue>>(null); + let InternalTreeContext = createContext<{isDetached?: boolean, isEmphasized?: boolean}>({}); @@ -92,6 +96,7 @@ const tree = style({ }, getAllowedOverrides({height: true})); function TreeView(props: TreeViewProps, ref: DOMRef) { + [props, ref] = useSpectrumContextProps(props, ref, TreeViewContext); let {children, isDetached, isEmphasized, UNSAFE_className, UNSAFE_style} = props; let scale = useScale(); diff --git a/packages/@react-spectrum/s2/src/index.ts b/packages/@react-spectrum/s2/src/index.ts index bd022c26ddc..8df90c6fe2c 100644 --- a/packages/@react-spectrum/s2/src/index.ts +++ b/packages/@react-spectrum/s2/src/index.ts @@ -53,6 +53,7 @@ export {Image, ImageContext} from './Image'; export {ImageCoordinator} from './ImageCoordinator'; export {InlineAlert, InlineAlertContext} from './InlineAlert'; export {Link, LinkContext} from './Link'; +export {ListView, ListViewItem} from './ListView'; export {MenuItem, MenuTrigger, Menu, MenuSection, SubmenuTrigger, MenuContext} from './Menu'; export {Meter, MeterContext} from './Meter'; export {NotificationBadge, NotificationBadgeContext} from './NotificationBadge'; @@ -80,7 +81,7 @@ export {ToastContainer as UNSTABLE_ToastContainer, ToastQueue as UNSTABLE_ToastQ export {ToggleButton, ToggleButtonContext} from './ToggleButton'; export {ToggleButtonGroup, ToggleButtonGroupContext} from './ToggleButtonGroup'; export {Tooltip, TooltipTrigger} from './Tooltip'; -export {TreeView, TreeViewItem, TreeViewItemContent} from './TreeView'; +export {TreeView, TreeViewItem, TreeViewItemContent, TreeViewContext} from './TreeView'; export {pressScale} from './pressScale'; @@ -126,6 +127,7 @@ export type {InlineAlertProps} from './InlineAlert'; export type {ImageProps} from './Image'; export type {ImageCoordinatorProps} from './ImageCoordinator'; export type {LinkProps} from './Link'; +export type {ListViewProps, ListViewItemProps} from './ListView'; export type {MenuTriggerProps, MenuProps, MenuItemProps, MenuSectionProps, SubmenuTriggerProps} from './Menu'; export type {MeterProps} from './Meter'; export type {NotificationBadgeProps} from './NotificationBadge'; diff --git a/packages/@react-spectrum/s2/stories/ListView.stories.tsx b/packages/@react-spectrum/s2/stories/ListView.stories.tsx new file mode 100644 index 00000000000..744a5e1e139 --- /dev/null +++ b/packages/@react-spectrum/s2/stories/ListView.stories.tsx @@ -0,0 +1,179 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {categorizeArgTypes} from './utils'; +import {ListView, ListViewItem, Text, Image, ActionButton, ActionButtonGroup} from '../'; +import type {Meta, StoryObj} from '@storybook/react'; +import { Key } from 'react-aria'; +import Folder from '../s2wf-icons/S2_Icon_Folder_20_N.svg'; +import File from '../s2wf-icons/S2_Icon_File_20_N.svg'; +import Edit from '../s2wf-icons/S2_Icon_Edit_20_N.svg'; +import Delete from '../s2wf-icons/S2_Icon_Delete_20_N.svg'; +import { ReactNode } from 'react'; + +const meta: Meta = { + component: ListView, + parameters: { + layout: 'centered' + }, + tags: ['autodocs'], + argTypes: { + ...categorizeArgTypes('Events', ['onSelectionChange']) + }, + title: 'ListView', + decorators: [ + (Story) => ( +
+ +
+ ) + ] +}; + +export default meta; +type Story = StoryObj; + +export const Example: Story = { + args: { + 'aria-label': 'Birthday', + children: ( + <> + + Item 1 + + + Item 2 + + + Item 3 + + + ) + } +}; + +interface Item { + id: Key, + name: string, + type?: 'file' | 'folder', + children?: Item[] +} + +const items: Item[] = [ + {id: 'a', name: 'Adobe Photoshop', type: 'file'}, + {id: 'b', name: 'Adobe XD', type: 'file'}, + {id: 'c', name: 'Documents', type: 'folder', children: [ + {id: 1, name: 'Sales Pitch'}, + {id: 2, name: 'Demo'}, + {id: 3, name: 'Taxes'} + ]}, + {id: 'd', name: 'Adobe InDesign', type: 'file'}, + {id: 'e', name: 'Utilities', type: 'folder', children: [ + {id: 1, name: 'Activity Monitor'} + ]}, + {id: 'f', name: 'Adobe AfterEffects', type: 'file'}, + {id: 'g', name: 'Adobe Illustrator', type: 'file'}, + {id: 'h', name: 'Adobe Lightroom', type: 'file'}, + {id: 'i', name: 'Adobe Premiere Pro', type: 'file'}, + {id: 'j', name: 'Adobe Fresco', type: 'file'}, + {id: 'k', name: 'Adobe Dreamweaver', type: 'file'}, + {id: 'l', name: 'Adobe Connect', type: 'file'}, + {id: 'm', name: 'Pictures', type: 'folder', children: [ + {id: 1, name: 'Yosemite'}, + {id: 2, name: 'Jackson Hole'}, + {id: 3, name: 'Crater Lake'} + ]}, + {id: 'n', name: 'Adobe Acrobat', type: 'file'} +]; + +export const Dynamic: Story = { + render: (args) => ( + + {item => ( + {item.name} + )} + + ), + args: { + 'aria-label': 'Birthday' + } +}; + + +// taken from https://random.dog/ +const itemsWithThumbs: Array<{id: string, title: string, url: string}> = [ + {id: '1', title: 'swimmer', url: 'https://random.dog/b2fe2172-cf11-43f4-9c7f-29bd19601712.jpg'}, + {id: '2', title: 'chocolate', url: 'https://random.dog/2032518a-eec8-4102-9d48-3dca5a26eb23.png'}, + {id: '3', title: 'good boi', url: 'https://random.dog/191091b2-7d69-47af-9f52-6605063f1a47.jpg'}, + {id: '4', title: 'polar bear', url: 'https://random.dog/c22c077e-a009-486f-834c-a19edcc36a17.jpg'}, + {id: '5', title: 'cold boi', url: 'https://random.dog/093a41da-e2c0-4535-a366-9ef3f2013f73.jpg'}, + {id: '6', title: 'pilot', url: 'https://random.dog/09f8ecf4-c22b-49f4-af24-29fb5c8dbb2d.jpg'}, + {id: '7', title: 'nerd', url: 'https://random.dog/1a0535a6-ca89-4059-9b3a-04a554c0587b.jpg'}, + {id: '8', title: 'audiophile', url: 'https://random.dog/32367-2062-4347.jpg'} +]; + +export const DynamicWithThumbs: Story = { + render: (args) => ( + + {item => ( + + {item.title} + {item.url ? {item.title} : null} + + )} + + ), + args: { + 'aria-label': 'Birthday' + } +}; + + +// taken from https://random.dog/ +const itemsWithIcons: Array<{id: string, title: string, icons: ReactNode}> = [ + {id: '0', title: 'folder of good bois', icons: }, + {id: '1', title: 'swimmer', icons: }, + {id: '2', title: 'chocolate', icons: }, + {id: '3', title: 'good boi', icons: }, + {id: '4', title: 'polar bear', icons: }, + {id: '5', title: 'cold boi', icons: }, + {id: '6', title: 'pilot', icons: }, + {id: '8', title: 'audiophile', icons: }, + {id: '9', title: 'file of great boi', icons: }, + {id: '10', title: 'fuzzy boi', icons: }, + {id: '11', title: 'i know what i am doing', icons: }, + {id: '12', title: 'kisses', icons: }, + {id: '13', title: 'belly rubs', icons: }, + {id: '14', title: 'long boi', icons: }, + {id: '15', title: 'floof', icons: }, + {id: '16', title: 'german sheparpadom', icons: }, +]; + +export const DynamicWithIcon: Story = { + render: (args) => ( + + {item => ( + + {item.title} + {item.icons ? item.icons : null} + + + + + + )} + + ), + args: { + 'aria-label': 'Birthday' + } +}; diff --git a/packages/react-aria-components/src/GridList.tsx b/packages/react-aria-components/src/GridList.tsx index ead3a3b1037..e7421e5fded 100644 --- a/packages/react-aria-components/src/GridList.tsx +++ b/packages/react-aria-components/src/GridList.tsx @@ -335,6 +335,8 @@ export const GridListItem = /*#__PURE__*/ createLeafComponent('item', function G defaultClassName: 'react-aria-GridListItem', values: { ...states, + isFirstItem: item.key === state.collection.getFirstKey(), + isLastItem: item.key === state.collection.getLastKey(), isHovered, isFocusVisible, selectionMode: state.selectionManager.selectionMode, From 379dac396cb8411e01274f18ff353b26a1591af9 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Wed, 17 Sep 2025 15:45:21 +1000 Subject: [PATCH 02/60] Explore highlight selection --- packages/@react-spectrum/s2/src/ListView.tsx | 43 ++- packages/@react-spectrum/s2/src/Picker.tsx | 4 +- packages/@react-spectrum/s2/src/TreeView.tsx | 193 ++++++++++--- .../s2/stories/HighlightSelection.stories.tsx | 269 ++++++++++++++++++ .../s2/stories/ListView.stories.tsx | 18 +- .../s2/stories/assets/check.tsx | 2 + .../s2/style/spectrum-theme.ts | 3 +- .../__snapshots__/imports.test.ts.snap | 4 +- .../multi-collection.test.ts.snap | 4 +- 9 files changed, 467 insertions(+), 73 deletions(-) create mode 100644 packages/@react-spectrum/s2/stories/HighlightSelection.stories.tsx create mode 100644 packages/@react-spectrum/s2/stories/assets/check.tsx diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index 443ac565a89..a086359102e 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -10,7 +10,11 @@ * governing permissions and limitations under the License. */ +import {ActionButtonGroupContext} from './ActionButtonGroup'; +import {baseColor, edgeToText, focusRing, fontRelative, space, style} from '../style' with {type: 'macro'}; +import {centerBaseline} from './CenterBaseline'; import { + ContextValue, DEFAULT_SLOT, GridList, GridListItem, @@ -19,32 +23,27 @@ import { GridListProps, GridListRenderProps, ListLayout, - ListStateContext, Provider, - useContextProps, - useLocale, + SlotProps, Virtualizer } from 'react-aria-components'; -import {JSXElementConstructor, ReactElement, ReactNode, createContext, forwardRef, useContext, useRef} from 'react'; -import {ContextValue, SlotProps} from 'react-aria-components'; -import {DOMProps, DOMRef, DOMRefValue} from '@react-types/shared'; -import {control, controlFont, controlSize, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; +import {controlFont, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; +import {createContext, forwardRef, JSXElementConstructor, ReactElement, ReactNode, useContext, useRef} from 'react'; +import {DOMProps, DOMRef, DOMRefValue, forwardRefType, GlobalDOMAttributes} from '@react-types/shared'; +import {IconContext} from './Icon'; +import {ImageContext} from './Image'; +import {pressScale} from './pressScale'; +import {Text, TextContext} from './Content'; +import {useDOMRef} from '@react-spectrum/utils'; +import {useScale} from './utils'; import {useSpectrumContextProps} from './useSpectrumContextProps'; -import {baseColor, edgeToText, focusRing, fontRelative, space, style} from '../style' with {type: 'macro'}; -import { useScale } from './utils'; -import { useDOMRef } from '@react-spectrum/utils'; -import { pressScale } from './pressScale'; -import { IconContext } from './Icon'; -import { centerBaseline } from './CenterBaseline'; -import { Text, TextContext } from './Content'; -import { ImageContext } from './Image'; -import { ActionButtonGroupContext } from './ActionButtonGroup'; -export interface ListViewProps extends GridListProps, DOMProps, StyleProps, ListViewStylesProps, SlotProps { +export interface ListViewProps extends Omit, 'className' | 'style' | 'children' | keyof GlobalDOMAttributes>, DOMProps, StyleProps, ListViewStylesProps, SlotProps { /** * Whether to automatically focus the Inline Alert when it first renders. */ - autoFocus?: boolean + autoFocus?: boolean, + children: ReactNode | ((item: T) => ReactNode) } interface ListViewStylesProps { @@ -84,10 +83,10 @@ const listView = style({ borderStyle: 'solid' }, getAllowedOverrides()); -export const ListView = /*#__PURE__*/ forwardRef(function ListView( +export const ListView = /*#__PURE__*/ (forwardRef as forwardRefType)(function ListView( props: ListViewProps, ref: DOMRef -): ReactNode { +) { [props, ref] = useSpectrumContextProps(props, ref, ListViewContext); let {children, isQuiet} = props; let scale = useScale(); @@ -242,10 +241,10 @@ let actionButtonGroup = style({ export function ListViewItem(props: ListViewItemProps): ReactNode { let ref = useRef(null); let isLink = props.href != null; - let isLinkOut = isLink && props.target === '_blank'; + // let isLinkOut = isLink && props.target === '_blank'; let {isQuiet} = useContext(InternalListViewContext); let textValue = props.textValue || (typeof props.children === 'string' ? props.children : undefined); - let {direction} = useLocale(); + // let {direction} = useLocale(); return ( void, /** Whether the tree should be displayed with a [emphasized style](https://spectrum.adobe.com/page/tree-view/#Emphasis). */ - isEmphasized?: boolean + isEmphasized?: boolean, + selectionStyle?: 'highlight' | 'checkbox', + selectionCornerStyle?: 'square' | 'round' } export interface TreeViewProps extends Omit, 'style' | 'className' | 'onRowAction' | 'selectionBehavior' | 'onScroll' | 'onCellAction' | 'dragAndDropHooks' | keyof GlobalDOMAttributes>, UnsafeStyles, S2TreeProps { @@ -78,10 +81,10 @@ interface TreeRendererContextValue { } const TreeRendererContext = createContext({}); -export const TreeViewContext = createContext, DOMRefValue>>(null); +export const TreeViewContext = createContext>, DOMRefValue>>(null); -let InternalTreeContext = createContext<{isDetached?: boolean, isEmphasized?: boolean}>({}); +let InternalTreeContext = createContext<{isDetached?: boolean, isEmphasized?: boolean, selectionStyle: 'highlight' | 'checkbox', selectionCornerStyle: 'square' | 'round'}>({selectionStyle: 'checkbox', selectionCornerStyle: 'round'}); // TODO: the below is needed so the borders of the top and bottom row isn't cut off if the TreeView is wrapped within a container by always reserving the 2px needed for the // keyboard focus ring. Perhaps find a different way of rendering the outlines since the top of the item doesn't @@ -113,7 +116,7 @@ const tree = style({ */ export const TreeView = /*#__PURE__*/ (forwardRef as forwardRefType)(function TreeView(props: TreeViewProps, ref: DOMRef) { [props, ref] = useSpectrumContextProps(props, ref, TreeViewContext); - let {children, isDetached, isEmphasized, UNSAFE_className, UNSAFE_style} = props; + let {children, isDetached, isEmphasized, selectionStyle = 'checkbox', selectionCornerStyle = 'round', UNSAFE_className, UNSAFE_style} = props; let scale = useScale(); let renderer; @@ -131,12 +134,12 @@ export const TreeView = /*#__PURE__*/ (forwardRef as forwardRefType)(function Tr gap: isDetached ? 2 : 0 }}> - + (UNSAFE_className ?? '') + tree({isDetached, ...renderProps}, props.styles)} - selectionBehavior="toggle" + selectionBehavior={selectionStyle === 'highlight' ? 'replace' : 'toggle'} ref={domRef}> {props.children} @@ -150,28 +153,45 @@ const selectedBackground = lightDark(colorMix('gray-25', 'informative-900', 10), const selectedActiveBackground = lightDark(colorMix('gray-25', 'informative-900', 15), colorMix('gray-25', 'informative-700', 15)); const rowBackgroundColor = { - default: '--s2-container-bg', - isFocusVisibleWithin: colorMix('gray-25', 'gray-900', 7), - isHovered: colorMix('gray-25', 'gray-900', 7), - isPressed: colorMix('gray-25', 'gray-900', 10), - isSelected: { - default: colorMix('gray-25', 'gray-900', 7), - isEmphasized: selectedBackground, - isFocusVisibleWithin: { - default: colorMix('gray-25', 'gray-900', 10), - isEmphasized: selectedActiveBackground - }, - isHovered: { - default: colorMix('gray-25', 'gray-900', 10), - isEmphasized: selectedActiveBackground + selectionStyle: { + checkbox: { + default: '--s2-container-bg', + isFocusVisibleWithin: colorMix('gray-25', 'gray-900', 7), + isHovered: colorMix('gray-25', 'gray-900', 7), + isPressed: colorMix('gray-25', 'gray-900', 10), + isSelected: { + default: colorMix('gray-25', 'gray-900', 7), + isEmphasized: selectedBackground, + isFocusVisibleWithin: { + default: colorMix('gray-25', 'gray-900', 10), + isEmphasized: selectedActiveBackground + }, + isHovered: { + default: colorMix('gray-25', 'gray-900', 10), + isEmphasized: selectedActiveBackground + }, + isPressed: { + default: colorMix('gray-25', 'gray-900', 10), + isEmphasized: selectedActiveBackground + } + }, + forcedColors: { + default: 'Background' + } }, - isPressed: { - default: colorMix('gray-25', 'gray-900', 10), - isEmphasized: selectedActiveBackground + highlight: { + default: '--s2-container-bg', + isFocusVisibleWithin: 'gray-100', + isHovered: 'gray-100', + isPressed: 'gray-100', + isSelected: { + default: 'gray-100', + isEmphasized: 'blue-200' + }, + forcedColors: { + default: 'Background' + } } - }, - forcedColors: { - default: 'Background' } } as const; @@ -213,14 +233,35 @@ const treeCellGrid = style({ gridTemplateAreas: [ 'drag-handle checkbox level-padding expand-button icon content actions actionmenu' ], - backgroundColor: '--rowBackgroundColor', paddingEnd: 4, // account for any focus rings on the last item in the cell color: { + default: 'gray-700', + isHovered: 'gray-800', + isSelected: 'gray-900', isDisabled: { default: 'gray-400', forcedColors: 'GrayText' } }, + '--thumbnailBorderColor': { + type: 'color', + value: { + default: 'gray-500', + isHovered: 'gray-800', + isSelected: 'gray-900', + isEmphasized: { + isSelected: 'blue-900' + }, + isDisabled: { + default: 'gray-400', + forcedColors: 'GrayText' + } + } + }, + fontWeight: { + default: 'normal', + isSelected: 'medium' + }, '--rowSelectedBorderColor': { type: 'outlineColor', value: { @@ -253,6 +294,63 @@ const treeCellGrid = style({ } }); +const treeRowBackground = style({ + position: 'absolute', + zIndex: -1, + inset: 0, + backgroundColor: '--rowBackgroundColor', + borderTopStartRadius: { + selectionStyle: { + default: 'default', + highlight: { + default: 'default', + isPreviousSelected: 'none' + } + }, + selectionCornerStyle: { + square: 'none' + } + }, + borderTopEndRadius: { + selectionStyle: { + default: 'default', + highlight: { + default: 'default', + isPreviousSelected: 'none' + } + }, + selectionCornerStyle: { + square: 'none' + } + }, + borderBottomStartRadius: { + selectionStyle: { + default: 'default', + highlight: { + default: 'default', + isNextSelected: 'none' + } + }, + selectionCornerStyle: { + square: 'none' + } + }, + borderBottomEndRadius: { + selectionStyle: { + default: 'default', + highlight: { + default: 'default', + isNextSelected: 'none' + } + }, + selectionCornerStyle: { + square: 'none' + } + }, + borderWidth: 0, + borderStyle: 'solid' +}); + const treeCheckbox = style({ gridArea: 'checkbox', marginStart: 12, @@ -269,6 +367,21 @@ const treeIcon = style({ } }); +const treeThumbnail = style({ + gridArea: 'icon', + marginEnd: 'text-to-visual', + width: 32, + aspectRatio: 'square', + objectFit: 'contain', + borderRadius: 'sm', + borderWidth: 1, + borderColor: '--thumbnailBorderColor', + borderStyle: 'solid', + padding: 2, + backgroundColor: 'white', + boxSizing: 'border-box' +}); + const treeContent = style({ gridArea: 'content', textOverflow: 'ellipsis', @@ -317,15 +430,16 @@ export const TreeViewItem = (props: TreeViewItemProps): ReactNode => { let { href } = props; - let {isDetached, isEmphasized} = useContext(InternalTreeContext); + let {isDetached, isEmphasized, selectionStyle} = useContext(InternalTreeContext); return ( treeRow({ ...renderProps, - isLink: !!href, isEmphasized - }) + (renderProps.isFocusVisible && !isDetached ? ' ' + treeRowFocusIndicator : '')} /> + isLink: !!href, isEmphasized, + selectionStyle + }) + (renderProps.isFocusVisible && !isDetached && selectionStyle !== 'highlight' ? ' ' + treeRowFocusIndicator : '')} /> ); }; @@ -338,21 +452,27 @@ export const TreeViewItemContent = (props: TreeViewItemContentProps): ReactNode let { children } = props; - let {isDetached, isEmphasized} = useContext(InternalTreeContext); + let {isDetached, isEmphasized, selectionStyle, selectionCornerStyle} = useContext(InternalTreeContext); let scale = useScale(); return ( - {({isExpanded, hasChildItems, selectionMode, selectionBehavior, isDisabled, isFocusVisible, isSelected, id, state}) => { + {({isExpanded, hasChildItems, selectionMode, selectionBehavior, isDisabled, isFocusVisible, isSelected, id, state, isHovered}) => { let isNextSelected = false; let isNextFocused = false; + let isPreviousSelected = false; + let keyBefore = state.collection.getKeyBefore(id); let keyAfter = state.collection.getKeyAfter(id); + if (keyBefore != null) { + isPreviousSelected = state.selectionManager.isSelected(keyBefore); + } if (keyAfter != null) { isNextSelected = state.selectionManager.isSelected(keyAfter); } let isFirst = state.collection.getFirstKey() === id; return ( -
+
+
{selectionMode !== 'none' && selectionBehavior === 'toggle' && ( // TODO: add transition?
@@ -370,12 +490,13 @@ export const TreeViewItemContent = (props: TreeViewItemContentProps): ReactNode {typeof children === 'string' ? {children} : children} diff --git a/packages/@react-spectrum/s2/stories/HighlightSelection.stories.tsx b/packages/@react-spectrum/s2/stories/HighlightSelection.stories.tsx new file mode 100644 index 00000000000..2bf94dda138 --- /dev/null +++ b/packages/@react-spectrum/s2/stories/HighlightSelection.stories.tsx @@ -0,0 +1,269 @@ +/** + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import { + ActionButton, + ActionButtonGroup, + Collection, + Image, + Key, + Text, + TreeView, + TreeViewItem, + TreeViewItemContent, + TreeViewItemProps, + TreeViewLoadMoreItem, + TreeViewLoadMoreItemProps, + TreeViewProps +} from '../src'; +import {categorizeArgTypes, getActionArgs} from './utils'; +import {checkers} from './assets/check'; +import FileTxt from '../s2wf-icons/S2_Icon_FileText_20_N.svg'; +import Folder from '../s2wf-icons/S2_Icon_Folder_20_N.svg'; +import FolderOpen from '../s2wf-icons/S2_Icon_FolderOpen_20_N.svg'; +import Lock from '../s2wf-icons/S2_Icon_Lock_20_N.svg'; +import LockOpen from '../s2wf-icons/S2_Icon_LockOpen_20_N.svg'; +import type {Meta, StoryObj} from '@storybook/react'; +import React, {ReactElement, useState} from 'react'; +import Visibility from '../s2wf-icons/S2_Icon_Visibility_20_N.svg'; + +import VisibilityOff from '../s2wf-icons/S2_Icon_VisibilityOff_20_N.svg'; + +const events = ['onSelectionChange']; + +const meta: Meta = { + component: TreeView, + parameters: { + layout: 'centered' + }, + tags: ['autodocs'], + args: {...getActionArgs(events)}, + argTypes: { + ...categorizeArgTypes('Events', events), + children: {table: {disable: true}} + } +}; + +export default meta; + + +interface TreeViewLayersItemType { + id?: string, + name: string, + icon?: ReactElement, + childItems?: TreeViewLayersItemType[], + isLocked?: boolean, + isVisible?: boolean +} + +let layersRows: TreeViewLayersItemType[] = [ + {id: 'layer-1', name: 'Layer', icon: }, + {id: 'layer-2', name: 'Layer', icon: , isVisible: false}, + {id: 'layer-group-1', name: 'Layer group', icon: , isVisible: false, childItems: [ + {id: 'layer-group-1-1', name: 'Layer', icon: }, + {id: 'layer-group-1-2', name: 'Layer', icon: }, + {id: 'layer-group-1-3', name: 'Layer', icon: }, + {id: 'layer-group-1-4', name: 'Layer', icon: }, + {id: 'layer-group-1-group-1', name: 'Layer Group', icon: , childItems: [ + {id: 'layer-group-1-group-1-1', name: 'Layer', icon: }, + {id: 'layer-group-1-group-1-2', name: 'Layer', icon: }, + {id: 'layer-group-1-group-1-3', name: 'Layer', icon: } + ]} + ]}, + {id: 'layer-group-2', name: 'Layer group', icon: , isLocked: true, childItems: [ + {id: 'layer-group-2-1', name: 'Layer', icon: }, + {id: 'layer-group-2-2', name: 'Layer', icon: , isVisible: false}, + {id: 'layer-group-2-3', name: 'Layer', icon: , isLocked: true}, + {id: 'layer-group-2-4', name: 'Layer', icon: }, + {id: 'layer-group-2-group-1', name: 'Layer Group', icon: } + ]}, + {id: 'layer-group-3', name: 'Layer group', icon: , childItems: [ + {id: 'reports-1', name: 'Reports 1', icon: , childItems: [ + {id: 'layer-group-3-1', name: 'Layer', icon: }, + {id: 'layer-group-3-2', name: 'Layer', icon: }, + {id: 'layer-group-3-3', name: 'Layer', icon: }, + {id: 'layer-group-3-4', name: 'Layer', icon: }, + {id: 'layer-group-3-group-1', name: 'Layer Group', icon: , childItems: [ + {id: 'layer-group-3-group-1-1', name: 'Layer', icon: }, + {id: 'layer-group-3-group-1-2', name: 'Layer', icon: }, + {id: 'layer-group-3-group-1-3', name: 'Layer', icon: } + ]} + ]}, + {id: 'layer-group-3-2', name: 'Layer', icon: }, + {id: 'layer-group-3-3', name: 'Layer', icon: }, + {id: 'layer-group-3-4', name: 'Layer', icon: }, + ...Array.from({length: 100}, (_, i) => ({id: `layer-group-3-repeat-${i}`, name: 'Layer', icon: })) + ]}, + {id: 'layer-4', name: 'Layer', icon: , isLocked: true, isVisible: false} +]; + +const TreeExampleLayersItem = (props: Omit & TreeViewLayersItemType & TreeViewLoadMoreItemProps): ReactElement => { + let {childItems, name, icon = , loadingState, onLoadMore, isLocked = false, isVisible = true} = props; + return ( + <> + + + {name} + {icon} + + {isLocked ? : } + {isVisible ? : } + + + + {(item) => ( + + )} + + {onLoadMore && loadingState && } + + + ); +}; + +const TreeExampleLayers = (args: TreeViewProps): ReactElement => ( +
+ + {(item) => ( + + )} + +
+); + +export const LayersTree: StoryObj = { + render: TreeExampleLayers, + args: { + defaultExpandedKeys: ['layer-group-2'], + selectionMode: 'multiple', + selectionStyle: 'highlight' + } +}; + +interface TreeViewFileItemType { + id?: string, + name: string, + icon?: ReactElement, + childItems?: TreeViewFileItemType[], + isExpanded?: boolean +} + +let rows: TreeViewFileItemType[] = [ + {id: 'documentation', name: 'Documentation', icon: , childItems: [ + {id: 'project-1', name: 'Project 1 Level 1', icon: }, + {id: 'project-2', name: 'Project 2 Level 1', icon: , childItems: [ + {id: 'project-2A', name: 'Project 2A Level 2', icon: }, + {id: 'project-2B', name: 'Project 2B Level 2', icon: }, + {id: 'project-2C', name: 'Project 2C Level 3', icon: } + ]}, + {id: 'project-3', name: 'Project 3', icon: }, + {id: 'project-4', name: 'Project 4', icon: }, + {id: 'project-5', name: 'Project 5', icon: , childItems: [ + {id: 'project-5A', name: 'Project 5A', icon: }, + {id: 'project-5B', name: 'Project 5B', icon: }, + {id: 'project-5C', name: 'Project 5C', icon: } + ]}, + ...Array.from({length: 100}, (_, i) => ({id: `projects-repeat-${i}`, name: `Reports ${i}`, icon: })) + ]}, + {id: 'branding', name: 'Branding', icon: , childItems: [ + {id: 'proposals', name: 'Proposals', icon: }, + {id: 'explorations', name: 'Explorations', icon: }, + {id: 'assets', name: 'Assets', icon: } + ]}, + {id: 'file01', name: 'File 01', icon: }, + {id: 'file02', name: 'File 02', icon: }, + {id: 'file03', name: 'File 03', icon: } +]; + +const TreeExampleFileItem = (props: Omit & TreeViewFileItemType & TreeViewLoadMoreItemProps & {expandedKeys: Set}): ReactElement => { + let {childItems, name, icon = , loadingState, onLoadMore, expandedKeys} = props; + let isExpanded = expandedKeys.has(props.id as Key); + return ( + <> + + + {name} + {isExpanded ? : icon} + + + {(item) => ( + + )} + + {onLoadMore && loadingState && } + + + ); +}; + +const TreeExampleFiles = (args: TreeViewProps): ReactElement => { + let [expandedKeys, setExpandedKeys] = useState>(new Set(['branding'])); + let [items, setItems] = useState(rows); + let onExpandedChange = (keys: Set) => { + setExpandedKeys(keys); + // Iterate over depth first all items in 'rows' that are in the keys set, add a property 'isExpanded' to the item. we must maintain the tree structure. + // This is to work around the fact that we cannot change the icon inside the TreeViewItemContent because it doesn't re-render for the expanded state change. + let newItems = rows.reduce((acc, item) => { + let iterator = (children: TreeViewFileItemType[]) => { + return children.map(child => { + let newChild = {...child}; + if (keys.has(child.id as Key)) { + newChild.isExpanded = true; + } + if (child.childItems) { + newChild.childItems = iterator(child.childItems); + } + return newChild; + }); + }; + let newChildren; + if (item.childItems) { + newChildren = iterator(item.childItems); + } + acc.push({...item, isExpanded: keys.has(item.id as Key), childItems: newChildren}); + return acc; + }, [] as TreeViewFileItemType[]); + setItems(newItems); + }; + return ( +
+ + {(item) => ( + + )} + +
+ ); +}; + +export const FileTree: StoryObj = { + render: TreeExampleFiles, + args: { + selectionMode: 'multiple', + selectionStyle: 'highlight' + } +}; diff --git a/packages/@react-spectrum/s2/stories/ListView.stories.tsx b/packages/@react-spectrum/s2/stories/ListView.stories.tsx index 744a5e1e139..e30795267ba 100644 --- a/packages/@react-spectrum/s2/stories/ListView.stories.tsx +++ b/packages/@react-spectrum/s2/stories/ListView.stories.tsx @@ -10,15 +10,15 @@ * governing permissions and limitations under the License. */ +import {ActionButton, ActionButtonGroup, Image, ListView, ListViewItem, Text} from '../'; import {categorizeArgTypes} from './utils'; -import {ListView, ListViewItem, Text, Image, ActionButton, ActionButtonGroup} from '../'; -import type {Meta, StoryObj} from '@storybook/react'; -import { Key } from 'react-aria'; -import Folder from '../s2wf-icons/S2_Icon_Folder_20_N.svg'; -import File from '../s2wf-icons/S2_Icon_File_20_N.svg'; -import Edit from '../s2wf-icons/S2_Icon_Edit_20_N.svg'; import Delete from '../s2wf-icons/S2_Icon_Delete_20_N.svg'; -import { ReactNode } from 'react'; +import Edit from '../s2wf-icons/S2_Icon_Edit_20_N.svg'; +import File from '../s2wf-icons/S2_Icon_File_20_N.svg'; +import Folder from '../s2wf-icons/S2_Icon_Folder_20_N.svg'; +import {Key} from 'react-aria'; +import type {Meta, StoryObj} from '@storybook/react'; +import {ReactNode} from 'react'; const meta: Meta = { component: ListView, @@ -98,7 +98,7 @@ const items: Item[] = [ export const Dynamic: Story = { render: (args) => ( - {item => ( + {(item) => ( {item.name} )} @@ -155,7 +155,7 @@ const itemsWithIcons: Array<{id: string, title: string, icons: ReactNode}> = [ {id: '13', title: 'belly rubs', icons: }, {id: '14', title: 'long boi', icons: }, {id: '15', title: 'floof', icons: }, - {id: '16', title: 'german sheparpadom', icons: }, + {id: '16', title: 'german sheparpadom', icons: } ]; export const DynamicWithIcon: Story = { diff --git a/packages/@react-spectrum/s2/stories/assets/check.tsx b/packages/@react-spectrum/s2/stories/assets/check.tsx new file mode 100644 index 00000000000..c461a431e3d --- /dev/null +++ b/packages/@react-spectrum/s2/stories/assets/check.tsx @@ -0,0 +1,2 @@ + +export let checkers = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTAuMTAwMDk4IDBIMy4xMDAxVjNIMC4xMDAwOThWMFoiIGZpbGw9IiNFMUUxRTEiLz4KPHBhdGggZD0iTTAuMTAwMDk4IDE4SDMuMTAwMVYyMUgwLjEwMDA5OFYxOFoiIGZpbGw9IiNFMUUxRTEiLz4KPHBhdGggZD0iTTYuMTAwMSAwSDkuMTAwMVYzSDYuMTAwMVYwWiIgZmlsbD0iI0UxRTFFMSIvPgo8cGF0aCBkPSJNNi4xMDAxIDE4SDkuMTAwMVYyMUg2LjEwMDFWMThaIiBmaWxsPSIjRTFFMUUxIi8+CjxwYXRoIGQ9Ik0xMi4xMDAxIDE4SDkuMTAwMVYxNUgxMi4xMDAxVjE4WiIgZmlsbD0iI0UxRTFFMSIvPgo8cGF0aCBkPSJNMTIuMTAwMSAyNEg5LjEwMDFWMjFIMTIuMTAwMVYyNFoiIGZpbGw9IiNFMUUxRTEiLz4KPHBhdGggZD0iTTE1LjEwMDEgMEgxMi4xMDAxVjNIMTUuMTAwMVYwWiIgZmlsbD0iI0UxRTFFMSIvPgo8cGF0aCBkPSJNMTUuMTAwMSAxOEgxMi4xMDAxVjIxSDE1LjEwMDFWMThaIiBmaWxsPSIjRTFFMUUxIi8+CjxwYXRoIGQ9Ik0xOC4xMDAxIDE4SDE1LjEwMDFWMTVIMTguMTAwMVYxOFoiIGZpbGw9IiNFMUUxRTEiLz4KPHBhdGggZD0iTTE4LjEwMDEgMjRIMTUuMTAwMVYyMUgxOC4xMDAxVjI0WiIgZmlsbD0iI0UxRTFFMSIvPgo8cGF0aCBkPSJNMjEuMTAwMSAwSDE4LjEwMDFWM0gyMS4xMDAxVjBaIiBmaWxsPSIjRTFFMUUxIi8+CjxwYXRoIGQ9Ik0yMS4xMDAxIDE4SDE4LjEwMDFWMjFIMjEuMTAwMVYxOFoiIGZpbGw9IiNFMUUxRTEiLz4KPHBhdGggZD0iTTI0LjEwMDEgMThIMjEuMTAwMVYxNUgyNC4xMDAxVjE4WiIgZmlsbD0iI0UxRTFFMSIvPgo8cGF0aCBkPSJNMjEuMTAwMSAyMUgyNC4xMDAxVjI0SDIxLjEwMDFWMjFaIiBmaWxsPSIjRTFFMUUxIi8+CjxwYXRoIGQ9Ik02LjEwMDEgMThIMy4xMDAxVjE1SDYuMTAwMVYxOFoiIGZpbGw9IiNFMUUxRTEiLz4KPHBhdGggZD0iTTYuMTAwMSAyNEgzLjEwMDFWMjFINi4xMDAxVjI0WiIgZmlsbD0iI0UxRTFFMSIvPgo8cGF0aCBkPSJNMC4xMDAwOTggNkgzLjEwMDFWM0gwLjEwMDA5OFY2WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTYuMTAwMSA2SDkuMTAwMVYzSDYuMTAwMVY2WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTEyLjEwMDEgMTJIOS4xMDAxVjE1SDEyLjEwMDFWMTJaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMTUuMTAwMSA2SDEyLjEwMDFWM0gxNS4xMDAxVjZaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMTguMTAwMSAxMkgxNS4xMDAxVjE1SDE4LjEwMDFWMTJaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMjEuMTAwMSA2SDE4LjEwMDFWM0gyMS4xMDAxVjZaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMjQuMTAwMSAxMkgyMS4xMDAxVjE1SDI0LjEwMDFWMTJaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNNi4xMDAxIDEySDMuMTAwMVYxNUg2LjEwMDFWMTJaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMC4xMDAwOTggOUgzLjEwMDFWNkgwLjEwMDA5OFY5WiIgZmlsbD0iI0UxRTFFMSIvPgo8cGF0aCBkPSJNNi4xMDAxIDlIOS4xMDAxVjZINi4xMDAxVjlaIiBmaWxsPSIjRTFFMUUxIi8+CjxwYXRoIGQ9Ik0xMi4xMDAxIDlIOS4xMDAxVjEySDEyLjEwMDFWOVoiIGZpbGw9IiNFMUUxRTEiLz4KPHBhdGggZD0iTTE1LjEwMDEgOUgxMi4xMDAxVjZIMTUuMTAwMVY5WiIgZmlsbD0iI0UxRTFFMSIvPgo8cGF0aCBkPSJNMTguMTAwMSA5SDE1LjEwMDFWMTJIMTguMTAwMVY5WiIgZmlsbD0iI0UxRTFFMSIvPgo8cGF0aCBkPSJNMjEuMTAwMSA5SDE4LjEwMDFWNkgyMS4xMDAxVjlaIiBmaWxsPSIjRTFFMUUxIi8+CjxwYXRoIGQ9Ik0yNC4xMDAxIDlIMjEuMTAwMVYxMkgyNC4xMDAxVjlaIiBmaWxsPSIjRTFFMUUxIi8+CjxwYXRoIGQ9Ik02LjEwMDEgOUgzLjEwMDFWMTJINi4xMDAxVjlaIiBmaWxsPSIjRTFFMUUxIi8+CjxwYXRoIGQ9Ik0wLjEwMDA5OCAxMkgzLjEwMDFWOUgwLjEwMDA5OFYxMloiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik02LjEwMDEgMTJIOS4xMDAxVjlINi4xMDAxVjEyWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTEyLjEwMDEgNkg5LjEwMDFWOUgxMi4xMDAxVjZaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMTUuMTAwMSAxMkgxMi4xMDAxVjlIMTUuMTAwMVYxMloiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0xOC4xMDAxIDZIMTUuMTAwMVY5SDE4LjEwMDFWNloiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0yMS4xMDAxIDEySDE4LjEwMDFWOUgyMS4xMDAxVjEyWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTI0LjEwMDEgNkgyMS4xMDAxVjlIMjQuMTAwMVY2WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTYuMTAwMSA2SDMuMTAwMVY5SDYuMTAwMVY2WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTAuMTAwMDk4IDE1SDMuMTAwMVYxMkgwLjEwMDA5OFYxNVoiIGZpbGw9IiNFMUUxRTEiLz4KPHBhdGggZD0iTTYuMTAwMSAxNUg5LjEwMDFWMTJINi4xMDAxVjE1WiIgZmlsbD0iI0UxRTFFMSIvPgo8cGF0aCBkPSJNMTIuMTAwMSAzSDkuMTAwMVY2SDEyLjEwMDFWM1oiIGZpbGw9IiNFMUUxRTEiLz4KPHBhdGggZD0iTTE1LjEwMDEgMTVIMTIuMTAwMVYxMkgxNS4xMDAxVjE1WiIgZmlsbD0iI0UxRTFFMSIvPgo8cGF0aCBkPSJNMTguMTAwMSAzSDE1LjEwMDFWNkgxOC4xMDAxVjNaIiBmaWxsPSIjRTFFMUUxIi8+CjxwYXRoIGQ9Ik0yMS4xMDAxIDE1SDE4LjEwMDFWMTJIMjEuMTAwMVYxNVoiIGZpbGw9IiNFMUUxRTEiLz4KPHBhdGggZD0iTTI0LjEwMDEgM0gyMS4xMDAxVjZIMjQuMTAwMVYzWiIgZmlsbD0iI0UxRTFFMSIvPgo8cGF0aCBkPSJNNi4xMDAxIDNIMy4xMDAxVjZINi4xMDAxVjNaIiBmaWxsPSIjRTFFMUUxIi8+CjxwYXRoIGQ9Ik0wLjEwMDA5OCAxOEgzLjEwMDFWMTVIMC4xMDAwOThWMThaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMC4xMDAwOTggMjRIMy4xMDAxVjIxSDAuMTAwMDk4VjI0WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTYuMTAwMSAxOEg5LjEwMDFWMTVINi4xMDAxVjE4WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTYuMTAwMSAyNEg5LjEwMDFWMjFINi4xMDAxVjI0WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTEyLjEwMDEgMEg5LjEwMDFWM0gxMi4xMDAxVjBaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMTIuMTAwMSAxOEg5LjEwMDFWMjFIMTIuMTAwMVYxOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0xNS4xMDAxIDE4SDEyLjEwMDFWMTVIMTUuMTAwMVYxOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0xNS4xMDAxIDI0SDEyLjEwMDFWMjFIMTUuMTAwMVYyNFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0xOC4xMDAxIDBIMTUuMTAwMVYzSDE4LjEwMDFWMFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0xOC4xMDAxIDE4SDE1LjEwMDFWMjFIMTguMTAwMVYxOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0yMS4xMDAxIDE4SDE4LjEwMDFWMTVIMjEuMTAwMVYxOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0yMS4xMDAxIDI0SDE4LjEwMDFWMjFIMjEuMTAwMVYyNFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0yMS4xMDAxIDNIMjQuMTAwMVYwSDIxLjEwMDFWM1oiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0yNC4xMDAxIDE4SDIxLjEwMDFWMjFIMjQuMTAwMVYxOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik02LjEwMDEgMEgzLjEwMDFWM0g2LjEwMDFWMFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik02LjEwMDEgMThIMy4xMDAxVjIxSDYuMTAwMVYxOFoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo='; diff --git a/packages/@react-spectrum/s2/style/spectrum-theme.ts b/packages/@react-spectrum/s2/style/spectrum-theme.ts index 0e8b732c35a..422d37bd360 100644 --- a/packages/@react-spectrum/s2/style/spectrum-theme.ts +++ b/packages/@react-spectrum/s2/style/spectrum-theme.ts @@ -615,7 +615,8 @@ export const style = createTheme({ borderColor: new SpectrumColorProperty('borderColor', { ...baseColors, negative: colorToken('negative-border-color-default'), - disabled: colorToken('disabled-border-color') + disabled: colorToken('disabled-border-color'), + 'neutral-subdued': colorToken('neutral-subdued-content-color-default') }), outlineColor: new SpectrumColorProperty('outlineColor', { ...baseColors, diff --git a/packages/dev/codemods/src/s1-to-s2/__tests__/__snapshots__/imports.test.ts.snap b/packages/dev/codemods/src/s1-to-s2/__tests__/__snapshots__/imports.test.ts.snap index 4b07144fd42..44706dcc54b 100644 --- a/packages/dev/codemods/src/s1-to-s2/__tests__/__snapshots__/imports.test.ts.snap +++ b/packages/dev/codemods/src/s1-to-s2/__tests__/__snapshots__/imports.test.ts.snap @@ -40,8 +40,8 @@ import * as RSP1 from "@react-spectrum/s2"; `; exports[`should not import Item from S2 1`] = ` -"import { MenuItem, Menu } from "@react-spectrum/s2"; -import { ListView, Item } from '@adobe/react-spectrum'; +"import { MenuItem, Menu, ListView } from "@react-spectrum/s2"; +import { Item } from '@adobe/react-spectrum';
diff --git a/packages/dev/codemods/src/s1-to-s2/__tests__/__snapshots__/multi-collection.test.ts.snap b/packages/dev/codemods/src/s1-to-s2/__tests__/__snapshots__/multi-collection.test.ts.snap index 1e78a4964fa..8efe5edb7e1 100644 --- a/packages/dev/codemods/src/s1-to-s2/__tests__/__snapshots__/multi-collection.test.ts.snap +++ b/packages/dev/codemods/src/s1-to-s2/__tests__/__snapshots__/multi-collection.test.ts.snap @@ -1,10 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Does not affect unimplemented collections 1`] = ` -"import {Item, ActionBarContainer, ActionBar, ListView, ListBox} from '@adobe/react-spectrum'; +"import { Item, ActionBarContainer, ActionBar, ListBox } from '@adobe/react-spectrum'; import {SearchAutocomplete} from '@react-spectrum/autocomplete'; import {StepList} from '@react-spectrum/steplist'; +import { ListView } from "@react-spectrum/s2"; +
One From 5ee48f93afac64ab8a9f771fe87b0f1fe19071c4 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Wed, 17 Sep 2025 15:56:21 +1000 Subject: [PATCH 03/60] fix docs type check --- packages/react-aria-components/src/GridList.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/react-aria-components/src/GridList.tsx b/packages/react-aria-components/src/GridList.tsx index 6787e6309af..e010ca598c9 100644 --- a/packages/react-aria-components/src/GridList.tsx +++ b/packages/react-aria-components/src/GridList.tsx @@ -272,7 +272,10 @@ function GridListInner({props, collection, gridListRef: ref}: ); } -export interface GridListItemRenderProps extends ItemRenderProps {} +export interface GridListItemRenderProps extends ItemRenderProps { + isFirstItem: boolean, + isLastItem: boolean +} export interface GridListItemProps extends RenderProps, LinkDOMProps, HoverEvents, PressEvents, Omit, 'onClick'> { /** The unique id of the item. */ From 1dc83e0dcbe4b57c373340c4c371c6610d95d3cf Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Thu, 18 Sep 2025 16:39:23 +1000 Subject: [PATCH 04/60] Add highlight selection option b to table --- packages/@react-spectrum/s2/src/TableView.tsx | 106 +++++++++++---- packages/@react-spectrum/s2/src/TreeView.tsx | 55 ++------ .../HighlightSelectionTable.stories.tsx | 126 ++++++++++++++++++ ...tsx => HighlightSelectionTree.stories.tsx} | 5 +- 4 files changed, 225 insertions(+), 67 deletions(-) create mode 100644 packages/@react-spectrum/s2/stories/HighlightSelectionTable.stories.tsx rename packages/@react-spectrum/s2/stories/{HighlightSelection.stories.tsx => HighlightSelectionTree.stories.tsx} (99%) diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index 6057c98d6e6..0295a9f95e4 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -14,6 +14,7 @@ import {baseColor, colorMix, focusRing, fontRelative, lightDark, space, style} f import { Button, CellRenderProps, + CheckboxContext, Collection, ColumnRenderProps, ColumnResizer, @@ -104,7 +105,9 @@ interface S2TableProps { /** Handler that is called when more items should be loaded, e.g. while scrolling near the bottom. */ onLoadMore?: () => any, /** Provides the ActionBar to display when rows are selected in the TableView. */ - renderActionBar?: (selectedKeys: 'all' | Set) => ReactElement + renderActionBar?: (selectedKeys: 'all' | Set) => ReactElement, + selectionStyle?: 'highlight' | 'checkbox', + isEmphasized?: boolean } // TODO: Note that loadMore and loadingState are now on the Table instead of on the TableBody @@ -282,6 +285,8 @@ export const TableView = forwardRef(function TableView(props: TableViewProps, re onResizeEnd: propsOnResizeEnd, onAction, onLoadMore, + selectionStyle = 'checkbox', + isEmphasized = false, ...otherProps } = props; @@ -306,11 +311,13 @@ export const TableView = forwardRef(function TableView(props: TableViewProps, re loadingState, onLoadMore, isInResizeMode, - setIsInResizeMode - }), [isQuiet, density, overflowMode, loadingState, onLoadMore, isInResizeMode, setIsInResizeMode]); + setIsInResizeMode, + selectionStyle, + isEmphasized + }), [isQuiet, density, overflowMode, loadingState, onLoadMore, isInResizeMode, setIsInResizeMode, selectionStyle, isEmphasized]); let scrollRef = useRef(null); - let isCheckboxSelection = props.selectionMode === 'multiple' || props.selectionMode === 'single'; + let isCheckboxSelection = (props.selectionMode === 'multiple' || props.selectionMode === 'single') && selectionStyle === 'checkbox'; let {selectedKeys, onSelectionChange, actionBar, actionBarHeight} = useActionBarContainer({...props, scrollRef}); @@ -872,7 +879,7 @@ export interface TableHeaderProps extends Omit, 'style export const TableHeader = /*#__PURE__*/ (forwardRef as forwardRefType)(function TableHeader({columns, dependencies, children}: TableHeaderProps, ref: DOMRef) { let scale = useScale(); let {selectionBehavior, selectionMode} = useTableOptions(); - let {isQuiet} = useContext(InternalTableContext); + let {isQuiet, selectionStyle} = useContext(InternalTableContext); let domRef = useDOMRef(ref); return ( @@ -881,7 +888,7 @@ export const TableHeader = /*#__PURE__*/ (forwardRef as forwardRefType)(function ref={domRef} className={tableHeader}> {/* Add extra columns for selection. */} - {selectionBehavior === 'toggle' && ( + {selectionBehavior === 'toggle' && selectionStyle === 'checkbox' && ( // Also isSticky prop is applied just for the layout, will decide what the RAC api should be later // @ts-ignore @@ -1003,6 +1010,16 @@ const cellContent = style({ backgroundColor: { default: 'transparent', isSticky: '--rowBackgroundColor' + }, + fontWeight: { + selectionStyle: { + highlight: { + default: 'normal', + isRowHeader: { + isSelected: 'medium' + } + } + } } }); @@ -1036,15 +1053,33 @@ export const Cell = forwardRef(function Cell(props: CellProps, ref: DOMRef {({isFocusVisible}) => ( - <> - {children} - {isFocusVisible && } - + // @ts-ignore + )} ); }); +let InnerCell = function InnerCell(props: {isFocusVisible: boolean, children: ReactNode, isSticky?: boolean, align?: 'start' | 'center' | 'end', isRowHeader?: boolean}) { + let {isFocusVisible, children, isSticky, align, isRowHeader} = props; + let tableVisualOptions = useContext(InternalTableContext); + let {isSelected} = useSlottedContext(CheckboxContext, 'selection') ?? {isSelected: false}; + + return ( + <> + {children} + {isFocusVisible && } + + ); +}; + // Use color-mix instead of transparency so sticky cells work correctly. const selectedBackground = lightDark(colorMix('gray-25', 'informative-900', 10), colorMix('gray-25', 'informative-700', 10)); const selectedActiveBackground = lightDark(colorMix('gray-25', 'informative-900', 15), colorMix('gray-25', 'informative-700', 15)); @@ -1053,17 +1088,33 @@ const rowBackgroundColor = { default: 'gray-25', isQuiet: '--s2-container-bg' }, - isFocusVisibleWithin: colorMix('gray-25', 'gray-900', 7), // table-row-hover-color - isHovered: colorMix('gray-25', 'gray-900', 7), // table-row-hover-color - isPressed: colorMix('gray-25', 'gray-900', 10), // table-row-hover-color - isSelected: { - default: selectedBackground, // table-selected-row-background-color, opacity /10 - isFocusVisibleWithin: selectedActiveBackground, // table-selected-row-background-color, opacity /15 - isHovered: selectedActiveBackground, // table-selected-row-background-color, opacity /15 - isPressed: selectedActiveBackground // table-selected-row-background-color, opacity /15 - }, - forcedColors: { - default: 'Background' + selectionStyle: { + checkbox: { + isFocusVisibleWithin: colorMix('gray-25', 'gray-900', 7), // table-row-hover-color + isHovered: colorMix('gray-25', 'gray-900', 7), // table-row-hover-color + isPressed: colorMix('gray-25', 'gray-900', 10), // table-row-hover-color + isSelected: { + default: selectedBackground, // table-selected-row-background-color, opacity /10 + isFocusVisibleWithin: selectedActiveBackground, // table-selected-row-background-color, opacity /15 + isHovered: selectedActiveBackground, // table-selected-row-background-color, opacity /15 + isPressed: selectedActiveBackground // table-selected-row-background-color, opacity /15 + }, + forcedColors: { + default: 'Background' + } + }, + highlight: { + isFocusVisibleWithin: 'gray-100', + isHovered: 'gray-100', + isPressed: 'gray-100', + isSelected: { + default: 'gray-100', + isEmphasized: 'blue-200' + }, + forcedColors: { + default: 'Background' + } + } } } as const; @@ -1117,7 +1168,16 @@ const row = style({ default: 'gray-300', forcedColors: 'ButtonBorder' }, - forcedColorAdjust: 'none' + forcedColorAdjust: 'none', + color: { + selectionStyle: { + highlight: { + default: 'gray-700', + isHovered: 'gray-800', + isPressed: 'gray-900' + } + } + } }); export interface RowProps extends Pick, 'id' | 'columns' | 'children' | 'textValue' | 'dependencies' | keyof GlobalDOMAttributes> {} @@ -1141,7 +1201,7 @@ export const Row = /*#__PURE__*/ (forwardRef as forwardRefType)(function Row - {selectionMode !== 'none' && selectionBehavior === 'toggle' && ( + {selectionMode !== 'none' && selectionBehavior === 'toggle' && tableVisualOptions.selectionStyle === 'checkbox' && ( diff --git a/packages/@react-spectrum/s2/src/TreeView.tsx b/packages/@react-spectrum/s2/src/TreeView.tsx index 5b958325443..569d1f0391a 100644 --- a/packages/@react-spectrum/s2/src/TreeView.tsx +++ b/packages/@react-spectrum/s2/src/TreeView.tsx @@ -300,52 +300,16 @@ const treeRowBackground = style({ inset: 0, backgroundColor: '--rowBackgroundColor', borderTopStartRadius: { - selectionStyle: { - default: 'default', - highlight: { - default: 'default', - isPreviousSelected: 'none' - } - }, - selectionCornerStyle: { - square: 'none' - } + isRoundTop: 'default' }, borderTopEndRadius: { - selectionStyle: { - default: 'default', - highlight: { - default: 'default', - isPreviousSelected: 'none' - } - }, - selectionCornerStyle: { - square: 'none' - } + isRoundTop: 'default' }, borderBottomStartRadius: { - selectionStyle: { - default: 'default', - highlight: { - default: 'default', - isNextSelected: 'none' - } - }, - selectionCornerStyle: { - square: 'none' - } + isRoundBottom: 'default' }, borderBottomEndRadius: { - selectionStyle: { - default: 'default', - highlight: { - default: 'default', - isNextSelected: 'none' - } - }, - selectionCornerStyle: { - square: 'none' - } + isRoundBottom: 'default' }, borderWidth: 0, borderStyle: 'solid' @@ -470,9 +434,16 @@ export const TreeViewItemContent = (props: TreeViewItemContentProps): ReactNode isNextSelected = state.selectionManager.isSelected(keyAfter); } let isFirst = state.collection.getFirstKey() === id; + let isRoundTop = false; + let isRoundBottom = false; + if (selectionStyle === 'highlight' && selectionCornerStyle === 'round') { + isRoundTop = (isHovered && !isSelected) || (isSelected && !isPreviousSelected); + isRoundBottom = (isHovered && !isSelected) || (isSelected && !isNextSelected); + } + return ( -
-
+
+
{selectionMode !== 'none' && selectionBehavior === 'toggle' && ( // TODO: add transition?
diff --git a/packages/@react-spectrum/s2/stories/HighlightSelectionTable.stories.tsx b/packages/@react-spectrum/s2/stories/HighlightSelectionTable.stories.tsx new file mode 100644 index 00000000000..fada953506c --- /dev/null +++ b/packages/@react-spectrum/s2/stories/HighlightSelectionTable.stories.tsx @@ -0,0 +1,126 @@ +/** + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {CalendarDate, getLocalTimeZone} from '@internationalized/date'; +import {categorizeArgTypes, getActionArgs} from './utils'; +import { + Cell, + Column, + Row, + TableBody, + TableHeader, + TableView, + TableViewProps, + TreeView +} from '../src'; +import type {Meta} from '@storybook/react'; +import React, {ReactElement} from 'react'; +import {style} from '../style/spectrum-theme' with {type: 'macro'}; +import UserGroup from '../s2wf-icons/S2_Icon_UserGroup_20_N.svg'; + +const events = ['onSelectionChange']; + +const meta: Meta = { + title: 'Highlight Selection/TableView', + component: TableView, + parameters: { + layout: 'centered' + }, + tags: ['autodocs'], + args: {...getActionArgs(events)}, + argTypes: { + ...categorizeArgTypes('Events', events), + children: {table: {disable: true}} + } +}; + +export default meta; + +let columns = [ + {name: 'Name', id: 'name', isRowHeader: true, minWidth: 400}, + {name: 'Sharing', id: 'sharing', minWidth: 200}, + {name: 'Date modified', id: 'date', minWidth: 200} +]; + +interface Item { + id: number, + name: { + name: string, + meta: string, + description?: string + }, + sharing: string, + date: CalendarDate +} + +let items: Item[] = [ + {id: 1, name: {name: 'Designer resume', meta: 'PDF', description: 'From Molly Holt'}, sharing: 'public', date: new CalendarDate(2020, 7, 6)}, + // eslint-disable-next-line quotes + {id: 2, name: {name: `Career Management for IC's`, meta: 'PDF'}, sharing: 'public', date: new CalendarDate(2020, 7, 6)}, + {id: 3, name: {name: 'CMP Sessions', meta: 'PDF'}, sharing: 'public', date: new CalendarDate(2020, 7, 6)}, + {id: 4, name: {name: 'Clifton Strength Assessment Info', meta: 'Folder'}, sharing: 'none', date: new CalendarDate(2020, 7, 6)}, + {id: 5, name: {name: 'Personal Brand', meta: 'Zip'}, sharing: 'private', date: new CalendarDate(2020, 7, 6)}, + {id: 6, name: {name: 'Personal Brand', meta: 'Zip'}, sharing: 'private', date: new CalendarDate(2020, 7, 6)}, + {id: 7, name: {name: 'Personal Brand', meta: 'Zip'}, sharing: 'private', date: new CalendarDate(2020, 7, 6)}, + {id: 8, name: {name: 'Personal Brand', meta: 'Zip'}, sharing: 'private', date: new CalendarDate(2020, 7, 6)}, + {id: 9, name: {name: 'Personal Brand', meta: 'Zip'}, sharing: 'private', date: new CalendarDate(2020, 7, 6)}, + {id: 10, name: {name: 'Personal Brand', meta: 'Zip'}, sharing: 'private', date: new CalendarDate(2020, 7, 6)} +]; + +export const DocumentsTable = { + render: (args: TableViewProps): ReactElement => ( + + + {(column) => ( + {column.name} + )} + + + {item => ( + + {(column) => { + if (column.id === 'sharing') { + let content = item[column.id] === 'public' ?
Shared
: 'Only you'; + if (item[column.id] === 'none') { + content = '-'; + } + return {content}; + } + if (column.id === 'name') { + return ( + +
+
{item[column.id].name}
+
+
{item[column.id].meta}
+ {item[column.id].description && <>
·
{item[column.id].description}
} +
+
+
+ ); + } + if (column.id === 'date') { + return {item[column.id].toDate(getLocalTimeZone()).toLocaleDateString('en-US', {year: 'numeric', month: 'long', day: 'numeric'})}; + } + return {item[column.id]}; + }} +
+ )} +
+
+ ), + args: { + overflowMode: 'wrap', + selectionStyle: 'highlight', + selectionMode: 'multiple' + } +}; diff --git a/packages/@react-spectrum/s2/stories/HighlightSelection.stories.tsx b/packages/@react-spectrum/s2/stories/HighlightSelectionTree.stories.tsx similarity index 99% rename from packages/@react-spectrum/s2/stories/HighlightSelection.stories.tsx rename to packages/@react-spectrum/s2/stories/HighlightSelectionTree.stories.tsx index 2bf94dda138..1f1220e9865 100644 --- a/packages/@react-spectrum/s2/stories/HighlightSelection.stories.tsx +++ b/packages/@react-spectrum/s2/stories/HighlightSelectionTree.stories.tsx @@ -35,12 +35,12 @@ import LockOpen from '../s2wf-icons/S2_Icon_LockOpen_20_N.svg'; import type {Meta, StoryObj} from '@storybook/react'; import React, {ReactElement, useState} from 'react'; import Visibility from '../s2wf-icons/S2_Icon_Visibility_20_N.svg'; - import VisibilityOff from '../s2wf-icons/S2_Icon_VisibilityOff_20_N.svg'; const events = ['onSelectionChange']; const meta: Meta = { + title: 'Highlight Selection/TreeView', component: TreeView, parameters: { layout: 'centered' @@ -147,7 +147,8 @@ export const LayersTree: StoryObj = { args: { defaultExpandedKeys: ['layer-group-2'], selectionMode: 'multiple', - selectionStyle: 'highlight' + selectionStyle: 'highlight', + selectionCornerStyle: 'round' } }; From 1d8e8a1d2105a27b7a2cc50827bf0ba12a3a487b Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Thu, 18 Sep 2025 17:32:28 +1000 Subject: [PATCH 05/60] Add highlight selection option b to ListView --- packages/@react-spectrum/s2/src/ListView.tsx | 59 ++++++------ .../HighlightSelectionList.stories.tsx | 89 +++++++++++++++++++ .../HighlightSelectionTable.stories.tsx | 5 +- 3 files changed, 124 insertions(+), 29 deletions(-) create mode 100644 packages/@react-spectrum/s2/stories/HighlightSelectionList.stories.tsx diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index a086359102e..4b2ce2e0c63 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -27,7 +27,7 @@ import { SlotProps, Virtualizer } from 'react-aria-components'; -import {controlFont, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; +import {controlFont, getAllowedOverrides, StyleProps, StylesPropWithHeight, UnsafeStyles} from './style-utils' with {type: 'macro'}; import {createContext, forwardRef, JSXElementConstructor, ReactElement, ReactNode, useContext, useRef} from 'react'; import {DOMProps, DOMRef, DOMRefValue, forwardRefType, GlobalDOMAttributes} from '@react-types/shared'; import {IconContext} from './Icon'; @@ -38,7 +38,8 @@ import {useDOMRef} from '@react-spectrum/utils'; import {useScale} from './utils'; import {useSpectrumContextProps} from './useSpectrumContextProps'; -export interface ListViewProps extends Omit, 'className' | 'style' | 'children' | keyof GlobalDOMAttributes>, DOMProps, StyleProps, ListViewStylesProps, SlotProps { +export interface ListViewProps extends Omit, 'className' | 'style' | 'children' | keyof GlobalDOMAttributes>, DOMProps, UnsafeStyles, ListViewStylesProps, SlotProps { + styles?: StylesPropWithHeight, /** * Whether to automatically focus the Inline Alert when it first renders. */ @@ -47,7 +48,9 @@ export interface ListViewProps extends Omit, 'className' | ' } interface ListViewStylesProps { - isQuiet?: boolean + isQuiet?: boolean, + isEmphasized?: boolean, + selectionStyle?: 'highlight' | 'checkbox' } export interface ListViewItemProps extends Omit, StyleProps { @@ -64,7 +67,7 @@ const ListViewRendererContext = createContext({}); export const ListViewContext = createContext>, DOMRefValue>>(null); -let InternalListViewContext = createContext<{isQuiet?: boolean}>({}); +let InternalListViewContext = createContext<{isQuiet?: boolean, isEmphasized?: boolean}>({}); const listView = style({ ...focusRing(), @@ -81,14 +84,14 @@ const listView = style({ borderColor: 'gray-300', borderWidth: 1, borderStyle: 'solid' -}, getAllowedOverrides()); +}, getAllowedOverrides({height: true})); export const ListView = /*#__PURE__*/ (forwardRef as forwardRefType)(function ListView( props: ListViewProps, ref: DOMRef ) { [props, ref] = useSpectrumContextProps(props, ref, ListViewContext); - let {children, isQuiet} = props; + let {children, isQuiet, isEmphasized} = props; let scale = useScale(); let renderer; @@ -105,7 +108,7 @@ export const ListView = /*#__PURE__*/ (forwardRef as forwardRefType)(function Li rowHeight: scale === 'large' ? 50 : 40 }}> - + ({ +const listitem = style({ ...focusRing(), outlineOffset: 0, columnGap: 0, @@ -130,10 +133,16 @@ const listitem = style (props.UNSAFE_className || '') + listitem({ ...renderProps, isLink, - isQuiet + isQuiet, + isEmphasized }, props.styles)}> {(renderProps) => { let {children} = props; @@ -274,7 +281,7 @@ export function ListViewItem(props: ListViewItemProps): ReactNode { } }], [ImageContext, {styles: image}], - [ActionButtonGroupContext, {styles: actionButtonGroup}] + [ActionButtonGroupContext, {styles: actionButtonGroup, size: 'S', isQuiet: true}] ]}> {typeof children === 'string' ? {children} : children} diff --git a/packages/@react-spectrum/s2/stories/HighlightSelectionList.stories.tsx b/packages/@react-spectrum/s2/stories/HighlightSelectionList.stories.tsx new file mode 100644 index 00000000000..408d37c7987 --- /dev/null +++ b/packages/@react-spectrum/s2/stories/HighlightSelectionList.stories.tsx @@ -0,0 +1,89 @@ +/** + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +// TODO: pull all the highlight styles out into a separate macro(s) for the background color, text color, etc. +import ABC from '../s2wf-icons/S2_Icon_ABC_20_N.svg'; +import { + ActionButton, + ActionButtonGroup, + ListView, + ListViewItem, + Text +} from '../src'; +import Add from '../s2wf-icons/S2_Icon_Add_20_N.svg'; +import {categorizeArgTypes, getActionArgs} from './utils'; +import InfoCircle from '../s2wf-icons/S2_Icon_InfoCircle_20_N.svg'; +import type {Meta, StoryObj} from '@storybook/react'; +import React from 'react'; +import {style} from '../style/spectrum-theme' with {type: 'macro'}; +import TextNumbers from '../s2wf-icons/S2_Icon_TextNumbers_20_N.svg'; + +const events = ['onSelectionChange']; + +const meta: Meta = { + title: 'Highlight Selection/ListView', + component: ListView, + parameters: { + layout: 'centered' + }, + tags: ['autodocs'], + args: {...getActionArgs(events)}, + argTypes: { + ...categorizeArgTypes('Events', events), + children: {table: {disable: true}} + } +}; + +export default meta; + +interface Item { + id: number, + name: string, + type: 'letter' | 'number' +} + +let items: Item[] = [ + {id: 1, name: 'Count', type: 'number'}, + {id: 2, name: 'City', type: 'letter'}, + {id: 3, name: 'Count of identities', type: 'number'}, + {id: 4, name: 'Current day', type: 'number'}, + {id: 5, name: 'Current month', type: 'letter'}, + {id: 6, name: 'Current week', type: 'number'}, + {id: 7, name: 'Current year', type: 'number'}, + {id: 8, name: 'Current whatever', type: 'number'}, + {id: 9, name: 'Alphabet', type: 'letter'}, + {id: 10, name: 'Numbers', type: 'number'} +]; + +export const DocumentsTable: StoryObj = { + render: (args) => ( + + {item => ( + + {item.type === 'number' ? : } + {item.name} + + + + + + + + + + )} + + ), + args: { + selectionStyle: 'highlight', + selectionMode: 'multiple' + } +}; diff --git a/packages/@react-spectrum/s2/stories/HighlightSelectionTable.stories.tsx b/packages/@react-spectrum/s2/stories/HighlightSelectionTable.stories.tsx index fada953506c..3d3703be546 100644 --- a/packages/@react-spectrum/s2/stories/HighlightSelectionTable.stories.tsx +++ b/packages/@react-spectrum/s2/stories/HighlightSelectionTable.stories.tsx @@ -19,8 +19,7 @@ import { TableBody, TableHeader, TableView, - TableViewProps, - TreeView + TableViewProps } from '../src'; import type {Meta} from '@storybook/react'; import React, {ReactElement} from 'react'; @@ -29,7 +28,7 @@ import UserGroup from '../s2wf-icons/S2_Icon_UserGroup_20_N.svg'; const events = ['onSelectionChange']; -const meta: Meta = { +const meta: Meta = { title: 'Highlight Selection/TableView', component: TableView, parameters: { From e62eb1d706d39ef47ed0e42322d16196ea66a532 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Thu, 18 Sep 2025 17:49:27 +1000 Subject: [PATCH 06/60] change selection behaviour of table for highlight mode --- packages/@react-spectrum/s2/src/TableView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index 0295a9f95e4..fc88e521ac4 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -359,9 +359,9 @@ export const TableView = forwardRef(function TableView(props: TableViewProps, re isCheckboxSelection, isQuiet })} - selectionBehavior="toggle" onRowAction={onAction} {...otherProps} + selectionBehavior={selectionStyle === 'highlight' ? 'replace' : 'toggle'} selectedKeys={selectedKeys} defaultSelectedKeys={undefined} onSelectionChange={onSelectionChange} /> From 020541a6e14b0d65460672f8788fb465de7b0333 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Fri, 19 Sep 2025 15:33:46 +1000 Subject: [PATCH 07/60] add option D as highlightMode --- packages/@react-spectrum/s2/src/ListView.tsx | 43 ++++++++++++++----- packages/@react-spectrum/s2/src/TableView.tsx | 29 +++++++++++-- packages/@react-spectrum/s2/src/TreeView.tsx | 37 ++++++++++------ .../HighlightSelectionList.stories.tsx | 7 +-- .../HighlightSelectionTable.stories.tsx | 9 ++-- .../HighlightSelectionTree.stories.tsx | 9 ++-- 6 files changed, 95 insertions(+), 39 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index 4b2ce2e0c63..7ca81cb0224 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -50,7 +50,8 @@ export interface ListViewProps extends Omit, 'className' | ' interface ListViewStylesProps { isQuiet?: boolean, isEmphasized?: boolean, - selectionStyle?: 'highlight' | 'checkbox' + selectionStyle?: 'highlight' | 'checkbox', + highlightMode?: 'normal' | 'inverse' } export interface ListViewItemProps extends Omit, StyleProps { @@ -67,7 +68,7 @@ const ListViewRendererContext = createContext({}); export const ListViewContext = createContext>, DOMRefValue>>(null); -let InternalListViewContext = createContext<{isQuiet?: boolean, isEmphasized?: boolean}>({}); +let InternalListViewContext = createContext<{isQuiet?: boolean, isEmphasized?: boolean, highlightMode?: 'normal' | 'inverse'}>({}); const listView = style({ ...focusRing(), @@ -91,7 +92,7 @@ export const ListView = /*#__PURE__*/ (forwardRef as forwardRefType)(function Li ref: DOMRef ) { [props, ref] = useSpectrumContextProps(props, ref, ListViewContext); - let {children, isQuiet, isEmphasized} = props; + let {children, isQuiet, isEmphasized, highlightMode} = props; let scale = useScale(); let renderer; @@ -108,7 +109,7 @@ export const ListView = /*#__PURE__*/ (forwardRef as forwardRefType)(function Li rowHeight: scale === 'large' ? 50 : 40 }}> - + ({ +const listitem = style({ ...focusRing(), outlineOffset: 0, columnGap: 0, @@ -135,14 +136,28 @@ const listitem = style {(renderProps) => { let {children} = props; @@ -281,7 +297,12 @@ export function ListViewItem(props: ListViewItemProps): ReactNode { } }], [ImageContext, {styles: image}], - [ActionButtonGroupContext, {styles: actionButtonGroup, size: 'S', isQuiet: true}] + [ActionButtonGroupContext, { + styles: actionButtonGroup, + size: 'S', + isQuiet: true, + staticColor: highlightMode === 'inverse' && renderProps.isSelected ? 'white' : undefined // how to invert this and react to color scheme? also, too bright/bold in dark mode unselected + }] ]}> {typeof children === 'string' ? {children} : children} diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index fc88e521ac4..be2dce3de83 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -107,7 +107,8 @@ interface S2TableProps { /** Provides the ActionBar to display when rows are selected in the TableView. */ renderActionBar?: (selectedKeys: 'all' | Set) => ReactElement, selectionStyle?: 'highlight' | 'checkbox', - isEmphasized?: boolean + isEmphasized?: boolean, + highlightMode?: 'normal' | 'inverse' } // TODO: Note that loadMore and loadingState are now on the Table instead of on the TableBody @@ -287,6 +288,7 @@ export const TableView = forwardRef(function TableView(props: TableViewProps, re onLoadMore, selectionStyle = 'checkbox', isEmphasized = false, + highlightMode = 'normal', ...otherProps } = props; @@ -313,8 +315,9 @@ export const TableView = forwardRef(function TableView(props: TableViewProps, re isInResizeMode, setIsInResizeMode, selectionStyle, - isEmphasized - }), [isQuiet, density, overflowMode, loadingState, onLoadMore, isInResizeMode, setIsInResizeMode, selectionStyle, isEmphasized]); + isEmphasized, + highlightMode + }), [isQuiet, density, overflowMode, loadingState, onLoadMore, isInResizeMode, setIsInResizeMode, selectionStyle, isEmphasized, highlightMode]); let scrollRef = useRef(null); let isCheckboxSelection = (props.selectionMode === 'multiple' || props.selectionMode === 'single') && selectionStyle === 'checkbox'; @@ -984,6 +987,10 @@ const checkboxCellStyle = style({ const cellContent = style({ truncate: true, + '--iconPrimary': { + type: 'fill', + value: 'currentColor' + }, whiteSpace: { default: 'nowrap', overflowMode: { @@ -1007,6 +1014,13 @@ const cellContent = style({ default: -4, isSticky: 0 }, + color: { + highlightMode: { + inverse: { + isSelected: 'gray-25' + } + } + }, backgroundColor: { default: 'transparent', isSticky: '--rowBackgroundColor' @@ -1109,7 +1123,14 @@ const rowBackgroundColor = { isPressed: 'gray-100', isSelected: { default: 'gray-100', - isEmphasized: 'blue-200' + highlightMode: { + normal: { + isEmphasized: 'blue-200' + }, + inverse: { + isEmphasized: 'blue-800' + } + } }, forcedColors: { default: 'Background' diff --git a/packages/@react-spectrum/s2/src/TreeView.tsx b/packages/@react-spectrum/s2/src/TreeView.tsx index 569d1f0391a..7c144c3f6c8 100644 --- a/packages/@react-spectrum/s2/src/TreeView.tsx +++ b/packages/@react-spectrum/s2/src/TreeView.tsx @@ -58,7 +58,8 @@ interface S2TreeProps { /** Whether the tree should be displayed with a [emphasized style](https://spectrum.adobe.com/page/tree-view/#Emphasis). */ isEmphasized?: boolean, selectionStyle?: 'highlight' | 'checkbox', - selectionCornerStyle?: 'square' | 'round' + selectionCornerStyle?: 'square' | 'round', + highlightMode?: 'normal' | 'inverse' } export interface TreeViewProps extends Omit, 'style' | 'className' | 'onRowAction' | 'selectionBehavior' | 'onScroll' | 'onCellAction' | 'dragAndDropHooks' | keyof GlobalDOMAttributes>, UnsafeStyles, S2TreeProps { @@ -84,7 +85,7 @@ const TreeRendererContext = createContext({}); export const TreeViewContext = createContext>, DOMRefValue>>(null); -let InternalTreeContext = createContext<{isDetached?: boolean, isEmphasized?: boolean, selectionStyle: 'highlight' | 'checkbox', selectionCornerStyle: 'square' | 'round'}>({selectionStyle: 'checkbox', selectionCornerStyle: 'round'}); +let InternalTreeContext = createContext<{isDetached?: boolean, isEmphasized?: boolean, selectionStyle: 'highlight' | 'checkbox', selectionCornerStyle: 'square' | 'round', highlightMode?: 'normal' | 'inverse'}>({selectionStyle: 'checkbox', selectionCornerStyle: 'round'}); // TODO: the below is needed so the borders of the top and bottom row isn't cut off if the TreeView is wrapped within a container by always reserving the 2px needed for the // keyboard focus ring. Perhaps find a different way of rendering the outlines since the top of the item doesn't @@ -116,7 +117,7 @@ const tree = style({ */ export const TreeView = /*#__PURE__*/ (forwardRef as forwardRefType)(function TreeView(props: TreeViewProps, ref: DOMRef) { [props, ref] = useSpectrumContextProps(props, ref, TreeViewContext); - let {children, isDetached, isEmphasized, selectionStyle = 'checkbox', selectionCornerStyle = 'round', UNSAFE_className, UNSAFE_style} = props; + let {children, isDetached, isEmphasized, selectionStyle = 'checkbox', selectionCornerStyle = 'round', UNSAFE_className, UNSAFE_style, highlightMode = 'normal'} = props; let scale = useScale(); let renderer; @@ -134,7 +135,7 @@ export const TreeView = /*#__PURE__*/ (forwardRef as forwardRefType)(function Tr gap: isDetached ? 2 : 0 }}> - + { let { href } = props; - let {isDetached, isEmphasized, selectionStyle} = useContext(InternalTreeContext); + let {isDetached, isEmphasized, selectionStyle, highlightMode} = useContext(InternalTreeContext); return ( treeRow({ ...renderProps, - isLink: !!href, isEmphasized, - selectionStyle + isLink: !!href, + isEmphasized, + selectionStyle, + highlightMode }) + (renderProps.isFocusVisible && !isDetached && selectionStyle !== 'highlight' ? ' ' + treeRowFocusIndicator : '')} /> ); }; @@ -416,7 +429,7 @@ export const TreeViewItemContent = (props: TreeViewItemContentProps): ReactNode let { children } = props; - let {isDetached, isEmphasized, selectionStyle, selectionCornerStyle} = useContext(InternalTreeContext); + let {isDetached, isEmphasized, selectionStyle, selectionCornerStyle, highlightMode} = useContext(InternalTreeContext); let scale = useScale(); return ( @@ -442,7 +455,7 @@ export const TreeViewItemContent = (props: TreeViewItemContentProps): ReactNode } return ( -
+
{selectionMode !== 'none' && selectionBehavior === 'toggle' && ( // TODO: add transition? @@ -466,7 +479,7 @@ export const TreeViewItemContent = (props: TreeViewItemContentProps): ReactNode render: centerBaseline({slot: 'icon', styles: treeIcon}), styles: style({size: fontRelative(20), flexShrink: 0}) }], - [ActionButtonGroupContext, {styles: treeActions, size: 'S'}], + [ActionButtonGroupContext, {styles: treeActions, size: 'S', staticColor: highlightMode === 'inverse' && isSelected ? 'white' : undefined}], [ActionMenuContext, {styles: treeActionMenu, isQuiet: true, size: 'S'}] ]}> {typeof children === 'string' ? {children} : children} diff --git a/packages/@react-spectrum/s2/stories/HighlightSelectionList.stories.tsx b/packages/@react-spectrum/s2/stories/HighlightSelectionList.stories.tsx index 408d37c7987..aa6cae100e3 100644 --- a/packages/@react-spectrum/s2/stories/HighlightSelectionList.stories.tsx +++ b/packages/@react-spectrum/s2/stories/HighlightSelectionList.stories.tsx @@ -34,7 +34,6 @@ const meta: Meta = { parameters: { layout: 'centered' }, - tags: ['autodocs'], args: {...getActionArgs(events)}, argTypes: { ...categorizeArgTypes('Events', events), @@ -63,7 +62,7 @@ let items: Item[] = [ {id: 10, name: 'Numbers', type: 'number'} ]; -export const DocumentsTable: StoryObj = { +export const AttributesList: StoryObj = { render: (args) => ( {item => ( @@ -84,6 +83,8 @@ export const DocumentsTable: StoryObj = { ), args: { selectionStyle: 'highlight', - selectionMode: 'multiple' + selectionMode: 'multiple', + highlightMode: 'inverse', + isEmphasized: true } }; diff --git a/packages/@react-spectrum/s2/stories/HighlightSelectionTable.stories.tsx b/packages/@react-spectrum/s2/stories/HighlightSelectionTable.stories.tsx index 3d3703be546..09e76f893a2 100644 --- a/packages/@react-spectrum/s2/stories/HighlightSelectionTable.stories.tsx +++ b/packages/@react-spectrum/s2/stories/HighlightSelectionTable.stories.tsx @@ -34,7 +34,6 @@ const meta: Meta = { parameters: { layout: 'centered' }, - tags: ['autodocs'], args: {...getActionArgs(events)}, argTypes: { ...categorizeArgTypes('Events', events), @@ -99,10 +98,6 @@ export const DocumentsTable = {
{item[column.id].name}
-
-
{item[column.id].meta}
- {item[column.id].description && <>
·
{item[column.id].description}
} -
); @@ -120,6 +115,8 @@ export const DocumentsTable = { args: { overflowMode: 'wrap', selectionStyle: 'highlight', - selectionMode: 'multiple' + selectionMode: 'multiple', + highlightMode: 'inverse', + isEmphasized: true } }; diff --git a/packages/@react-spectrum/s2/stories/HighlightSelectionTree.stories.tsx b/packages/@react-spectrum/s2/stories/HighlightSelectionTree.stories.tsx index 1f1220e9865..057290d74ff 100644 --- a/packages/@react-spectrum/s2/stories/HighlightSelectionTree.stories.tsx +++ b/packages/@react-spectrum/s2/stories/HighlightSelectionTree.stories.tsx @@ -45,7 +45,6 @@ const meta: Meta = { parameters: { layout: 'centered' }, - tags: ['autodocs'], args: {...getActionArgs(events)}, argTypes: { ...categorizeArgTypes('Events', events), @@ -148,7 +147,9 @@ export const LayersTree: StoryObj = { defaultExpandedKeys: ['layer-group-2'], selectionMode: 'multiple', selectionStyle: 'highlight', - selectionCornerStyle: 'round' + selectionCornerStyle: 'round', + highlightMode: 'inverse', + isEmphasized: true } }; @@ -265,6 +266,8 @@ export const FileTree: StoryObj = { render: TreeExampleFiles, args: { selectionMode: 'multiple', - selectionStyle: 'highlight' + selectionStyle: 'highlight', + highlightMode: 'inverse', + isEmphasized: true } }; From 22545f2e3632af8c17c1483caa68c16b8a935b4a Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Thu, 2 Oct 2025 10:05:19 +1000 Subject: [PATCH 08/60] change font weight to bold for highlight selection --- packages/@react-spectrum/s2/src/ListView.tsx | 2 +- packages/@react-spectrum/s2/src/TableView.tsx | 2 +- packages/@react-spectrum/s2/src/TreeView.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index 7ca81cb0224..56bdde25d48 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -201,7 +201,7 @@ export let label = style({ color: 'inherit', fontWeight: { default: 'normal', - isSelected: 'medium' + isSelected: 'bold' }, // TODO: token values for padding not defined yet, revisit marginTop: '--labelPadding', diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index be2dce3de83..b097a99901a 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -1030,7 +1030,7 @@ const cellContent = style({ highlight: { default: 'normal', isRowHeader: { - isSelected: 'medium' + isSelected: 'bold' } } } diff --git a/packages/@react-spectrum/s2/src/TreeView.tsx b/packages/@react-spectrum/s2/src/TreeView.tsx index 7c144c3f6c8..27f7ed072d6 100644 --- a/packages/@react-spectrum/s2/src/TreeView.tsx +++ b/packages/@react-spectrum/s2/src/TreeView.tsx @@ -271,7 +271,7 @@ const treeCellGrid = style({ }, fontWeight: { default: 'normal', - isSelected: 'medium' + isSelected: 'bold' }, '--rowSelectedBorderColor': { type: 'outlineColor', From 49beedfc09fc1456d1b638085e15c427bcf2d665 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Fri, 3 Oct 2025 08:55:45 +1000 Subject: [PATCH 09/60] update background color --- packages/@react-spectrum/s2/src/ListView.tsx | 2 +- packages/@react-spectrum/s2/src/TableView.tsx | 2 +- packages/@react-spectrum/s2/src/TreeView.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index 56bdde25d48..ed6871eaac3 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -139,7 +139,7 @@ const listitem = style Date: Tue, 21 Oct 2025 17:07:15 +1100 Subject: [PATCH 10/60] Start new highlight selection prototypes, ListView and part of TreeView --- packages/@react-spectrum/s2/src/ListView.tsx | 106 +++++++---- packages/@react-spectrum/s2/src/TreeView.tsx | 174 +++++++++++------- .../HighlightSelectionTree.stories.tsx | 2 - .../s2/style/spectrum-theme.ts | 21 ++- .../react-aria-components/src/GridList.tsx | 2 + 5 files changed, 193 insertions(+), 112 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index ed6871eaac3..62a14080154 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -68,7 +68,7 @@ const ListViewRendererContext = createContext({}); export const ListViewContext = createContext>, DOMRefValue>>(null); -let InternalListViewContext = createContext<{isQuiet?: boolean, isEmphasized?: boolean, highlightMode?: 'normal' | 'inverse'}>({}); +let InternalListViewContext = createContext<{isQuiet?: boolean, isEmphasized?: boolean}>({}); const listView = style({ ...focusRing(), @@ -92,7 +92,7 @@ export const ListView = /*#__PURE__*/ (forwardRef as forwardRefType)(function Li ref: DOMRef ) { [props, ref] = useSpectrumContextProps(props, ref, ListViewContext); - let {children, isQuiet, isEmphasized, highlightMode} = props; + let {children, isQuiet, isEmphasized} = props; let scale = useScale(); let renderer; @@ -109,7 +109,7 @@ export const ListView = /*#__PURE__*/ (forwardRef as forwardRefType)(function Li rowHeight: scale === 'large' ? 50 : 40 }}> - + ({ +const listitem = style({ ...focusRing(), - outlineOffset: 0, + boxSizing: 'border-box', + outlineOffset: -2, columnGap: 0, paddingX: 0, paddingBottom: '--labelPadding', backgroundColor: { default: 'transparent', isHovered: 'gray-100', - isSelected: 'gray-100', - highlightMode: { - normal: { - isEmphasized: { - isSelected: 'blue-900/10' - } - }, - inverse: { - isEmphasized: { - isSelected: 'blue-800' - } - } + isSelected: { + default: 'blue-900/10', + isHovered: 'blue-900/15' } }, color: { default: baseColor('neutral-subdued'), isHovered: 'gray-800', - isSelected: { - highlightMode: { - normal: 'gray-900', - inverse: 'gray-25' - } - }, + isSelected: 'gray-900', isDisabled: { default: 'disabled', forcedColors: 'GrayText' @@ -184,14 +182,48 @@ const listitem = style {(renderProps) => { let {children} = props; @@ -300,8 +327,7 @@ export function ListViewItem(props: ListViewItemProps): ReactNode { [ActionButtonGroupContext, { styles: actionButtonGroup, size: 'S', - isQuiet: true, - staticColor: highlightMode === 'inverse' && renderProps.isSelected ? 'white' : undefined // how to invert this and react to color scheme? also, too bright/bold in dark mode unselected + isQuiet: true }] ]}> {typeof children === 'string' ? {children} : children} diff --git a/packages/@react-spectrum/s2/src/TreeView.tsx b/packages/@react-spectrum/s2/src/TreeView.tsx index 487da480938..c2bc05d4ec5 100644 --- a/packages/@react-spectrum/s2/src/TreeView.tsx +++ b/packages/@react-spectrum/s2/src/TreeView.tsx @@ -58,8 +58,7 @@ interface S2TreeProps { /** Whether the tree should be displayed with a [emphasized style](https://spectrum.adobe.com/page/tree-view/#Emphasis). */ isEmphasized?: boolean, selectionStyle?: 'highlight' | 'checkbox', - selectionCornerStyle?: 'square' | 'round', - highlightMode?: 'normal' | 'inverse' + selectionCornerStyle?: 'square' | 'round' } export interface TreeViewProps extends Omit, 'style' | 'className' | 'onRowAction' | 'selectionBehavior' | 'onScroll' | 'onCellAction' | 'dragAndDropHooks' | keyof GlobalDOMAttributes>, UnsafeStyles, S2TreeProps { @@ -85,7 +84,7 @@ const TreeRendererContext = createContext({}); export const TreeViewContext = createContext>, DOMRefValue>>(null); -let InternalTreeContext = createContext<{isDetached?: boolean, isEmphasized?: boolean, selectionStyle: 'highlight' | 'checkbox', selectionCornerStyle: 'square' | 'round', highlightMode?: 'normal' | 'inverse'}>({selectionStyle: 'checkbox', selectionCornerStyle: 'round'}); +let InternalTreeContext = createContext<{isDetached?: boolean, isEmphasized?: boolean, selectionStyle: 'highlight' | 'checkbox', selectionCornerStyle: 'square' | 'round'}>({selectionStyle: 'checkbox', selectionCornerStyle: 'round'}); // TODO: the below is needed so the borders of the top and bottom row isn't cut off if the TreeView is wrapped within a container by always reserving the 2px needed for the // keyboard focus ring. Perhaps find a different way of rendering the outlines since the top of the item doesn't @@ -117,7 +116,7 @@ const tree = style({ */ export const TreeView = /*#__PURE__*/ (forwardRef as forwardRefType)(function TreeView(props: TreeViewProps, ref: DOMRef) { [props, ref] = useSpectrumContextProps(props, ref, TreeViewContext); - let {children, isDetached, isEmphasized, selectionStyle = 'checkbox', selectionCornerStyle = 'round', UNSAFE_className, UNSAFE_style, highlightMode = 'normal'} = props; + let {children, isDetached, isEmphasized, selectionStyle = 'checkbox', selectionCornerStyle = 'round', UNSAFE_className, UNSAFE_style} = props; let scale = useScale(); let renderer; @@ -135,7 +134,7 @@ export const TreeView = /*#__PURE__*/ (forwardRef as forwardRefType)(function Tr gap: isDetached ? 2 : 0 }}> - + { let { href } = props; - let {isDetached, isEmphasized, selectionStyle, highlightMode} = useContext(InternalTreeContext); + let {isDetached, isEmphasized, selectionStyle, selectionCornerStyle} = useContext(InternalTreeContext); return ( { isLink: !!href, isEmphasized, selectionStyle, - highlightMode - }) + (renderProps.isFocusVisible && !isDetached && selectionStyle !== 'highlight' ? ' ' + treeRowFocusIndicator : '')} /> + isRound: selectionCornerStyle === 'round' + }) + (renderProps.isFocusVisible && !isDetached ? ' ' + treeRowFocusIndicator : '')} /> ); }; @@ -429,7 +471,7 @@ export const TreeViewItemContent = (props: TreeViewItemContentProps): ReactNode let { children } = props; - let {isDetached, isEmphasized, selectionStyle, selectionCornerStyle, highlightMode} = useContext(InternalTreeContext); + let {isDetached, isEmphasized, selectionCornerStyle} = useContext(InternalTreeContext); let scale = useScale(); return ( @@ -447,16 +489,10 @@ export const TreeViewItemContent = (props: TreeViewItemContentProps): ReactNode isNextSelected = state.selectionManager.isSelected(keyAfter); } let isFirst = state.collection.getFirstKey() === id; - let isRoundTop = false; - let isRoundBottom = false; - if (selectionStyle === 'highlight' && selectionCornerStyle === 'round') { - isRoundTop = (isHovered && !isSelected) || (isSelected && !isPreviousSelected); - isRoundBottom = (isHovered && !isSelected) || (isSelected && !isNextSelected); - } + let isRound = selectionCornerStyle === 'round'; return ( -
-
+
{selectionMode !== 'none' && selectionBehavior === 'toggle' && ( // TODO: add transition?
@@ -479,7 +515,7 @@ export const TreeViewItemContent = (props: TreeViewItemContentProps): ReactNode render: centerBaseline({slot: 'icon', styles: treeIcon}), styles: style({size: fontRelative(20), flexShrink: 0}) }], - [ActionButtonGroupContext, {styles: treeActions, size: 'S', staticColor: highlightMode === 'inverse' && isSelected ? 'white' : undefined}], + [ActionButtonGroupContext, {styles: treeActions, size: 'S'}], [ActionMenuContext, {styles: treeActionMenu, isQuiet: true, size: 'S'}] ]}> {typeof children === 'string' ? {children} : children} diff --git a/packages/@react-spectrum/s2/stories/HighlightSelectionTree.stories.tsx b/packages/@react-spectrum/s2/stories/HighlightSelectionTree.stories.tsx index 057290d74ff..df4d1e7d695 100644 --- a/packages/@react-spectrum/s2/stories/HighlightSelectionTree.stories.tsx +++ b/packages/@react-spectrum/s2/stories/HighlightSelectionTree.stories.tsx @@ -148,7 +148,6 @@ export const LayersTree: StoryObj = { selectionMode: 'multiple', selectionStyle: 'highlight', selectionCornerStyle: 'round', - highlightMode: 'inverse', isEmphasized: true } }; @@ -267,7 +266,6 @@ export const FileTree: StoryObj = { args: { selectionMode: 'multiple', selectionStyle: 'highlight', - highlightMode: 'inverse', isEmphasized: true } }; diff --git a/packages/@react-spectrum/s2/style/spectrum-theme.ts b/packages/@react-spectrum/s2/style/spectrum-theme.ts index 422d37bd360..a6fd9414134 100644 --- a/packages/@react-spectrum/s2/style/spectrum-theme.ts +++ b/packages/@react-spectrum/s2/style/spectrum-theme.ts @@ -612,7 +612,25 @@ export const style = createTheme({ pasteboard: weirdColorToken('background-pasteboard-color'), elevated: weirdColorToken('background-elevated-color') }), - borderColor: new SpectrumColorProperty('borderColor', { + borderStartColor: new SpectrumColorProperty('borderInlineStartColor', { + ...baseColors, + negative: colorToken('negative-border-color-default'), + disabled: colorToken('disabled-border-color'), + 'neutral-subdued': colorToken('neutral-subdued-content-color-default') + }), + borderTopColor: new SpectrumColorProperty('borderTopColor', { + ...baseColors, + negative: colorToken('negative-border-color-default'), + disabled: colorToken('disabled-border-color'), + 'neutral-subdued': colorToken('neutral-subdued-content-color-default') + }), + borderEndColor: new SpectrumColorProperty('borderInlineEndColor', { + ...baseColors, + negative: colorToken('negative-border-color-default'), + disabled: colorToken('disabled-border-color'), + 'neutral-subdued': colorToken('neutral-subdued-content-color-default') + }), + borderBottomColor: new SpectrumColorProperty('borderBottomColor', { ...baseColors, negative: colorToken('negative-border-color-default'), disabled: colorToken('disabled-border-color'), @@ -984,6 +1002,7 @@ export const style = createTheme({ borderBottomRadius: ['borderBottomStartRadius', 'borderBottomEndRadius'] as const, borderStartRadius: ['borderTopStartRadius', 'borderBottomStartRadius'] as const, borderEndRadius: ['borderTopEndRadius', 'borderBottomEndRadius'] as const, + borderColor: ['borderTopColor', 'borderBottomColor', 'borderStartColor', 'borderEndColor'] as const, translate: ['translateX', 'translateY'] as const, scale: ['scaleX', 'scaleY'] as const, inset: ['top', 'bottom', 'insetStart', 'insetEnd'] as const, diff --git a/packages/react-aria-components/src/GridList.tsx b/packages/react-aria-components/src/GridList.tsx index e010ca598c9..2adbb4950ae 100644 --- a/packages/react-aria-components/src/GridList.tsx +++ b/packages/react-aria-components/src/GridList.tsx @@ -350,6 +350,8 @@ export const GridListItem = /*#__PURE__*/ createLeafComponent(ItemNode, function ...states, isFirstItem: item.key === state.collection.getFirstKey(), isLastItem: item.key === state.collection.getLastKey(), + isNextSelected: state.collection.getKeyAfter(item.key) !== null && state.selectionManager.isSelected(state.collection.getKeyAfter(item.key)!) || undefined, + isPrevSelected: state.collection.getKeyBefore(item.key) !== null && state.selectionManager.isSelected(state.collection.getKeyBefore(item.key)!) || undefined, isHovered, isFocusVisible, selectionMode: state.selectionManager.selectionMode, From e5fc83d6eebb2f3e1231fd0d2b28aebaa6f40b72 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Tue, 21 Oct 2025 17:12:24 +1100 Subject: [PATCH 11/60] fix lint --- packages/react-aria-components/src/GridList.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/react-aria-components/src/GridList.tsx b/packages/react-aria-components/src/GridList.tsx index 2adbb4950ae..672c0e7bb97 100644 --- a/packages/react-aria-components/src/GridList.tsx +++ b/packages/react-aria-components/src/GridList.tsx @@ -274,7 +274,9 @@ function GridListInner({props, collection, gridListRef: ref}: export interface GridListItemRenderProps extends ItemRenderProps { isFirstItem: boolean, - isLastItem: boolean + isLastItem: boolean, + isNextSelected: boolean, + isPrevSelected: boolean } export interface GridListItemProps extends RenderProps, LinkDOMProps, HoverEvents, PressEvents, Omit, 'onClick'> { @@ -350,8 +352,8 @@ export const GridListItem = /*#__PURE__*/ createLeafComponent(ItemNode, function ...states, isFirstItem: item.key === state.collection.getFirstKey(), isLastItem: item.key === state.collection.getLastKey(), - isNextSelected: state.collection.getKeyAfter(item.key) !== null && state.selectionManager.isSelected(state.collection.getKeyAfter(item.key)!) || undefined, - isPrevSelected: state.collection.getKeyBefore(item.key) !== null && state.selectionManager.isSelected(state.collection.getKeyBefore(item.key)!) || undefined, + isNextSelected: state.collection.getKeyAfter(item.key) !== null && state.selectionManager.isSelected(state.collection.getKeyAfter(item.key)!) || false, + isPrevSelected: state.collection.getKeyBefore(item.key) !== null && state.selectionManager.isSelected(state.collection.getKeyBefore(item.key)!) || false, isHovered, isFocusVisible, selectionMode: state.selectionManager.selectionMode, From f75c75a39c78f683ea49181a220b852458ede0d6 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Wed, 22 Oct 2025 13:05:51 +1100 Subject: [PATCH 12/60] update styles for listview and treeview --- packages/@react-spectrum/s2/src/ListView.tsx | 74 +++++++--- packages/@react-spectrum/s2/src/TreeView.tsx | 135 +++++++++--------- .../HighlightSelectionTree.stories.tsx | 1 + 3 files changed, 129 insertions(+), 81 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index 62a14080154..19bd11644c3 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -190,41 +190,80 @@ const listitem = style +
{typeof children === 'string' ? {children} : children} ); diff --git a/packages/@react-spectrum/s2/src/TreeView.tsx b/packages/@react-spectrum/s2/src/TreeView.tsx index c2bc05d4ec5..fa0199e2801 100644 --- a/packages/@react-spectrum/s2/src/TreeView.tsx +++ b/packages/@react-spectrum/s2/src/TreeView.tsx @@ -40,7 +40,6 @@ import {ImageContext} from './Image'; // @ts-ignore import intlMessages from '../intl/*.json'; import {ProgressCircle} from './ProgressCircle'; -import {raw} from '../style/style-macro' with {type: 'macro'}; import React, {createContext, forwardRef, JSXElementConstructor, ReactElement, ReactNode, useContext, useRef} from 'react'; import {Text, TextContext} from './Content'; import {useDOMRef} from '@react-spectrum/utils'; @@ -99,6 +98,7 @@ const tree = style({ height: 'full', overflow: 'auto', boxSizing: 'border-box', + padding: 4, justifyContent: { isEmpty: 'center' }, @@ -180,25 +180,18 @@ const rowBackgroundColor = { } }, highlight: { - default: '--s2-container-bg', - isHovered: 'gray-100', - isPressed: 'gray-100', - isSelected: { - default: 'gray-100', - isEmphasized: 'blue-900/10' - }, - forcedColors: { - default: 'Background' - } + default: 'transparent' } } } as const; const treeRow = style({ + ...focusRing(), + outlineOffset: 2, position: 'relative', display: 'flex', height: 40, - width: 'full', + width: 'calc(100% - 24px)', boxSizing: 'border-box', font: 'ui', color: 'body', @@ -250,9 +243,9 @@ const treeCellGrid = style({ display: 'grid', width: 'full', height: 'full', - boxSizing: 'border-box', alignContent: 'center', alignItems: 'center', + boxSizing: 'border-box', gridTemplateColumns: ['auto', 'auto', 'auto', 'auto', 'auto', '1fr', 'minmax(0, auto)', 'auto'], gridTemplateRows: '1fr', gridTemplateAreas: [ @@ -298,7 +291,6 @@ const treeCellGrid = style({ forcedColors: 'Highlight' } }, - backgroundColor: '--rowBackgroundColor', '--borderColor': { type: 'borderTopColor', value: { @@ -306,56 +298,70 @@ const treeCellGrid = style({ isSelected: 'blue-900', forcedColors: 'ButtonBorder' } + } +}); + +const treeRowBackground = style({ + position: 'absolute', + zIndex: -1, + inset: 0, + backgroundColor: { + default: '--rowBackgroundColor', + isHovered: 'gray-900/5', + isPressed: 'gray-900/10', + isSelected: { + default: 'blue-900/10', + isHovered: 'blue-900/15', + isPressed: 'blue-900/15' + }, + forcedColors: { + default: 'Background' + } }, - borderWidth: 1, - borderStyle: 'solid', borderTopStartRadius: { default: '--borderRadiusTreeItem', - isPreviousSelected: 'none', + isPreviousSelected: { + default: '--borderRadiusTreeItem', + isSelected: 'none' + }, isDetached: 'default' }, borderTopEndRadius: { default: '--borderRadiusTreeItem', - isPreviousSelected: 'none', + isPreviousSelected: { + default: '--borderRadiusTreeItem', + isSelected: 'none' + }, isDetached: 'default' }, borderBottomStartRadius: { default: '--borderRadiusTreeItem', - isNextSelected: 'none', + isNextSelected: { + default: '--borderRadiusTreeItem', + isSelected: 'none' + }, isDetached: 'default' }, borderBottomEndRadius: { default: '--borderRadiusTreeItem', - isNextSelected: 'none', + isNextSelected: { + default: '--borderRadiusTreeItem', + isSelected: 'none' + }, isDetached: 'default' }, - borderTopColor: { - default: 'transparent', - isSelected: '--borderColor', - isPreviousSelected: 'transparent', - isDetached: { - default: 'transparent', - isSelected: '--rowSelectedBorderColor' - } - }, - borderBottomColor: { - default: 'transparent', - isSelected: '--borderColor', - isNextSelected: 'transparent', - isDetached: { - default: 'transparent', - isSelected: '--rowSelectedBorderColor' - } + borderTopWidth: { + default: 1, + isPreviousSelected: 0 }, - borderStartColor: { - default: 'transparent', - isSelected: '--borderColor', - isDetached: { - default: 'transparent', - isSelected: '--rowSelectedBorderColor' - } + borderBottomWidth: { + default: 1, + isNextSelected: 0 }, - borderEndColor: { + borderStartWidth: 1, + borderEndWidth: 1, + borderStyle: 'solid', + borderColor: { default: 'transparent', isSelected: '--borderColor', isDetached: { @@ -425,29 +431,29 @@ const cellFocus = { borderRadius: '[6px]' } as const; -const treeRowFocusIndicator = raw(` - &:before { - content: ""; - display: block; - position: absolute; - inset-inline-start: -4px; - inset-block-start: -4px; - inset-block-end: -4px; - inset-inline-end: -4px; - border-radius: var(--borderRadiusTreeItem); - border-width: 2px; - border-style: solid; - border-color: var(--rowFocusIndicatorColor); - z-index: 3; - pointer-events: none; - }` -); +// const treeRowFocusIndicator = raw(` +// &:before { +// content: ""; +// display: block; +// position: absolute; +// inset-inline-start: -4px; +// inset-block-start: -4px; +// inset-block-end: -4px; +// inset-inline-end: -4px; +// border-radius: var(--borderRadiusTreeItem); +// border-width: 2px; +// border-style: solid; +// border-color: var(--rowFocusIndicatorColor); +// z-index: 3; +// pointer-events: none; +// }` +// ); export const TreeViewItem = (props: TreeViewItemProps): ReactNode => { let { href } = props; - let {isDetached, isEmphasized, selectionStyle, selectionCornerStyle} = useContext(InternalTreeContext); + let {isEmphasized, selectionStyle, selectionCornerStyle} = useContext(InternalTreeContext); return ( { isEmphasized, selectionStyle, isRound: selectionCornerStyle === 'round' - }) + (renderProps.isFocusVisible && !isDetached ? ' ' + treeRowFocusIndicator : '')} /> + })} /> ); }; @@ -493,6 +499,7 @@ export const TreeViewItemContent = (props: TreeViewItemContentProps): ReactNode return (
+
{selectionMode !== 'none' && selectionBehavior === 'toggle' && ( // TODO: add transition?
diff --git a/packages/@react-spectrum/s2/stories/HighlightSelectionTree.stories.tsx b/packages/@react-spectrum/s2/stories/HighlightSelectionTree.stories.tsx index df4d1e7d695..39ee3532a88 100644 --- a/packages/@react-spectrum/s2/stories/HighlightSelectionTree.stories.tsx +++ b/packages/@react-spectrum/s2/stories/HighlightSelectionTree.stories.tsx @@ -266,6 +266,7 @@ export const FileTree: StoryObj = { args: { selectionMode: 'multiple', selectionStyle: 'highlight', + selectionCornerStyle: 'square', isEmphasized: true } }; From 3fb6b67047474ad2fab742034dbed468681db223 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Wed, 22 Oct 2025 14:55:54 +1100 Subject: [PATCH 13/60] tableview --- packages/@react-spectrum/s2/src/TableView.tsx | 108 ++++++++++++------ .../HighlightSelectionTable.stories.tsx | 1 - packages/react-aria-components/src/Table.tsx | 16 +++ 3 files changed, 89 insertions(+), 36 deletions(-) diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index a7ceceb4ef0..9bd91a6f5b8 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -107,8 +107,7 @@ interface S2TableProps { /** Provides the ActionBar to display when rows are selected in the TableView. */ renderActionBar?: (selectedKeys: 'all' | Set) => ReactElement, selectionStyle?: 'highlight' | 'checkbox', - isEmphasized?: boolean, - highlightMode?: 'normal' | 'inverse' + isEmphasized?: boolean } // TODO: Note that loadMore and loadingState are now on the Table instead of on the TableBody @@ -288,7 +287,6 @@ export const TableView = forwardRef(function TableView(props: TableViewProps, re onLoadMore, selectionStyle = 'checkbox', isEmphasized = false, - highlightMode = 'normal', ...otherProps } = props; @@ -315,9 +313,8 @@ export const TableView = forwardRef(function TableView(props: TableViewProps, re isInResizeMode, setIsInResizeMode, selectionStyle, - isEmphasized, - highlightMode - }), [isQuiet, density, overflowMode, loadingState, onLoadMore, isInResizeMode, setIsInResizeMode, selectionStyle, isEmphasized, highlightMode]); + isEmphasized + }), [isQuiet, density, overflowMode, loadingState, onLoadMore, isInResizeMode, setIsInResizeMode, selectionStyle, isEmphasized]); let scrollRef = useRef(null); let isCheckboxSelection = (props.selectionMode === 'multiple' || props.selectionMode === 'single') && selectionStyle === 'checkbox'; @@ -1014,26 +1011,9 @@ const cellContent = style({ default: -4, isSticky: 0 }, - color: { - highlightMode: { - inverse: { - isSelected: 'gray-25' - } - } - }, backgroundColor: { default: 'transparent', isSticky: '--rowBackgroundColor' - }, - fontWeight: { - selectionStyle: { - highlight: { - default: 'normal', - isRowHeader: { - isSelected: 'bold' - } - } - } } }); @@ -1122,15 +1102,8 @@ const rowBackgroundColor = { isHovered: 'gray-100', isPressed: 'gray-100', isSelected: { - default: 'gray-100', - highlightMode: { - normal: { - isEmphasized: 'blue-900/10' - }, - inverse: { - isEmphasized: 'blue-800' - } - } + default: 'blue-900/10', + isHovered: 'blue-900/15' }, forcedColors: { default: 'Background' @@ -1180,8 +1153,17 @@ const row = style({ // } // }, outlineStyle: 'none', - borderTopWidth: 0, - borderBottomWidth: 1, + borderTopWidth: { + default: 0, + isSelected: 0 + }, + borderBottomWidth: { + default: 1, + isSelected: { + default: 0, + isNextSelected: 1 + } + }, borderStartWidth: 0, borderEndWidth: 0, borderStyle: 'solid', @@ -1189,6 +1171,44 @@ const row = style({ default: 'gray-300', forcedColors: 'ButtonBorder' }, + '--rowSelectionIndicatorColor': { + type: 'borderTopColor', + value: { + default: 'gray-300', + isSelected: 'blue-900', + forcedColors: 'ButtonBorder' + } + }, + '--rowSelectionIndicatorBorderTopWidth': { + type: 'borderTopWidth', + value: { + default: 0, + isSelected: 1, + isPreviousSelected: 0 + } + }, + '--rowSelectionIndicatorBorderBottomWidth': { + type: 'borderBottomWidth', + value: { + default: 0, + isSelected: 1, + isNextSelected: 0 + } + }, + '--rowSelectionIndicatorBorderStartWidth': { + type: 'borderStartWidth', + value: { + default: 0, + isSelected: 1 + } + }, + '--rowSelectionIndicatorBorderEndWidth': { + type: 'borderEndWidth', + value: { + default: 0, + isSelected: 1 + } + }, forcedColorAdjust: 'none', color: { selectionStyle: { @@ -1201,6 +1221,22 @@ const row = style({ } }); +let rowSelectionIndicator = raw(` + &:before { + content: ""; + display: inline-block; + position: absolute; + inset: 0; + border-color: var(--rowSelectionIndicatorColor); + border-top-width: var(--rowSelectionIndicatorBorderTopWidth); + border-bottom-width: var(--rowSelectionIndicatorBorderBottomWidth); + border-inline-start-width: var(--rowSelectionIndicatorBorderStartWidth); + border-inline-end-width: var(--rowSelectionIndicatorBorderEndWidth); + border-style: solid; + z-index: 3; + }`); +let rowFocusIndicator = raw('&:after { content: ""; display: inline-block; position: sticky; inset-inline-start: 0; width: 3px; height: 100%; margin-inline-end: -3px; margin-block-end: 1px; z-index: 3; background-color: var(--rowFocusIndicatorColor)'); + export interface RowProps extends Pick, 'id' | 'columns' | 'children' | 'textValue' | 'dependencies' | keyof GlobalDOMAttributes> {} /** @@ -1220,7 +1256,9 @@ export const Row = /*#__PURE__*/ (forwardRef as forwardRefType)(function Row row({ ...renderProps, ...tableVisualOptions - }) + (renderProps.isFocusVisible && ' ' + raw('&:before { content: ""; display: inline-block; position: sticky; inset-inline-start: 0; width: 3px; height: 100%; margin-inline-end: -3px; margin-block-end: 1px; z-index: 3; background-color: var(--rowFocusIndicatorColor)'))} + }) + + (renderProps.isSelected ? (' ' + rowSelectionIndicator) : '') + + (renderProps.isFocusVisible ? (' ' + rowFocusIndicator) : '')} {...otherProps}> {selectionMode !== 'none' && selectionBehavior === 'toggle' && tableVisualOptions.selectionStyle === 'checkbox' && ( diff --git a/packages/@react-spectrum/s2/stories/HighlightSelectionTable.stories.tsx b/packages/@react-spectrum/s2/stories/HighlightSelectionTable.stories.tsx index 09e76f893a2..0f854194b8c 100644 --- a/packages/@react-spectrum/s2/stories/HighlightSelectionTable.stories.tsx +++ b/packages/@react-spectrum/s2/stories/HighlightSelectionTable.stories.tsx @@ -116,7 +116,6 @@ export const DocumentsTable = { overflowMode: 'wrap', selectionStyle: 'highlight', selectionMode: 'multiple', - highlightMode: 'inverse', isEmphasized: true } }; diff --git a/packages/react-aria-components/src/Table.tsx b/packages/react-aria-components/src/Table.tsx index 497346964ef..eb6b9e4fd13 100644 --- a/packages/react-aria-components/src/Table.tsx +++ b/packages/react-aria-components/src/Table.tsx @@ -1011,6 +1011,10 @@ export const TableBody = /*#__PURE__*/ createBranchComponent(TableBodyNode, Date: Thu, 23 Oct 2025 16:01:18 +1100 Subject: [PATCH 14/60] design updates --- packages/@react-spectrum/s2/src/ListView.tsx | 30 +++++++++++--------- packages/@react-spectrum/s2/src/TreeView.tsx | 5 ++-- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index 19bd11644c3..9d27ccd838e 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -136,7 +136,9 @@ const listitem = style({ ...focusRing(), boxSizing: 'border-box', @@ -204,6 +206,18 @@ const listitem = style {(renderProps) => { diff --git a/packages/@react-spectrum/s2/src/TreeView.tsx b/packages/@react-spectrum/s2/src/TreeView.tsx index fa0199e2801..7cfc2fed256 100644 --- a/packages/@react-spectrum/s2/src/TreeView.tsx +++ b/packages/@react-spectrum/s2/src/TreeView.tsx @@ -98,7 +98,6 @@ const tree = style({ height: 'full', overflow: 'auto', boxSizing: 'border-box', - padding: 4, justifyContent: { isEmpty: 'center' }, @@ -187,11 +186,11 @@ const rowBackgroundColor = { const treeRow = style({ ...focusRing(), - outlineOffset: 2, + outlineOffset: -2, position: 'relative', display: 'flex', height: 40, - width: 'calc(100% - 24px)', + width: 'full', boxSizing: 'border-box', font: 'ui', color: 'body', From ccc682ec1ed765402c282dcbf2aefa7b4c84313c Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Fri, 24 Oct 2025 06:54:51 +1100 Subject: [PATCH 15/60] fix colors and add disabled items --- packages/@react-spectrum/s2/src/TreeView.tsx | 24 ++++++++++++++----- .../HighlightSelectionList.stories.tsx | 2 +- .../HighlightSelectionTable.stories.tsx | 2 +- .../HighlightSelectionTree.stories.tsx | 4 ++-- packages/react-aria-components/src/Tree.tsx | 8 +++++-- 5 files changed, 28 insertions(+), 12 deletions(-) diff --git a/packages/@react-spectrum/s2/src/TreeView.tsx b/packages/@react-spectrum/s2/src/TreeView.tsx index 7cfc2fed256..af144ebe6aa 100644 --- a/packages/@react-spectrum/s2/src/TreeView.tsx +++ b/packages/@react-spectrum/s2/src/TreeView.tsx @@ -258,6 +258,14 @@ const treeCellGrid = style({ isDisabled: { default: 'gray-400', forcedColors: 'GrayText' + }, + selectionStyle: { + highlight: { + isSelectionDisabled: { + default: 'gray-400', + forcedColors: 'GrayText' + } + } } }, '--thumbnailBorderColor': { @@ -306,13 +314,16 @@ const treeRowBackground = style({ inset: 0, backgroundColor: { default: '--rowBackgroundColor', - isHovered: 'gray-900/5', - isPressed: 'gray-900/10', + isHovered: 'gray-100', + isPressed: 'gray-100', isSelected: { default: 'blue-900/10', isHovered: 'blue-900/15', isPressed: 'blue-900/15' }, + isDisabled: { + default: 'gray-100' + }, forcedColors: { default: 'Background' } @@ -476,12 +487,13 @@ export const TreeViewItemContent = (props: TreeViewItemContentProps): ReactNode let { children } = props; - let {isDetached, isEmphasized, selectionCornerStyle} = useContext(InternalTreeContext); + let {isDetached, isEmphasized, selectionCornerStyle, selectionStyle} = useContext(InternalTreeContext); let scale = useScale(); return ( - {({isExpanded, hasChildItems, selectionMode, selectionBehavior, isDisabled, isFocusVisible, isSelected, id, state, isHovered}) => { + {({isExpanded, hasChildItems, selectionMode, selectionBehavior, isDisabled, isFocusVisible, isSelected, id, state, isHovered, isSelectionDisabled}) => { + console.log('isSelectionDisabled', isSelectionDisabled); let isNextSelected = false; let isNextFocused = false; let isPreviousSelected = false; @@ -497,8 +509,8 @@ export const TreeViewItemContent = (props: TreeViewItemContentProps): ReactNode let isRound = selectionCornerStyle === 'round'; return ( -
-
+
+
{selectionMode !== 'none' && selectionBehavior === 'toggle' && ( // TODO: add transition?
diff --git a/packages/@react-spectrum/s2/stories/HighlightSelectionList.stories.tsx b/packages/@react-spectrum/s2/stories/HighlightSelectionList.stories.tsx index aa6cae100e3..02a848016f0 100644 --- a/packages/@react-spectrum/s2/stories/HighlightSelectionList.stories.tsx +++ b/packages/@react-spectrum/s2/stories/HighlightSelectionList.stories.tsx @@ -64,7 +64,7 @@ let items: Item[] = [ export const AttributesList: StoryObj = { render: (args) => ( - + {item => ( {item.type === 'number' ? : } diff --git a/packages/@react-spectrum/s2/stories/HighlightSelectionTable.stories.tsx b/packages/@react-spectrum/s2/stories/HighlightSelectionTable.stories.tsx index 0f854194b8c..4476cb3b5d4 100644 --- a/packages/@react-spectrum/s2/stories/HighlightSelectionTable.stories.tsx +++ b/packages/@react-spectrum/s2/stories/HighlightSelectionTable.stories.tsx @@ -76,7 +76,7 @@ let items: Item[] = [ export const DocumentsTable = { render: (args: TableViewProps): ReactElement => ( - + {(column) => ( {column.name} diff --git a/packages/@react-spectrum/s2/stories/HighlightSelectionTree.stories.tsx b/packages/@react-spectrum/s2/stories/HighlightSelectionTree.stories.tsx index 39ee3532a88..d7504847773 100644 --- a/packages/@react-spectrum/s2/stories/HighlightSelectionTree.stories.tsx +++ b/packages/@react-spectrum/s2/stories/HighlightSelectionTree.stories.tsx @@ -133,7 +133,7 @@ const TreeExampleLayersItem = (props: Omit & Tree const TreeExampleLayers = (args: TreeViewProps): ReactElement => (
- + {(item) => ( )} @@ -246,7 +246,7 @@ const TreeExampleFiles = (args: TreeViewProps): ReactEleme }; return (
- + {(item) => ( , /** The unique id of the tree row. */ - id: Key + id: Key, + /** Whether the tree item has its selection disabled. */ + isSelectionDisabled: boolean } export interface TreeItemContentRenderProps extends TreeItemRenderProps {} @@ -557,13 +559,14 @@ export const TreeItem = /*#__PURE__*/ createBranchComponent(TreeItemNode, Date: Fri, 24 Oct 2025 07:04:02 +1100 Subject: [PATCH 16/60] fix lint --- packages/react-aria-components/src/Tree.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-aria-components/src/Tree.tsx b/packages/react-aria-components/src/Tree.tsx index a29f494c891..5d4735b52fd 100644 --- a/packages/react-aria-components/src/Tree.tsx +++ b/packages/react-aria-components/src/Tree.tsx @@ -559,7 +559,7 @@ export const TreeItem = /*#__PURE__*/ createBranchComponent(TreeItemNode, Date: Thu, 19 Feb 2026 10:59:42 -0600 Subject: [PATCH 17/60] initialize docs --- packages/dev/s2-docs/pages/s2/ListView.mdx | 180 +++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 packages/dev/s2-docs/pages/s2/ListView.mdx diff --git a/packages/dev/s2-docs/pages/s2/ListView.mdx b/packages/dev/s2-docs/pages/s2/ListView.mdx new file mode 100644 index 00000000000..9e868f3e2f2 --- /dev/null +++ b/packages/dev/s2-docs/pages/s2/ListView.mdx @@ -0,0 +1,180 @@ +import {Layout} from '../../src/Layout'; +export default Layout; + +import docs from 'docs:@react-spectrum/s2'; + +export const tags = ['list', 'data', 'collection', 'table', 'grid']; + +# ListView + +{docs.exports.ListView.description} + +```tsx render docs={docs.exports.ListView} links={docs.links} props={['selectionMode', 'selectionStyle']} initialProps={{'aria-label': 'Files', selectionMode: 'multiple'}} type="s2" +import {ListView, ListViewItem} from '@react-spectrum/s2'; +import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; + + + Adobe Photoshop + Adobe XD + Adobe InDesign + +``` + +## Content + +`ListView` follows the [Collection Components API](collections.html?component=TreeView), accepting both static and dynamic collections. + +```tsx render type="s2" +"use client"; +import {ListView, ListViewItem} from '@react-spectrum/s2'; +import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; + +///- begin collapse -/// +let files = [ + {id: 'a', name: 'Adobe Photoshop'}, + {id: 'b', name: 'Adobe XD'}, + {id: 'c', name: 'Adobe InDesign'} +]; +///- end collapse -/// + + + {item => ( + {item.name} + )} + +``` + +### Slots + +`ListViewItem` supports icons, `Text`, image content, and action content. + +```tsx render type="s2" +"use client"; +import {ActionButton, ActionButtonGroup, ListView, ListViewItem, Text} from '@react-spectrum/s2'; +import Delete from '@react-spectrum/s2/icons/Delete'; +import Edit from '@react-spectrum/s2/icons/Edit'; +import File from '@react-spectrum/s2/icons/File'; +import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; + +///- begin collapse -/// +let files = [ + {id: 'a', name: 'Project brief.pdf'}, + {id: 'b', name: 'Design directions.fig'}, + {id: 'c', name: 'Final assets.zip'} +]; +///- end collapse -/// + + + {item => ( + + + {item.name} + + + + + + + + + + )} + +``` + +### Selection and Actions + +Use `selectionStyle="checkbox"` (default) for toggle selection with checkboxes, or `selectionStyle="highlight"` for row highlight selection behavior. Use `onAction` to handle row actions. + +```tsx render docs={docs.exports.ListView} links={docs.links} props={['selectionMode', 'selectionStyle']} initialProps={{'aria-label': 'Files', selectionMode: 'multiple', selectionStyle: 'highlight'}} type="s2" +"use client"; +import {ListView, ListViewItem} from '@react-spectrum/s2'; +import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; + + alert(`Action on ${key}`)} + /* PROPS */> + Brand guidelines.pdf + Icon set.svg + Homepage comp.fig + +``` + +### Links + +Use the `href` prop on a `ListViewItem` to create links. + +```tsx render docs={docs.exports.ListView} links={docs.links} props={['selectionMode']} initialProps={{'aria-label': 'Bookmarks', selectionMode: 'multiple'}} type="s2" +import {ListView, ListViewItem} from '@react-spectrum/s2'; +import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; + + + + adobe.com + + + spectrum.adobe.com + + + react-spectrum.adobe.com + + +``` + +### Asynchronous loading + +Use the `loadingState` and `onLoadMore` props to enable async loading and infinite scrolling. + +```tsx render type="s2" +"use client"; +import {ListView, ListViewItem} from '@react-spectrum/s2'; +import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; +import {useAsyncList} from 'react-stately'; + +interface Character { + name: string +} + +function AsyncListView() { + let list = useAsyncList({ + async load({signal, cursor}) { + if (cursor) { + cursor = cursor.replace(/^http:\/\//i, 'https://'); + } + + let res = await fetch(cursor || 'https://swapi.py4e.com/api/people/?search=', {signal}); + let json = await res.json(); + return { + items: json.results, + cursor: json.next + }; + } + }); + + return ( + + {item => ( + {item.name} + )} + + ); +} +``` + +### Empty State + +Use `renderEmptyState` to render placeholder content when there are no items. + + From 68ff3d0356ad9866531070bd70a0abfe31c6db85 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Thu, 19 Feb 2026 11:00:09 -0600 Subject: [PATCH 18/60] fix edgeToText import --- packages/@react-spectrum/s2/src/ListView.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index 9d27ccd838e..5b2956449bf 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -11,7 +11,7 @@ */ import {ActionButtonGroupContext} from './ActionButtonGroup'; -import {baseColor, edgeToText, focusRing, fontRelative, space, style} from '../style' with {type: 'macro'}; +import {baseColor, focusRing, fontRelative, space, style} from '../style' with {type: 'macro'}; import {centerBaseline} from './CenterBaseline'; import { ContextValue, @@ -30,6 +30,7 @@ import { import {controlFont, getAllowedOverrides, StyleProps, StylesPropWithHeight, UnsafeStyles} from './style-utils' with {type: 'macro'}; import {createContext, forwardRef, JSXElementConstructor, ReactElement, ReactNode, useContext, useRef} from 'react'; import {DOMProps, DOMRef, DOMRefValue, forwardRefType, GlobalDOMAttributes} from '@react-types/shared'; +import {edgeToText} from '../style/spectrum-theme' with {type: 'macro'}; import {IconContext} from './Icon'; import {ImageContext} from './Image'; import {pressScale} from './pressScale'; From ee99c7754893a2f43d54e245d83967e7620d46ce Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Thu, 19 Feb 2026 14:26:16 -0600 Subject: [PATCH 19/60] update ListView (loadingMore, actionmenu, empty state, checkbox) --- packages/@react-spectrum/s2/src/ListView.tsx | 215 +++++++++++++++---- packages/@react-spectrum/s2/src/index.ts | 4 +- 2 files changed, 180 insertions(+), 39 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index 5b2956449bf..33284188f84 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -11,15 +11,22 @@ */ import {ActionButtonGroupContext} from './ActionButtonGroup'; +import {ActionMenuContext} from './ActionMenu'; import {baseColor, focusRing, fontRelative, space, style} from '../style' with {type: 'macro'}; import {centerBaseline} from './CenterBaseline'; +import {Checkbox} from './Checkbox'; import { + Collection, + CollectionRendererContext, ContextValue, DEFAULT_SLOT, + DefaultCollectionRenderer, GridList, GridListItem, GridListItemProps, GridListItemRenderProps, + GridListLoadMoreItem, + GridListLoadMoreItemProps, GridListProps, GridListRenderProps, ListLayout, @@ -29,28 +36,42 @@ import { } from 'react-aria-components'; import {controlFont, getAllowedOverrides, StyleProps, StylesPropWithHeight, UnsafeStyles} from './style-utils' with {type: 'macro'}; import {createContext, forwardRef, JSXElementConstructor, ReactElement, ReactNode, useContext, useRef} from 'react'; -import {DOMProps, DOMRef, DOMRefValue, forwardRefType, GlobalDOMAttributes} from '@react-types/shared'; +import {DOMProps, DOMRef, DOMRefValue, forwardRefType, GlobalDOMAttributes, LoadingState} from '@react-types/shared'; import {edgeToText} from '../style/spectrum-theme' with {type: 'macro'}; import {IconContext} from './Icon'; import {ImageContext} from './Image'; +// @ts-ignore +import intlMessages from '../intl/*.json'; import {pressScale} from './pressScale'; +import {ProgressCircle} from './ProgressCircle'; import {Text, TextContext} from './Content'; import {useDOMRef} from '@react-spectrum/utils'; +import {useLocalizedStringFormatter} from 'react-aria'; import {useScale} from './utils'; import {useSpectrumContextProps} from './useSpectrumContextProps'; -export interface ListViewProps extends Omit, 'className' | 'style' | 'children' | keyof GlobalDOMAttributes>, DOMProps, UnsafeStyles, ListViewStylesProps, SlotProps { +export interface ListViewProps extends Omit, 'className' | 'style' | 'children' | 'selectionBehavior' | keyof GlobalDOMAttributes>, DOMProps, UnsafeStyles, ListViewStylesProps, SlotProps { + /** Spectrum-defined styles, returned by the `style()` macro. */ styles?: StylesPropWithHeight, /** - * Whether to automatically focus the Inline Alert when it first renders. + * Whether to automatically focus the ListView when it first renders. */ autoFocus?: boolean, + /** The current loading state of the ListView. */ + loadingState?: LoadingState, + /** Handler that is called when more items should be loaded, e.g. while scrolling near the bottom. */ + onLoadMore?: () => void, + /** The children of the ListView. */ children: ReactNode | ((item: T) => ReactNode) } interface ListViewStylesProps { + /** Whether the ListView should be displayed with a quiet style. */ isQuiet?: boolean, - isEmphasized?: boolean, + /** + * How selection should be displayed. + * @default 'checkbox' + */ selectionStyle?: 'highlight' | 'checkbox', highlightMode?: 'normal' | 'inverse' } @@ -62,6 +83,11 @@ export interface ListViewItemProps extends Omit { + /** The current loading state of the ListView. */ + loadingState?: LoadingState +} + interface ListViewRendererContextValue { renderer?: (item) => ReactElement> } @@ -69,11 +95,14 @@ const ListViewRendererContext = createContext({}); export const ListViewContext = createContext>, DOMRefValue>>(null); -let InternalListViewContext = createContext<{isQuiet?: boolean, isEmphasized?: boolean}>({}); +let InternalListViewContext = createContext<{isQuiet?: boolean, selectionStyle?: 'highlight' | 'checkbox'}>({}); const listView = style({ ...focusRing(), - outlineOffset: -2, // make certain we are visible inside overflow hidden containers + outlineOffset: { + default: -2, + isQuiet: -1 + }, userSelect: 'none', minHeight: 0, minWidth: 0, @@ -82,9 +111,20 @@ const listView = style({ boxSizing: 'border-box', overflow: 'auto', fontSize: controlFont(), - borderRadius: 'default', + backgroundColor: { + default: 'gray-25', + isQuiet: 'transparent', + forcedColors: 'Background' + }, + borderRadius: { + default: 'default', + isQuiet: 'none' + }, borderColor: 'gray-300', - borderWidth: 1, + borderWidth: { + default: 1, + isQuiet: 0 + }, borderStyle: 'solid' }, getAllowedOverrides({height: true})); @@ -93,8 +133,9 @@ export const ListView = /*#__PURE__*/ (forwardRef as forwardRefType)(function Li ref: DOMRef ) { [props, ref] = useSpectrumContextProps(props, ref, ListViewContext); - let {children, isQuiet, isEmphasized} = props; + let {children, isQuiet, selectionStyle = 'checkbox', loadingState, onLoadMore, renderEmptyState: userRenderEmptyState, ...otherProps} = props; let scale = useScale(); + let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2'); let renderer; if (typeof children === 'function') { @@ -103,23 +144,78 @@ export const ListView = /*#__PURE__*/ (forwardRef as forwardRefType)(function Li let domRef = useDOMRef(ref); + let isLoading = loadingState === 'loading' || loadingState === 'loadingMore'; + let renderEmptyState: ListViewProps['renderEmptyState'] | undefined; + if (userRenderEmptyState != null && !isLoading) { + renderEmptyState = (renderProps) => ( +
+ + {userRenderEmptyState!(renderProps)} + +
+ ); + } else if (loadingState === 'loading') { + renderEmptyState = () => ( +
+ +
+ ); + } + + let loadMoreSpinner = onLoadMore ? ( + +
+ +
+
+ ) : null; + + let wrappedChildren: ReactNode; + let gridListProps = otherProps; + if (typeof children === 'function' && otherProps.items) { + let {items, dependencies = [], ...rest} = otherProps; + gridListProps = rest; + wrappedChildren = ( + <> + + {children} + + {loadMoreSpinner} + + ); + } else { + wrappedChildren = ( + <> + {children} + {loadMoreSpinner} + + ); + } + return ( - + (props.UNSAFE_className || '') + listView({ ...renderProps, isQuiet }, props.styles)}> - {children} + {wrappedChildren} @@ -133,7 +229,6 @@ const listitem = style {(renderProps) => { let {children} = props; + let {selectionMode, selectionBehavior, isDisabled} = renderProps; return (
+ {selectionMode !== 'none' && selectionBehavior === 'toggle' && ( +
+ +
+ )} {typeof children === 'string' ? {children} : children} ); @@ -382,3 +508,18 @@ export function ListViewItem(props: ListViewItemProps): ReactNode { ); } + +export const ListViewLoadMoreItem = (props: ListViewLoadMoreItemProps): ReactNode => { + let {loadingState, onLoadMore} = props; + let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2'); + let isLoading = loadingState === 'loading' || loadingState === 'loadingMore'; + return ( + +
+ +
+
+ ); +}; diff --git a/packages/@react-spectrum/s2/src/index.ts b/packages/@react-spectrum/s2/src/index.ts index 3c30d4dcc0b..5451bde9441 100644 --- a/packages/@react-spectrum/s2/src/index.ts +++ b/packages/@react-spectrum/s2/src/index.ts @@ -57,7 +57,7 @@ export {Image, ImageContext} from './Image'; export {ImageCoordinator} from './ImageCoordinator'; export {InlineAlert, InlineAlertContext} from './InlineAlert'; export {Link, LinkContext} from './Link'; -export {ListView, ListViewItem} from './ListView'; +export {ListView, ListViewItem, ListViewLoadMoreItem} from './ListView'; export {MenuItem, MenuTrigger, Menu, MenuSection, SubmenuTrigger, MenuContext} from './Menu'; export {Meter, MeterContext} from './Meter'; export {NotificationBadge, NotificationBadgeContext} from './NotificationBadge'; @@ -137,7 +137,7 @@ export type {InlineAlertProps} from './InlineAlert'; export type {ImageProps} from './Image'; export type {ImageCoordinatorProps} from './ImageCoordinator'; export type {LinkProps} from './Link'; -export type {ListViewProps, ListViewItemProps} from './ListView'; +export type {ListViewProps, ListViewItemProps, ListViewLoadMoreItemProps} from './ListView'; export type {MenuTriggerProps, MenuProps, MenuItemProps, MenuSectionProps, SubmenuTriggerProps} from './Menu'; export type {MeterProps} from './Meter'; export type {NotificationBadgeProps} from './NotificationBadge'; From c6edcb2457934ed5c19811e6802161a4ff3ec122 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Thu, 19 Feb 2026 16:38:03 -0600 Subject: [PATCH 20/60] match selection styles to treeview/tableview --- packages/@react-spectrum/s2/src/ListView.tsx | 97 ++++++++++++++++---- 1 file changed, 79 insertions(+), 18 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index 33284188f84..f0723663a07 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -12,7 +12,7 @@ import {ActionButtonGroupContext} from './ActionButtonGroup'; import {ActionMenuContext} from './ActionMenu'; -import {baseColor, focusRing, fontRelative, space, style} from '../style' with {type: 'macro'}; +import {baseColor, colorMix, focusRing, fontRelative, space, style} from '../style' with {type: 'macro'}; import {centerBaseline} from './CenterBaseline'; import {Checkbox} from './Checkbox'; import { @@ -234,7 +234,8 @@ const listitem = style({ ...focusRing(), boxSizing: 'border-box', @@ -276,14 +277,24 @@ const listitem = style({ position: 'absolute', zIndex: -1, inset: 0, backgroundColor: { default: '--rowBackgroundColor', - isHovered: 'gray-900/5', - isPressed: 'gray-900/10', + isHovered: { + default: 'gray-900/5', + selectionStyle: { + checkbox: selectedBackground + } + }, + isPressed: { + default: 'gray-900/10', + selectionStyle: { + checkbox: selectedActiveBackground + } + }, isSelected: { - default: 'blue-900/10', - isHovered: 'blue-900/15', - isPressed: 'blue-900/15' + selectionStyle: { + checkbox: { + default: selectedBackground, + isHovered: selectedActiveBackground, + isPressed: selectedActiveBackground, + isFocusVisible: selectedActiveBackground + }, + highlight: { + default: 'blue-900/10', + isHovered: 'blue-900/15', + isPressed: 'blue-900/15' + } + } }, forcedColors: { default: 'Background' @@ -340,18 +387,31 @@ const listRowBackground = style({ }, borderTopWidth: { default: 1, - isPrevSelected: 0 + isPrevSelected: { + selectionStyle: { + highlight: 0 + } + } }, borderBottomWidth: { default: 1, - isNextSelected: 0 + isNextSelected: { + selectionStyle: { + highlight: 0 + } + } }, borderStartWidth: 1, borderEndWidth: 1, borderStyle: 'solid', borderColor: { default: 'transparent', - isSelected: '--borderColor' + isSelected: { + selectionStyle: { + highlight: '--borderColor', + checkbox: 'transparent' + } + } } }); @@ -448,7 +508,7 @@ const emptyStateWrapper = style({ export function ListViewItem(props: ListViewItemProps): ReactNode { let ref = useRef(null); let isLink = props.href != null; - let {isQuiet} = useContext(InternalListViewContext); + let {isQuiet, selectionStyle} = useContext(InternalListViewContext); let textValue = props.textValue || (typeof props.children === 'string' ? props.children : undefined); return ( @@ -461,8 +521,9 @@ export function ListViewItem(props: ListViewItemProps): ReactNode { ...renderProps, isLink, isQuiet, + selectionStyle, isPrevNotSelected: !renderProps.isPrevSelected, - isNextNotSelected: !renderProps.isNextSelected, + isNextNotSelected: !renderProps.isNextSelected }, props.styles)}> {(renderProps) => { let {children} = props; @@ -495,7 +556,7 @@ export function ListViewItem(props: ListViewItemProps): ReactNode { isDisabled }] ]}> -
+
{selectionMode !== 'none' && selectionBehavior === 'toggle' && (
From d336314ee8ee06fc962bf308d3abe3dd342f1ce5 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Thu, 19 Feb 2026 17:13:50 -0600 Subject: [PATCH 21/60] docs improvements --- packages/dev/s2-docs/pages/s2/ListView.mdx | 217 +++++++++++++++------ 1 file changed, 154 insertions(+), 63 deletions(-) diff --git a/packages/dev/s2-docs/pages/s2/ListView.mdx b/packages/dev/s2-docs/pages/s2/ListView.mdx index 9e868f3e2f2..f978f3e978b 100644 --- a/packages/dev/s2-docs/pages/s2/ListView.mdx +++ b/packages/dev/s2-docs/pages/s2/ListView.mdx @@ -1,6 +1,7 @@ import {Layout} from '../../src/Layout'; export default Layout; +import {ListView, ListViewItem, ListViewLoadMoreItem, Text, ActionMenu, MenuItem, ActionButton, ActionButtonGroup, Image, IllustratedMessage, Heading, Content, Link} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['list', 'data', 'collection', 'table', 'grid']; @@ -9,23 +10,32 @@ export const tags = ['list', 'data', 'collection', 'table', 'grid']; {docs.exports.ListView.description} -```tsx render docs={docs.exports.ListView} links={docs.links} props={['selectionMode', 'selectionStyle']} initialProps={{'aria-label': 'Files', selectionMode: 'multiple'}} type="s2" -import {ListView, ListViewItem} from '@react-spectrum/s2'; +```tsx render docs={docs.exports.ListView} links={docs.links} props={['selectionMode', 'selectionStyle', 'isQuiet']} initialProps={{'aria-label': 'Files', selectionMode: 'multiple'}} type="s2" +import {ListView, ListViewItem, Text} from '@react-spectrum/s2'; import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; - Adobe Photoshop - Adobe XD - Adobe InDesign + styles={style({width: 'full', maxWidth: 400, height: 320})}> + + Adobe Photoshop + Image editing software + + + Adobe XD + UI/UX design tool + + + Adobe InDesign + Desktop publishing + ``` ## Content -`ListView` follows the [Collection Components API](collections.html?component=TreeView), accepting both static and dynamic collections. +`ListView` follows the [Collection Components API](collections.html?component=ListView), accepting both static and dynamic collections. ```tsx render type="s2" "use client"; @@ -34,13 +44,13 @@ import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; ///- begin collapse -/// let files = [ - {id: 'a', name: 'Adobe Photoshop'}, - {id: 'b', name: 'Adobe XD'}, - {id: 'c', name: 'Adobe InDesign'} + {id: 'adobe-photoshop', name: 'Adobe Photoshop'}, + {id: 'adobe-xd', name: 'Adobe XD'}, + {id: 'adobe-indesign', name: 'Adobe InDesign'} ]; ///- end collapse -/// - + {item => ( {item.name} )} @@ -49,11 +59,13 @@ let files = [ ### Slots -`ListViewItem` supports icons, `Text`, image content, and action content. +`ListViewItem` supports icons, `Text`, [Image](Image) content, [ActionMenu](ActionMenu), and [ActionButtonGroup](ActionButtonGroup) as children. + +Use `textValue` when item content includes non-text children so typeahead and accessibility can still use a clear text label. ```tsx render type="s2" "use client"; -import {ActionButton, ActionButtonGroup, ListView, ListViewItem, Text} from '@react-spectrum/s2'; +import {ActionButton, ActionButtonGroup, ActionMenu, Image, ListView, ListViewItem, MenuItem, Text} from '@react-spectrum/s2'; import Delete from '@react-spectrum/s2/icons/Delete'; import Edit from '@react-spectrum/s2/icons/Edit'; import File from '@react-spectrum/s2/icons/File'; @@ -61,18 +73,22 @@ import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; ///- begin collapse -/// let files = [ - {id: 'a', name: 'Project brief.pdf'}, - {id: 'b', name: 'Design directions.fig'}, - {id: 'c', name: 'Final assets.zip'} + {id: 'project-brief', name: 'Project brief.pdf'}, + {id: 'quarterly-report', name: 'Quarterly report.docx'}, + {id: 'budget', name: 'Budget.xlsx'}, + {id: 'dessert-sunset', name: 'Dessert sunset.jpg', image_url: 'https://images.unsplash.com/photo-1705034598432-1694e203cdf3?q=80&w=600&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D'}, + {id: 'hiking-trail', name: 'Hiking trail.png', image_url: 'https://images.unsplash.com/photo-1722233987129-61dc344db8b6?q=80&w=600&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D'} ]; ///- end collapse -/// - + {item => ( - - + + {/*- begin highlight -*/} + {item.image_url ? {item.name} : } {item.name} - + {item.image_url ? 'Image' : 'Document'} + @@ -80,52 +96,22 @@ let files = [ + + {/*- end highlight -*/} + + + Edit + + + + Delete + + )} ``` -### Selection and Actions - -Use `selectionStyle="checkbox"` (default) for toggle selection with checkboxes, or `selectionStyle="highlight"` for row highlight selection behavior. Use `onAction` to handle row actions. - -```tsx render docs={docs.exports.ListView} links={docs.links} props={['selectionMode', 'selectionStyle']} initialProps={{'aria-label': 'Files', selectionMode: 'multiple', selectionStyle: 'highlight'}} type="s2" -"use client"; -import {ListView, ListViewItem} from '@react-spectrum/s2'; -import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; - - alert(`Action on ${key}`)} - /* PROPS */> - Brand guidelines.pdf - Icon set.svg - Homepage comp.fig - -``` - -### Links - -Use the `href` prop on a `ListViewItem` to create links. - -```tsx render docs={docs.exports.ListView} links={docs.links} props={['selectionMode']} initialProps={{'aria-label': 'Bookmarks', selectionMode: 'multiple'}} type="s2" -import {ListView, ListViewItem} from '@react-spectrum/s2'; -import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; - - - - adobe.com - - - spectrum.adobe.com - - - react-spectrum.adobe.com - - -``` - ### Asynchronous loading Use the `loadingState` and `onLoadMore` props to enable async loading and infinite scrolling. @@ -164,7 +150,7 @@ function AsyncListView() { onLoadMore={list.loadMore} ///- end highlight -/// items={list.items} - styles={style({width: 'full', height: 320})}> + styles={style({width: 'full', maxWidth: 400, height: 320})}> {item => ( {item.name} )} @@ -173,8 +159,113 @@ function AsyncListView() { } ``` -### Empty State +### Links + +Use the `href` prop on a `ListViewItem` to create links. See the [getting started guide](getting-started) to learn how to integrate with your framework. + +```tsx render docs={docs.exports.ListView} links={docs.links} props={['selectionMode', 'isQuiet']} initialProps={{'aria-label': 'Bookmarks', selectionMode: 'none'}} type="s2" +import {ListView, ListViewItem} from '@react-spectrum/s2'; +import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; + + + + adobe.com + + + spectrum.adobe.com + + + react-spectrum.adobe.com + + +``` + +### Empty state Use `renderEmptyState` to render placeholder content when there are no items. +```tsx render type="s2" +"use client"; +import {ListView, IllustratedMessage, Heading, Content, Link} from '@react-spectrum/s2'; +import FolderOpen from '@react-spectrum/s2/illustrations/linear/FolderOpen'; +import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; + + ( + + + No results + Press here for more info. + + )}> + {/*- end highlight -*/} + {[]} + +``` + +## Selection and actions + +Use the `selectionMode` prop to enable single or multiple selection. Use `selectionStyle="checkbox"` (default) for toggle selection with checkboxes, or `selectionStyle="highlight"` for row highlight selection behavior. The `onAction` event handles item actions. Items can be disabled with the `isDisabled` prop. See the [selection guide](selection?component=ListView) for more details. + +```tsx render docs={docs.exports.ListView} links={docs.links} props={['selectionMode', 'selectionStyle', 'isQuiet', 'disabledBehavior', 'disallowEmptySelection']} initialProps={{'aria-label': 'Files', selectionMode: 'multiple'}} type="s2" +"use client"; +import {ListView, ListViewItem, type Selection} from '@react-spectrum/s2'; +import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; +import {useState} from 'react'; + +function Example(props) { + let [selected, setSelected] = useState(new Set()); + + return ( +
+ alert(`Action on ${key}`)} + ///- end highlight -/// + > + Brand guidelines.pdf + Icon set.svg + Homepage comp.fig + Archived.zip + +

Current selection: {selected === 'all' ? 'all' : [...selected].join(', ')}

+
+ ); +} +``` + +## API + +```tsx links={{ListView: '#listview', ListViewItem: '#listviewitem', ListViewLoadMoreItem: '#listviewloadmoreitem', ActionMenu: 'ActionMenu', ActionButtonGroup: 'ActionButtonGroup', Icon: 'icons', Text: 'https://developer.mozilla.org/en-US/docs/Web/HTML/Element/span', Image: 'https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img'}} + + + + + + + or + + + +``` + +### ListView + + + +### ListViewItem + + + +### ListViewLoadMoreItem + From cfbcd66f2cb9810bc2e62ced959c4a1cf3b3a434 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Thu, 19 Feb 2026 17:14:03 -0600 Subject: [PATCH 22/60] add jsdoc --- packages/@react-spectrum/s2/src/ListView.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index f0723663a07..bc86670cf08 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -128,6 +128,9 @@ const listView = style({ borderStyle: 'solid' }, getAllowedOverrides({height: true})); +/** + * A ListView displays a list of interactive items, and allows a user to navigate, select, or perform an action. + */ export const ListView = /*#__PURE__*/ (forwardRef as forwardRefType)(function ListView( props: ListViewProps, ref: DOMRef From bdc5d3572107755e063f955497769f4d8440319f Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Fri, 20 Feb 2026 15:26:05 -0600 Subject: [PATCH 23/60] support hasChildItems chevron --- packages/@react-spectrum/s2/src/ListView.tsx | 37 ++++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index bc86670cf08..47de61bdd7f 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -15,6 +15,7 @@ import {ActionMenuContext} from './ActionMenu'; import {baseColor, colorMix, focusRing, fontRelative, space, style} from '../style' with {type: 'macro'}; import {centerBaseline} from './CenterBaseline'; import {Checkbox} from './Checkbox'; +import Chevron from '../ui-icons/Chevron'; import { Collection, CollectionRendererContext, @@ -46,7 +47,7 @@ import {pressScale} from './pressScale'; import {ProgressCircle} from './ProgressCircle'; import {Text, TextContext} from './Content'; import {useDOMRef} from '@react-spectrum/utils'; -import {useLocalizedStringFormatter} from 'react-aria'; +import {useLocale, useLocalizedStringFormatter} from 'react-aria'; import {useScale} from './utils'; import {useSpectrumContextProps} from './useSpectrumContextProps'; @@ -80,7 +81,9 @@ export interface ListViewItemProps extends Omit { @@ -491,6 +494,15 @@ const listCheckbox = style({ } }); +const listChevron = style({ + gridArea: 'chevron', + gridRowEnd: 'span 2', + alignSelf: 'center', + display: 'flex', + alignItems: 'center', + marginStart: 'text-to-visual' +}); + const centeredWrapper = style({ display: 'flex', alignItems: 'center', @@ -510,13 +522,15 @@ const emptyStateWrapper = style({ export function ListViewItem(props: ListViewItemProps): ReactNode { let ref = useRef(null); + let {hasChildItems, ...otherProps} = props; let isLink = props.href != null; let {isQuiet, selectionStyle} = useContext(InternalListViewContext); let textValue = props.textValue || (typeof props.children === 'string' ? props.children : undefined); + let {direction} = useLocale(); return ( )} {typeof children === 'string' ? {children} : children} + {hasChildItems && ( +
+ +
+ )} ); }} From 2bdc7804ba064f3c4ba1206387608d16222c16c3 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Fri, 20 Feb 2026 15:36:20 -0600 Subject: [PATCH 24/60] add stories --- .../s2/stories/ListView.stories.tsx | 349 +++++++++++++++++- 1 file changed, 330 insertions(+), 19 deletions(-) diff --git a/packages/@react-spectrum/s2/stories/ListView.stories.tsx b/packages/@react-spectrum/s2/stories/ListView.stories.tsx index e30795267ba..91c39f2d70a 100644 --- a/packages/@react-spectrum/s2/stories/ListView.stories.tsx +++ b/packages/@react-spectrum/s2/stories/ListView.stories.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2024 Adobe. All rights reserved. + * Copyright 2026 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 @@ -10,15 +10,20 @@ * governing permissions and limitations under the License. */ -import {ActionButton, ActionButtonGroup, Image, ListView, ListViewItem, Text} from '../'; +import {action} from '@storybook/addon-actions'; +import {ActionButton, ActionButtonGroup, ActionMenu, Breadcrumb, Breadcrumbs, Content, Heading, IllustratedMessage, Image, ListView, ListViewItem, MenuItem, Text} from '../'; import {categorizeArgTypes} from './utils'; +import {chain} from '@react-aria/utils'; import Delete from '../s2wf-icons/S2_Icon_Delete_20_N.svg'; import Edit from '../s2wf-icons/S2_Icon_Edit_20_N.svg'; import File from '../s2wf-icons/S2_Icon_File_20_N.svg'; import Folder from '../s2wf-icons/S2_Icon_Folder_20_N.svg'; +import FolderOpen from '../spectrum-illustrations/linear/FolderOpen'; import {Key} from 'react-aria'; import type {Meta, StoryObj} from '@storybook/react'; -import {ReactNode} from 'react'; +import {ReactNode, useState} from 'react'; +import {style} from '../style' with {type: 'macro'}; +import {useAsyncList} from 'react-stately'; const meta: Meta = { component: ListView, @@ -44,17 +49,17 @@ type Story = StoryObj; export const Example: Story = { args: { - 'aria-label': 'Birthday', + 'aria-label': 'Files', children: ( <> - Item 1 + Item 1 - Item 2 + Item 2 - Item 3 + Item 3 ) @@ -104,13 +109,12 @@ export const Dynamic: Story = {
), args: { - 'aria-label': 'Birthday' + 'aria-label': 'Files' } }; - // taken from https://random.dog/ -const itemsWithThumbs: Array<{id: string, title: string, url: string}> = [ +const itemsWithImages: Array<{id: string, title: string, url: string}> = [ {id: '1', title: 'swimmer', url: 'https://random.dog/b2fe2172-cf11-43f4-9c7f-29bd19601712.jpg'}, {id: '2', title: 'chocolate', url: 'https://random.dog/2032518a-eec8-4102-9d48-3dca5a26eb23.png'}, {id: '3', title: 'good boi', url: 'https://random.dog/191091b2-7d69-47af-9f52-6605063f1a47.jpg'}, @@ -121,11 +125,11 @@ const itemsWithThumbs: Array<{id: string, title: string, url: string}> = [ {id: '8', title: 'audiophile', url: 'https://random.dog/32367-2062-4347.jpg'} ]; -export const DynamicWithThumbs: Story = { +export const WithImages: Story = { render: (args) => ( - + {item => ( - + {item.title} {item.url ? {item.title} : null} @@ -133,12 +137,10 @@ export const DynamicWithThumbs: Story = { ), args: { - 'aria-label': 'Birthday' + 'aria-label': 'Files' } }; - -// taken from https://random.dog/ const itemsWithIcons: Array<{id: string, title: string, icons: ReactNode}> = [ {id: '0', title: 'folder of good bois', icons: }, {id: '1', title: 'swimmer', icons: }, @@ -158,11 +160,11 @@ const itemsWithIcons: Array<{id: string, title: string, icons: ReactNode}> = [ {id: '16', title: 'german sheparpadom', icons: } ]; -export const DynamicWithIcon: Story = { +export const WithIcons: Story = { render: (args) => ( {item => ( - + {item.title} {item.icons ? item.icons : null} @@ -174,6 +176,315 @@ export const DynamicWithIcon: Story = { ), args: { - 'aria-label': 'Birthday' + 'aria-label': 'Files' + } +}; + +export const Selection: Story = { + render: (args) => ( + + {(item) => ( + + {item.type === 'folder' ? : null} + {item.name} + + )} + + ), + args: { + 'aria-label': 'Files', + selectionMode: 'multiple', + selectionStyle: 'checkbox' } }; + +export const HighlightSelection: Story = { + render: (args) => ( + + {(item) => ( + + {item.type === 'folder' ? : null} + {item.name} + + )} + + ), + args: { + 'aria-label': 'Files', + selectionMode: 'multiple', + selectionStyle: 'highlight' + } +}; + +export const DisabledItems: Story = { + render: (args) => ( + + {(item) => ( + + {item.type === 'folder' ? : null} + {item.name} + + )} + + ), + args: { + 'aria-label': 'Files', + selectionMode: 'multiple' + } +}; + +export const Links: Story = { + render: (args) => ( + + Adobe + Google + Apple + New York Times + + ), + args: { + 'aria-label': 'Bookmarks', + selectionMode: 'none' + } +}; + +export const WithActionMenu: Story = { + render: (args) => ( + + + + Utilities + 16 items + + Edit + Delete + + + + Adobe Photoshop + Application + + Edit + Delete + + + + Adobe Illustrator + Application + + Edit + Delete + + + + ), + args: { + 'aria-label': 'Files', + selectionMode: 'single' + } +}; + +export const WithActionButtonGroupAndMenu: Story = { + render: (args) => ( + + + + Utilities + 16 items + + + + + Edit + Delete + + + + Adobe Photoshop + Application + + + + + Edit + Delete + + + + ), + args: { + 'aria-label': 'Files', + selectionMode: 'single' + }, + name: 'With ActionButtonGroup and ActionMenu' +}; + +export const EmptyState: Story = { + render: (args) => ( + ( + + + No results + No results found. + + )}> + {[]} + + ), + args: { + 'aria-label': 'Empty list' + } +}; + +export const Loading: Story = { + render: (args) => ( + + {[]} + + ), + args: { + 'aria-label': 'Loading list', + loadingState: 'loading' + } +}; + +export const LoadingMore: Story = { + render: (args) => ( + + Adobe Photoshop + Adobe Illustrator + Adobe XD + + ), + args: { + 'aria-label': 'Loading more list', + loadingState: 'loadingMore', + onLoadMore: action('onLoadMore') + } +}; + +function AsyncListExample(props) { + interface StarWarsChar { + name: string, + url: string + } + + let list = useAsyncList({ + async load({signal, cursor}) { + if (cursor) { + cursor = cursor.replace(/^http:\/\//i, 'https://'); + } + await new Promise(resolve => setTimeout(resolve, 1500)); + let res = await fetch(cursor || 'https://swapi.py4e.com/api/people/?search=', {signal}); + let json = await res.json(); + return { + items: json.results, + cursor: json.next + }; + } + }); + + return ( + + {(item: StarWarsChar) => ( + {item.name} + )} + + ); +} + +export const AsyncLoading: Story = { + render: (args) => +}; + +export const LongText: Story = { + render: (args) => ( + + + This is a very very very very very very very very long title. + + + Short title + This is a very very very very very very very very long description. + + + This is a very very very very very very very very long title. + This is a very very very very very very very very long description. + + + ), + args: { + 'aria-label': 'Long text examples' + } +}; + +function NavigationExample(props: {disabledType?: 'file' | 'folder', showActions?: boolean}) { + let [selectedKeys, setSelectedKeys] = useState(new Set()); + let [breadcrumbs, setBreadcrumbs] = useState([ + { + id: 'root', + name: 'Root', + type: 'folder', + children: items + } + ]); + + let {name, children = []} = breadcrumbs[breadcrumbs.length - 1]; + + let onAction = (key: Key) => { + let item = children.find(item => item.id === key); + if (item?.type === 'folder') { + setBreadcrumbs([...breadcrumbs, item]); + setSelectedKeys(new Set()); + } + }; + + let onBreadcrumbAction = (key: Key) => { + let index = breadcrumbs.findIndex(item => item.id === key); + setBreadcrumbs(breadcrumbs.slice(0, index + 1)); + setSelectedKeys(new Set()); + }; + + return ( +
+ + {breadcrumbs.map(item => {item.name})} + + item.type === props.disabledType).map(item => item.id) : undefined} + onAction={chain(onAction, action('onAction'))}> + {(item: Item) => ( + + {item.type === 'folder' ? : null} + {item.name} + {props.showActions && ( + + Edit + Delete + + )} + + )} + +
+ ); +} + +export const Navigation: Story = { + render: () => , + name: 'hasChildItems navigation' +}; From e3f372b6804e5b3c2ba7889301ec325f9da00edf Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Fri, 20 Feb 2026 15:38:47 -0600 Subject: [PATCH 25/60] add hasChildItems example to docs --- packages/dev/s2-docs/pages/s2/ListView.mdx | 81 +++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/packages/dev/s2-docs/pages/s2/ListView.mdx b/packages/dev/s2-docs/pages/s2/ListView.mdx index f978f3e978b..b58670f078f 100644 --- a/packages/dev/s2-docs/pages/s2/ListView.mdx +++ b/packages/dev/s2-docs/pages/s2/ListView.mdx @@ -1,7 +1,7 @@ import {Layout} from '../../src/Layout'; export default Layout; -import {ListView, ListViewItem, ListViewLoadMoreItem, Text, ActionMenu, MenuItem, ActionButton, ActionButtonGroup, Image, IllustratedMessage, Heading, Content, Link} from '@react-spectrum/s2'; +import {ListView, ListViewItem, ListViewLoadMoreItem, Text, ActionMenu, MenuItem, ActionButton, ActionButtonGroup, Image, IllustratedMessage, Heading, Content, Link, Breadcrumbs, Breadcrumb} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['list', 'data', 'collection', 'table', 'grid']; @@ -243,6 +243,85 @@ function Example(props) { } ``` +### Navigation + +Use the `hasChildItems` prop on `ListViewItem` to display a chevron indicator, signaling that an item can be navigated into. Combine with `onAction` and [Breadcrumbs](Breadcrumbs) to build a drill-down navigation pattern. + +```tsx render type="s2" +"use client"; +import {Breadcrumbs, Breadcrumb, ListView, ListViewItem, Text} from '@react-spectrum/s2'; +import Folder from '@react-spectrum/s2/icons/Folder'; +import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; +import {useState} from 'react'; + +///- begin collapse -/// +let rootItems = [ + {id: 'photoshop', name: 'Adobe Photoshop', type: 'file'}, + {id: 'xd', name: 'Adobe XD', type: 'file'}, + {id: 'documents', name: 'Documents', type: 'folder', children: [ + {id: 'sales-pitch', name: 'Sales Pitch', type: 'file'}, + {id: 'demo', name: 'Demo', type: 'file'}, + {id: 'taxes', name: 'Taxes', type: 'file'} + ]}, + {id: 'indesign', name: 'Adobe InDesign', type: 'file'}, + {id: 'utilities', name: 'Utilities', type: 'folder', children: [ + {id: 'activity-monitor', name: 'Activity Monitor', type: 'file'} + ]}, + {id: 'aftereffects', name: 'Adobe AfterEffects', type: 'file'}, + {id: 'illustrator', name: 'Adobe Illustrator', type: 'file'}, + {id: 'pictures', name: 'Pictures', type: 'folder', children: [ + {id: 'yosemite', name: 'Yosemite', type: 'file'}, + {id: 'jackson-hole', name: 'Jackson Hole', type: 'file'}, + {id: 'crater-lake', name: 'Crater Lake', type: 'file'} + ]} +]; +///- end collapse -/// + +function NavigationExample() { + let [breadcrumbs, setBreadcrumbs] = useState([ + {id: 'root', name: 'Root', children: rootItems} + ]); + + let currentItems = breadcrumbs[breadcrumbs.length - 1].children; + + let onAction = (key) => { + let item = currentItems.find(item => item.id === key); + if (item?.type === 'folder') { + setBreadcrumbs(prev => [...prev, item]); + } + }; + + let onBreadcrumbAction = (key) => { + let index = breadcrumbs.findIndex(item => item.id === key); + setBreadcrumbs(breadcrumbs.slice(0, index + 1)); + }; + + return ( +
+ {/*- begin highlight -*/} + + {breadcrumbs.map(item => ( + {item.name} + ))} + + + {item => ( + + {item.type === 'folder' ? : null} + {item.name} + + )} + + {/*- end highlight -*/} +
+ ); +} +``` + ## API ```tsx links={{ListView: '#listview', ListViewItem: '#listviewitem', ListViewLoadMoreItem: '#listviewloadmoreitem', ActionMenu: 'ActionMenu', ActionButtonGroup: 'ActionButtonGroup', Icon: 'icons', Text: 'https://developer.mozilla.org/en-US/docs/Web/HTML/Element/span', Image: 'https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img'}} From e788526e8c622ff6537f3dd192f7188498467ba8 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Fri, 20 Feb 2026 15:51:08 -0600 Subject: [PATCH 26/60] fix overflow in empty state --- packages/@react-spectrum/s2/src/ListView.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index 47de61bdd7f..0d9463ef4a1 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -517,6 +517,7 @@ const emptyStateWrapper = style({ justifyContent: 'center', width: 'full', height: 'full', + boxSizing: 'border-box', padding: 16 }); From 7196465dc9f4b7f40b0e725605a2beb7a97f9dc2 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Fri, 20 Feb 2026 15:51:25 -0600 Subject: [PATCH 27/60] use objectFit: 'cover' for images --- packages/@react-spectrum/s2/src/ListView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index 0d9463ef4a1..206b284463e 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -466,7 +466,7 @@ let image = style({ borderRadius: 'sm', width: 32, aspectRatio: 'square', - objectFit: 'contain' + objectFit: 'cover' }); let actionButtonGroup = style({ From 2d8f2f2d6b933b5075464cb4c85b52ff85d119ce Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Fri, 20 Feb 2026 15:51:35 -0600 Subject: [PATCH 28/60] vertically align icons --- packages/@react-spectrum/s2/src/ListView.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index 206b284463e..6fd3196a184 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -443,7 +443,9 @@ export let description = style({ export let iconCenterWrapper = style({ display: 'flex', gridArea: 'icon', - alignSelf: 'center' + gridRowEnd: 'span 2', + alignSelf: 'center', + alignItems: 'center' }); export let icon = style({ From acff11ba26d791a6cd2e56c83a926622550136e3 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Fri, 20 Feb 2026 15:59:35 -0600 Subject: [PATCH 29/60] add overflowMode --- packages/@react-spectrum/s2/src/ListView.tsx | 48 +++++++++++++++----- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index 6fd3196a184..caba04391f7 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -74,6 +74,11 @@ interface ListViewStylesProps { * @default 'checkbox' */ selectionStyle?: 'highlight' | 'checkbox', + /** + * Sets the overflow behavior for item contents. + * @default 'truncate' + */ + overflowMode?: 'wrap' | 'truncate', highlightMode?: 'normal' | 'inverse' } @@ -98,7 +103,7 @@ const ListViewRendererContext = createContext({}); export const ListViewContext = createContext>, DOMRefValue>>(null); -let InternalListViewContext = createContext<{isQuiet?: boolean, selectionStyle?: 'highlight' | 'checkbox'}>({}); +let InternalListViewContext = createContext<{isQuiet?: boolean, selectionStyle?: 'highlight' | 'checkbox', overflowMode?: 'wrap' | 'truncate', scale?: 'medium' | 'large'}>({}); const listView = style({ ...focusRing(), @@ -139,9 +144,10 @@ export const ListView = /*#__PURE__*/ (forwardRef as forwardRefType)(function Li ref: DOMRef ) { [props, ref] = useSpectrumContextProps(props, ref, ListViewContext); - let {children, isQuiet, selectionStyle = 'checkbox', loadingState, onLoadMore, renderEmptyState: userRenderEmptyState, ...otherProps} = props; + let {children, isQuiet, selectionStyle = 'checkbox', overflowMode = 'truncate', loadingState, onLoadMore, renderEmptyState: userRenderEmptyState, ...otherProps} = props; let scale = useScale(); let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2'); + let rowHeight = scale === 'large' ? 50 : 40; let renderer; if (typeof children === 'function') { @@ -206,11 +212,11 @@ export const ListView = /*#__PURE__*/ (forwardRef as forwardRefType)(function Li - + ({ ...focusRing(), boxSizing: 'border-box', @@ -272,7 +279,12 @@ const listitem = style Date: Fri, 20 Feb 2026 16:05:06 -0600 Subject: [PATCH 30/60] audit props --- packages/@react-spectrum/s2/src/ListView.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index caba04391f7..1fd7c12eba8 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -51,13 +51,9 @@ import {useLocale, useLocalizedStringFormatter} from 'react-aria'; import {useScale} from './utils'; import {useSpectrumContextProps} from './useSpectrumContextProps'; -export interface ListViewProps extends Omit, 'className' | 'style' | 'children' | 'selectionBehavior' | keyof GlobalDOMAttributes>, DOMProps, UnsafeStyles, ListViewStylesProps, SlotProps { +export interface ListViewProps extends Omit, 'className' | 'style' | 'children' | 'selectionBehavior' | 'dragAndDropHooks' | 'layout' | 'render' | keyof GlobalDOMAttributes>, DOMProps, UnsafeStyles, ListViewStylesProps, SlotProps { /** Spectrum-defined styles, returned by the `style()` macro. */ styles?: StylesPropWithHeight, - /** - * Whether to automatically focus the ListView when it first renders. - */ - autoFocus?: boolean, /** The current loading state of the ListView. */ loadingState?: LoadingState, /** Handler that is called when more items should be loaded, e.g. while scrolling near the bottom. */ From 478e2a5541103de95b597400538673063a9f7c74 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Fri, 20 Feb 2026 16:30:02 -0600 Subject: [PATCH 31/60] codemods --- .storybook-s2/docs/Migrating.jsx | 7 ++++++ packages/dev/codemods/src/s1-to-s2/UPGRADE.md | 6 +++++ .../__snapshots__/imports.test.ts.snap | 8 +++--- .../__snapshots__/listview.test.ts.snap | 21 ++++++++++++++++ .../multi-collection.test.ts.snap | 9 +------ .../src/s1-to-s2/__tests__/imports.test.ts | 6 ++--- .../src/s1-to-s2/__tests__/listview.test.ts | 25 +++++++++++++++++++ .../__tests__/multi-collection.test.ts | 7 +----- .../src/codemods/components/Item/transform.ts | 2 ++ .../codemods/components/ListView/transform.ts | 13 ++++++++++ .../src/codemods/shared/transforms.ts | 6 ++--- 11 files changed, 86 insertions(+), 24 deletions(-) create mode 100644 packages/dev/codemods/src/s1-to-s2/__tests__/__snapshots__/listview.test.ts.snap create mode 100644 packages/dev/codemods/src/s1-to-s2/__tests__/listview.test.ts create mode 100644 packages/dev/codemods/src/s1-to-s2/src/codemods/components/ListView/transform.ts diff --git a/.storybook-s2/docs/Migrating.jsx b/.storybook-s2/docs/Migrating.jsx index 253e46f6080..76c44aa0fd7 100644 --- a/.storybook-s2/docs/Migrating.jsx +++ b/.storybook-s2/docs/Migrating.jsx @@ -259,6 +259,7 @@ export function Migrating() {
  • If within Picker: Update Item to be a PickerItem
  • If within ComboBox: Update Item to be a ComboBoxItem
  • If within ListBox: Update Item to be a ListBoxItem
  • +
  • If within ListView: Update Item to be a ListViewItem
  • If within TabList: Update Item to be a Tab
  • If within TabPanels: Update Item to be a TabPanel and remove surrounding TabPanels
  • Update key to be id (and keep key if rendered inside array.map)
  • @@ -275,6 +276,12 @@ export function Migrating() {
  • Update Item to be a ListBoxItem
  • +

    ListView

    +
      +
    • [PENDING] Comment out density (it has not been implemented yet)
    • +
    • [PENDING] Comment out dragAndDropHooks (it has not been implemented yet)
    • +
    +

    Menu

    • Update Item to be a MenuItem
    • diff --git a/packages/dev/codemods/src/s1-to-s2/UPGRADE.md b/packages/dev/codemods/src/s1-to-s2/UPGRADE.md index 56156f54447..aad4a67bbce 100644 --- a/packages/dev/codemods/src/s1-to-s2/UPGRADE.md +++ b/packages/dev/codemods/src/s1-to-s2/UPGRADE.md @@ -95,6 +95,10 @@ Note that `[PENDING]` indicates that future changes will occur before the final ## Link - Change `variant=“overBackground”` to `staticColor=“white”` +## ListView +- [PENDING] Comment out `density` (it has not been implemented yet) +- [PENDING] Comment out `dragAndDropHooks` (it has not been implemented yet) + ## MenuTrigger - [PENDING] Comment out `closeOnSelect` (it has not been implemented yet) @@ -217,6 +221,8 @@ Note that `[PENDING]` indicates that future changes will occur before the final - Update `Item` to be a `ComboBoxItem` - If within `ListBox`: - Update `Item` to be a `ListBoxItem` +- If within `ListView`: + - Update `Item` to be a `ListViewItem` - If within `TabList`: - Update `Item` to be a `Tab` - If within `TabPanels`: diff --git a/packages/dev/codemods/src/s1-to-s2/__tests__/__snapshots__/imports.test.ts.snap b/packages/dev/codemods/src/s1-to-s2/__tests__/__snapshots__/imports.test.ts.snap index 44706dcc54b..fa6477d4477 100644 --- a/packages/dev/codemods/src/s1-to-s2/__tests__/__snapshots__/imports.test.ts.snap +++ b/packages/dev/codemods/src/s1-to-s2/__tests__/__snapshots__/imports.test.ts.snap @@ -40,8 +40,8 @@ import * as RSP1 from "@react-spectrum/s2"; `; exports[`should not import Item from S2 1`] = ` -"import { MenuItem, Menu, ListView } from "@react-spectrum/s2"; -import { Item } from '@adobe/react-spectrum'; +"import { MenuItem, Menu } from "@react-spectrum/s2"; +import { ListBox, Item } from '@adobe/react-spectrum';
      @@ -49,11 +49,11 @@ import { Item } from '@adobe/react-spectrum'; Middle Right - + Left Middle Right - +
      " `; diff --git a/packages/dev/codemods/src/s1-to-s2/__tests__/__snapshots__/listview.test.ts.snap b/packages/dev/codemods/src/s1-to-s2/__tests__/__snapshots__/listview.test.ts.snap new file mode 100644 index 00000000000..e69c1c3cfad --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/__tests__/__snapshots__/listview.test.ts.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Leave a comment for density 1`] = ` +"import { ListViewItem, ListView } from "@react-spectrum/s2"; + +// TODO(S2-upgrade): density has not been implemented yet. + + Adobe Photoshop + Adobe Illustrator +" +`; + +exports[`Leave a comment for dragAndDropHooks 1`] = ` +"import { ListViewItem, ListView } from "@react-spectrum/s2"; + +// TODO(S2-upgrade): dragAndDropHooks has not been implemented yet. + + Adobe Photoshop + Adobe Illustrator +" +`; diff --git a/packages/dev/codemods/src/s1-to-s2/__tests__/__snapshots__/multi-collection.test.ts.snap b/packages/dev/codemods/src/s1-to-s2/__tests__/__snapshots__/multi-collection.test.ts.snap index 8efe5edb7e1..5552b2c5eaf 100644 --- a/packages/dev/codemods/src/s1-to-s2/__tests__/__snapshots__/multi-collection.test.ts.snap +++ b/packages/dev/codemods/src/s1-to-s2/__tests__/__snapshots__/multi-collection.test.ts.snap @@ -1,12 +1,10 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Does not affect unimplemented collections 1`] = ` -"import { Item, ActionBarContainer, ActionBar, ListBox } from '@adobe/react-spectrum'; +"import {Item, ActionBarContainer, ActionBar, ListBox} from '@adobe/react-spectrum'; import {SearchAutocomplete} from '@react-spectrum/autocomplete'; import {StepList} from '@react-spectrum/steplist'; -import { ListView } from "@react-spectrum/s2"; -
      One @@ -14,11 +12,6 @@ import { ListView } from "@react-spectrum/s2"; Three - - Adobe Photoshop - Adobe Illustrator - Adobe XD - One Two diff --git a/packages/dev/codemods/src/s1-to-s2/__tests__/imports.test.ts b/packages/dev/codemods/src/s1-to-s2/__tests__/imports.test.ts index 8c6fe0ce5c5..3e0a6406e34 100644 --- a/packages/dev/codemods/src/s1-to-s2/__tests__/imports.test.ts +++ b/packages/dev/codemods/src/s1-to-s2/__tests__/imports.test.ts @@ -65,7 +65,7 @@ const LazyButton = React.lazy(() => import('@react-spectrum/button')) `); test('should not import Item from S2', ` -import {Menu, ListView, Item} from '@adobe/react-spectrum'; +import {Menu, ListBox, Item} from '@adobe/react-spectrum';
      @@ -73,11 +73,11 @@ import {Menu, ListView, Item} from '@adobe/react-spectrum'; Middle Right - + Left Middle Right - +
      `); diff --git a/packages/dev/codemods/src/s1-to-s2/__tests__/listview.test.ts b/packages/dev/codemods/src/s1-to-s2/__tests__/listview.test.ts new file mode 100644 index 00000000000..81deb98457d --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/__tests__/listview.test.ts @@ -0,0 +1,25 @@ +// @ts-ignore +import {defineSnapshotTest} from 'jscodeshift/dist/testUtils'; +import transform from '../src/codemods/codemod'; + +const test = (name: string, input: string) => { + defineSnapshotTest(transform, {}, input, name); +}; + +test('Leave a comment for density', ` +import {Item, ListView} from '@adobe/react-spectrum'; + + + Adobe Photoshop + Adobe Illustrator + +`); + +test('Leave a comment for dragAndDropHooks', ` +import {Item, ListView} from '@adobe/react-spectrum'; + + ({})}> + Adobe Photoshop + Adobe Illustrator + +`); diff --git a/packages/dev/codemods/src/s1-to-s2/__tests__/multi-collection.test.ts b/packages/dev/codemods/src/s1-to-s2/__tests__/multi-collection.test.ts index 1be45f00987..41733c468b3 100644 --- a/packages/dev/codemods/src/s1-to-s2/__tests__/multi-collection.test.ts +++ b/packages/dev/codemods/src/s1-to-s2/__tests__/multi-collection.test.ts @@ -40,7 +40,7 @@ import {Breadcrumbs, Item, Menu, MenuTrigger, SubmenuTrigger, Button, Section, H `); test('Does not affect unimplemented collections', ` -import {Item, ActionBarContainer, ActionBar, ListView, ListBox} from '@adobe/react-spectrum'; +import {Item, ActionBarContainer, ActionBar, ListBox} from '@adobe/react-spectrum'; import {SearchAutocomplete} from '@react-spectrum/autocomplete'; import {StepList} from '@react-spectrum/steplist'; @@ -51,11 +51,6 @@ import {StepList} from '@react-spectrum/steplist'; Three - - Adobe Photoshop - Adobe Illustrator - Adobe XD - One Two diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Item/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Item/transform.ts index 9d8eee7501c..dcd688a4821 100644 --- a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Item/transform.ts +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Item/transform.ts @@ -10,6 +10,7 @@ import * as t from '@babel/types'; * - If within Breadcrumbs: Update Item to be a Breadcrumb. * - If within Picker: Update Item to be a PickerItem. * - If within ComboBox: Update Item to be a ComboBoxItem. + * - If within ListView: Update Item to be a ListViewItem. * - Update key to id (and keep key if rendered inside array.map). */ export default function transformItem(path: NodePath): void { @@ -20,6 +21,7 @@ export default function transformItem(path: NodePath): void { updateComponentWithinCollection(path, {parentComponentName: 'Breadcrumbs', newComponentName: 'Breadcrumb'}); updateComponentWithinCollection(path, {parentComponentName: 'Picker', newComponentName: 'PickerItem'}); updateComponentWithinCollection(path, {parentComponentName: 'ComboBox', newComponentName: 'ComboBoxItem'}); + updateComponentWithinCollection(path, {parentComponentName: 'ListView', newComponentName: 'ListViewItem'}); // Comment if parent collection not detected commentIfParentCollectionNotDetected(path); diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/ListView/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/ListView/transform.ts new file mode 100644 index 00000000000..a7a22085592 --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/ListView/transform.ts @@ -0,0 +1,13 @@ +import {commentOutProp} from '../../shared/transforms'; +import {NodePath} from '@babel/traverse'; +import * as t from '@babel/types'; + +/** + * Transforms ListView: + * - Comment out density (it has not been implemented yet). + * - Comment out dragAndDropHooks (it has not been implemented yet). + */ +export default function transformListView(path: NodePath): void { + commentOutProp(path, {propName: 'density'}); + commentOutProp(path, {propName: 'dragAndDropHooks'}); +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/shared/transforms.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/shared/transforms.ts index eaf1e909e76..bec584ad4ec 100644 --- a/packages/dev/codemods/src/s1-to-s2/src/codemods/shared/transforms.ts +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/shared/transforms.ts @@ -441,9 +441,9 @@ export function updateComponentWithinCollection( ): void { const {parentComponentName, newComponentName} = options; - // Collections currently implemented - // TODO: Add 'ActionGroup', 'ListBox', 'ListView' once implemented - const collectionItemParents = new Set(['Menu', 'ActionMenu', 'TagGroup', 'Breadcrumbs', 'Picker', 'ComboBox', 'ListBox', 'TabList', 'TabPanels', 'Collection']); + // Collections currently implemented. + // TODO: Add 'ActionGroup', 'ListBox', once implemented + const collectionItemParents = new Set(['Menu', 'ActionMenu', 'TagGroup', 'Breadcrumbs', 'Picker', 'ComboBox', 'ListBox', 'ListView', 'TabList', 'TabPanels', 'Collection']); if ( t.isJSXElement(path.node) && From 4f1c4ab3df31c29e01be4282883bc0bedb40b473 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Fri, 20 Feb 2026 17:13:38 -0600 Subject: [PATCH 32/60] add chromatic stories --- .../s2/chromatic/ListView.stories.tsx | 192 ++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 packages/@react-spectrum/s2/chromatic/ListView.stories.tsx diff --git a/packages/@react-spectrum/s2/chromatic/ListView.stories.tsx b/packages/@react-spectrum/s2/chromatic/ListView.stories.tsx new file mode 100644 index 00000000000..31791101f43 --- /dev/null +++ b/packages/@react-spectrum/s2/chromatic/ListView.stories.tsx @@ -0,0 +1,192 @@ +/* + * Copyright 2026 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {ActionButton, ActionButtonGroup, ActionMenu, Content, Heading, IllustratedMessage, ListView, ListViewItem, MenuItem, Text} from '../src'; +import Delete from '../s2wf-icons/S2_Icon_Delete_20_N.svg'; +import Edit from '../s2wf-icons/S2_Icon_Edit_20_N.svg'; +import File from '../s2wf-icons/S2_Icon_File_20_N.svg'; +import Folder from '../s2wf-icons/S2_Icon_Folder_20_N.svg'; +import FolderOpen from '../spectrum-illustrations/linear/FolderOpen'; +import type {Meta, StoryObj} from '@storybook/react'; +import {style} from '../style/spectrum-theme' with {type: 'macro'}; + +const meta: Meta = { + component: ListView, + parameters: { + chromaticProvider: {disableAnimations: true} + }, + title: 'S2 Chromatic/ListView' +}; + +export default meta; +type Story = StoryObj; + +let listViewStyles = style({width: 320, height: 320}); + +const items = [ + {id: 'utilities', name: 'Utilities', type: 'folder', description: '16 items'}, + {id: 'photoshop', name: 'Adobe Photoshop', type: 'file', description: 'Application'}, + {id: 'illustrator', name: 'Adobe Illustrator', type: 'file', description: 'Application'}, + {id: 'xd', name: 'Adobe XD', type: 'file', description: 'Application'} +]; + +export const Example: Story = { + render: (args) => ( + + + + Utilities + 16 items + + Adobe Photoshop + Adobe Illustrator + Adobe XD + + ), + args: { + selectionMode: 'multiple', + onLoadMore: undefined + } +}; + +export const HighlightSelection: Story = { + ...Example, + args: { + ...Example.args, + selectionStyle: 'highlight', + selectedKeys: ['photoshop', 'illustrator'] + } +}; + +export const Quiet: Story = { + ...Example, + args: { + ...Example.args, + isQuiet: true + } +}; + +export const OverflowTruncate: Story = { + render: (args) => ( + + + This is a very very very very very very very very long title. + + + Short title + This is a very very very very very very very very long description. + + + This is a very very very very very very very very long title. + This is a very very very very very very very very long description. + + + ), + args: { + ...Example.args, + overflowMode: 'truncate' + } +}; + +export const OverflowWrap: Story = { + render: (args) => ( + + + This is a very very very very very very very very long title. + + + Short title + This is a very very very very very very very very long description. + + + This is a very very very very very very very very long title. + This is a very very very very very very very very long description. + + + ), + args: { + ...Example.args, + overflowMode: 'wrap' + } +}; + +export const DisabledItems: Story = { + render: (args) => ( + + {(item) => ( + + {item.type === 'folder' ? : } + {item.name} + {item.description} + + )} + + ) +}; + +export const DisabledBehaviorSelection: Story = { + ...DisabledItems, + args: { + disabledBehavior: 'selection' + } +}; + +export const WithActions: Story = { + render: (args) => ( + + + + Utilities + 16 items + + + + + Edit + Delete + + + + Adobe Photoshop + Application + + + + + Edit + Delete + + + + ), + args: { + selectionMode: 'single' + } +}; + +export const EmptyState: Story = { + render: (args) => ( + ( + + + No results + No results found. + + )}> + {[]} + + ) +}; From a0458ca2bfbcab0b5a8742353fa14a389a2d1692 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Fri, 20 Feb 2026 17:18:26 -0600 Subject: [PATCH 33/60] lint --- packages/@react-spectrum/s2/src/Picker.tsx | 1 - packages/@react-spectrum/s2/src/TreeView.tsx | 11 ----------- .../s2/stories/HighlightSelectionList.stories.tsx | 3 +-- 3 files changed, 1 insertion(+), 14 deletions(-) diff --git a/packages/@react-spectrum/s2/src/Picker.tsx b/packages/@react-spectrum/s2/src/Picker.tsx index 4724c4f8156..afc38783c6f 100644 --- a/packages/@react-spectrum/s2/src/Picker.tsx +++ b/packages/@react-spectrum/s2/src/Picker.tsx @@ -57,7 +57,6 @@ import { listboxItem, LOADER_ROW_HEIGHTS } from './ComboBox'; -import {edgeToText} from '../style/spectrum-theme' with {type: 'macro'}; import { FieldErrorIcon, FieldLabel, diff --git a/packages/@react-spectrum/s2/src/TreeView.tsx b/packages/@react-spectrum/s2/src/TreeView.tsx index e6c01d0bcbc..d86e298de57 100644 --- a/packages/@react-spectrum/s2/src/TreeView.tsx +++ b/packages/@react-spectrum/s2/src/TreeView.tsx @@ -435,17 +435,6 @@ const treeActionMenu = style({ gridArea: 'actionmenu' }); -const cellFocus = { - outlineStyle: { - default: 'none', - isFocusVisible: 'solid' - }, - outlineOffset: -2, - outlineWidth: 2, - outlineColor: 'focus-ring', - borderRadius: '[6px]' -} as const; - const treeRowFocusIndicator = raw(` &:before { content: ""; diff --git a/packages/@react-spectrum/s2/stories/HighlightSelectionList.stories.tsx b/packages/@react-spectrum/s2/stories/HighlightSelectionList.stories.tsx index 02a848016f0..ac78c005c5c 100644 --- a/packages/@react-spectrum/s2/stories/HighlightSelectionList.stories.tsx +++ b/packages/@react-spectrum/s2/stories/HighlightSelectionList.stories.tsx @@ -84,7 +84,6 @@ export const AttributesList: StoryObj = { args: { selectionStyle: 'highlight', selectionMode: 'multiple', - highlightMode: 'inverse', - isEmphasized: true + highlightMode: 'inverse' } }; From 2d49050feb4491528dc16f9df4721867485f71a5 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Mon, 23 Feb 2026 09:38:38 -0600 Subject: [PATCH 34/60] add ListView testing docs --- packages/dev/s2-docs/pages/s2/ListView.mdx | 3 + .../dev/s2-docs/pages/s2/ListView/testing.mdx | 81 +++++++++++++++++++ packages/dev/s2-docs/pages/s2/testing.mdx | 1 + 3 files changed, 85 insertions(+) create mode 100644 packages/dev/s2-docs/pages/s2/ListView/testing.mdx diff --git a/packages/dev/s2-docs/pages/s2/ListView.mdx b/packages/dev/s2-docs/pages/s2/ListView.mdx index b58670f078f..39524264422 100644 --- a/packages/dev/s2-docs/pages/s2/ListView.mdx +++ b/packages/dev/s2-docs/pages/s2/ListView.mdx @@ -5,6 +5,9 @@ import {ListView, ListViewItem, ListViewLoadMoreItem, Text, ActionMenu, MenuItem import docs from 'docs:@react-spectrum/s2'; export const tags = ['list', 'data', 'collection', 'table', 'grid']; +export const relatedPages = [ + {title: 'Testing ListView', url: './ListView/testing'} +]; # ListView diff --git a/packages/dev/s2-docs/pages/s2/ListView/testing.mdx b/packages/dev/s2-docs/pages/s2/ListView/testing.mdx new file mode 100644 index 00000000000..74486c588d0 --- /dev/null +++ b/packages/dev/s2-docs/pages/s2/ListView/testing.mdx @@ -0,0 +1,81 @@ +import {Layout} from '../../../src/Layout'; +export default Layout; + +import {InlineAlert, Heading, Content} from '@react-spectrum/s2'; +import testUtilDocs from 'docs:@react-spectrum/test-utils'; +import {InstallCommand} from '../../../src/InstallCommand'; +import {PatternTestingFAQ} from '../../../src/PatternTestingFAQ'; + +export const isSubpage = true; +export const hideFromSearch = true; +export const tags = ['testing', 'listview', 'test-utils']; +export const description = 'Testing ListView with React Spectrum test utils'; + +# Testing ListView + +## General setup + +ListView supports long press interactions on its rows in certain configurations. See the following sections on how to handle these behaviors in your tests. + +* [Timers](../testing#timers) +* [Long press](../testing#simulating-long-press) + +## Test utils + +`@react-spectrum/test-utils` offers common list interaction testing utilities. Install it with your preferred package manager. + + + + + Requirements + Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work. + + +Initialize a `User` object at the top of your test file, and use it to create a `GridList` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions. + +```ts +// ListView.test.ts +import {render, within} from '@testing-library/react'; +import {User} from '@react-spectrum/test-utils'; + +let testUtilUser = new User({ + interactionType: 'mouse' +}); +// ... + +it('ListView can toggle row selection', async function () { + // Render your test component/app and initialize the list tester + let {getByTestId} = render( + + ... + + ); + let gridListTester = testUtilUser.createTester('GridList', {root: getByTestId('test-listview'), interactionType: 'keyboard'}); + + let row = gridListTester.rows[0]; + expect(within(row).getByRole('checkbox')).not.toBeChecked(); + expect(gridListTester.selectedRows).toHaveLength(0); + + await gridListTester.toggleRowSelection({row: 0}); + expect(within(row).getByRole('checkbox')).toBeChecked(); + expect(gridListTester.selectedRows).toHaveLength(1); + + await gridListTester.toggleRowSelection({row: 0}); + expect(within(row).getByRole('checkbox')).not.toBeChecked(); + expect(gridListTester.selectedRows).toHaveLength(0); +}); +``` + +## API + +### User + + + +### GridListTester + + + +## Testing FAQ + + diff --git a/packages/dev/s2-docs/pages/s2/testing.mdx b/packages/dev/s2-docs/pages/s2/testing.mdx index 5d7c06e9f31..5ceaaf81d32 100644 --- a/packages/dev/s2-docs/pages/s2/testing.mdx +++ b/packages/dev/s2-docs/pages/s2/testing.mdx @@ -211,6 +211,7 @@ Below is a list of the ARIA patterns testers currently supported by createTester - [CheckboxGroup](./CheckboxGroup/testing) - [ComboBox](./ComboBox/testing) - [Dialog](./Dialog/testing) +- [ListView](./ListView/testing) - [Menu](./Menu/testing) - [Picker](./Picker/testing) - [RadioGroup](./RadioGroup/testing) From eb7f7f4ffcf0dc3a7ebe0172cd32342e02008059 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Mon, 23 Feb 2026 09:42:17 -0600 Subject: [PATCH 35/60] fix types --- packages/@react-spectrum/s2/src/TreeView.tsx | 2 ++ packages/@react-spectrum/s2/stories/ListView.stories.tsx | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/@react-spectrum/s2/src/TreeView.tsx b/packages/@react-spectrum/s2/src/TreeView.tsx index d86e298de57..75c07e5b863 100644 --- a/packages/@react-spectrum/s2/src/TreeView.tsx +++ b/packages/@react-spectrum/s2/src/TreeView.tsx @@ -51,6 +51,8 @@ import {useSpectrumContextProps} from './useSpectrumContextProps'; interface S2TreeProps { /** Handler that is called when a user performs an action on a row. */ onAction?: (key: Key) => void, + /** Whether to remove the outer border/background from the container. */ + isDetached?: boolean, /** Whether the tree should be displayed with a [emphasized style](https://spectrum.adobe.com/page/tree-view/#Emphasis). */ isEmphasized?: boolean, selectionStyle?: 'highlight' | 'checkbox', diff --git a/packages/@react-spectrum/s2/stories/ListView.stories.tsx b/packages/@react-spectrum/s2/stories/ListView.stories.tsx index 91c39f2d70a..c3d01926c54 100644 --- a/packages/@react-spectrum/s2/stories/ListView.stories.tsx +++ b/packages/@react-spectrum/s2/stories/ListView.stories.tsx @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {action} from '@storybook/addon-actions'; +import {action} from 'storybook/actions'; import {ActionButton, ActionButtonGroup, ActionMenu, Breadcrumb, Breadcrumbs, Content, Heading, IllustratedMessage, Image, ListView, ListViewItem, MenuItem, Text} from '../'; import {categorizeArgTypes} from './utils'; import {chain} from '@react-aria/utils'; From 0fedad93935e947c5ab812fa6c2e169db94cf9b2 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Mon, 23 Feb 2026 10:11:26 -0600 Subject: [PATCH 36/60] docs types --- packages/dev/s2-docs/pages/s2/ListView.mdx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/dev/s2-docs/pages/s2/ListView.mdx b/packages/dev/s2-docs/pages/s2/ListView.mdx index 39524264422..762d9342744 100644 --- a/packages/dev/s2-docs/pages/s2/ListView.mdx +++ b/packages/dev/s2-docs/pages/s2/ListView.mdx @@ -258,7 +258,12 @@ import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; import {useState} from 'react'; ///- begin collapse -/// -let rootItems = [ +type FileItem = {id: string, name: string, type: 'file'}; +type FolderItem = {id: string, name: string, type: 'folder', children: ListItem[]}; +type ListItem = FileItem | FolderItem; +type BreadcrumbItem = {id: string, name: string, children: ListItem[]}; + +let rootItems: ListItem[] = [ {id: 'photoshop', name: 'Adobe Photoshop', type: 'file'}, {id: 'xd', name: 'Adobe XD', type: 'file'}, {id: 'documents', name: 'Documents', type: 'folder', children: [ @@ -281,20 +286,20 @@ let rootItems = [ ///- end collapse -/// function NavigationExample() { - let [breadcrumbs, setBreadcrumbs] = useState([ + let [breadcrumbs, setBreadcrumbs] = useState([ {id: 'root', name: 'Root', children: rootItems} ]); let currentItems = breadcrumbs[breadcrumbs.length - 1].children; - let onAction = (key) => { + let onAction = (key: string) => { let item = currentItems.find(item => item.id === key); if (item?.type === 'folder') { setBreadcrumbs(prev => [...prev, item]); } }; - let onBreadcrumbAction = (key) => { + let onBreadcrumbAction = (key: string) => { let index = breadcrumbs.findIndex(item => item.id === key); setBreadcrumbs(breadcrumbs.slice(0, index + 1)); }; From 53c15519f4448437b3e2d362b78a6d34491f10a8 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Mon, 23 Feb 2026 10:12:58 -0600 Subject: [PATCH 37/60] add description for search menu --- packages/dev/s2-docs/pages/s2/ListView.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/dev/s2-docs/pages/s2/ListView.mdx b/packages/dev/s2-docs/pages/s2/ListView.mdx index 762d9342744..bfe80fd01de 100644 --- a/packages/dev/s2-docs/pages/s2/ListView.mdx +++ b/packages/dev/s2-docs/pages/s2/ListView.mdx @@ -8,6 +8,7 @@ export const tags = ['list', 'data', 'collection', 'table', 'grid']; export const relatedPages = [ {title: 'Testing ListView', url: './ListView/testing'} ]; +export const description = 'Displays a list of items with selection and actions.'; # ListView From 4ce134bc5e73a453e21662c903d55f5f9fd4cdb6 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Mon, 23 Feb 2026 10:24:39 -0600 Subject: [PATCH 38/60] docs types --- packages/dev/s2-docs/pages/s2/ListView.mdx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/dev/s2-docs/pages/s2/ListView.mdx b/packages/dev/s2-docs/pages/s2/ListView.mdx index bfe80fd01de..15ba6a22b19 100644 --- a/packages/dev/s2-docs/pages/s2/ListView.mdx +++ b/packages/dev/s2-docs/pages/s2/ListView.mdx @@ -256,6 +256,7 @@ Use the `hasChildItems` prop on `ListViewItem` to display a chevron indicator, s import {Breadcrumbs, Breadcrumb, ListView, ListViewItem, Text} from '@react-spectrum/s2'; import Folder from '@react-spectrum/s2/icons/Folder'; import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; +import {Key} from 'react-aria'; import {useState} from 'react'; ///- begin collapse -/// @@ -293,14 +294,14 @@ function NavigationExample() { let currentItems = breadcrumbs[breadcrumbs.length - 1].children; - let onAction = (key: string) => { + let onAction = (key: Key) => { let item = currentItems.find(item => item.id === key); if (item?.type === 'folder') { setBreadcrumbs(prev => [...prev, item]); } }; - let onBreadcrumbAction = (key: string) => { + let onBreadcrumbAction = (key: Key) => { let index = breadcrumbs.findIndex(item => item.id === key); setBreadcrumbs(breadcrumbs.slice(0, index + 1)); }; From 5e484ccb81913e1a84b3393627e11fd7ce2dc771 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Mon, 23 Feb 2026 12:27:28 -0600 Subject: [PATCH 39/60] fix selection checkbox visibility (always hide if disabled) --- packages/@react-spectrum/s2/src/ListView.tsx | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index 1fd7c12eba8..929270cba27 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -15,8 +15,8 @@ import {ActionMenuContext} from './ActionMenu'; import {baseColor, colorMix, focusRing, fontRelative, space, style} from '../style' with {type: 'macro'}; import {centerBaseline} from './CenterBaseline'; import {Checkbox} from './Checkbox'; -import Chevron from '../ui-icons/Chevron'; import { + CheckboxContext, Collection, CollectionRendererContext, ContextValue, @@ -33,8 +33,10 @@ import { ListLayout, Provider, SlotProps, + useSlottedContext, Virtualizer } from 'react-aria-components'; +import Chevron from '../ui-icons/Chevron'; import {controlFont, getAllowedOverrides, StyleProps, StylesPropWithHeight, UnsafeStyles} from './style-utils' with {type: 'macro'}; import {createContext, forwardRef, JSXElementConstructor, ReactElement, ReactNode, useContext, useRef} from 'react'; import {DOMProps, DOMRef, DOMRefValue, forwardRefType, GlobalDOMAttributes, LoadingState} from '@react-types/shared'; @@ -544,6 +546,16 @@ const emptyStateWrapper = style({ padding: 16 }); +function ListSelectionCheckbox({isDisabled}: {isDisabled: boolean}) { + let selectionContext = useSlottedContext(CheckboxContext, 'selection'); + let isSelectionDisabled = isDisabled || !!selectionContext?.isDisabled; + return ( +
      + +
      + ); +} + export function ListViewItem(props: ListViewItemProps): ReactNode { let ref = useRef(null); let {hasChildItems, ...otherProps} = props; @@ -600,9 +612,7 @@ export function ListViewItem(props: ListViewItemProps): ReactNode { ]}>
      {selectionMode !== 'none' && selectionBehavior === 'toggle' && ( -
      - -
      + )} {typeof children === 'string' ? {children} : children} {hasChildItems && ( From 9fa3ff0924fa9346345083d30f5cf90c57d12760 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Mon, 23 Feb 2026 12:28:23 -0600 Subject: [PATCH 40/60] remove highlightMode prop --- packages/@react-spectrum/s2/src/ListView.tsx | 3 +-- .../s2/stories/HighlightSelectionList.stories.tsx | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index 929270cba27..3a7eb84b225 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -76,8 +76,7 @@ interface ListViewStylesProps { * Sets the overflow behavior for item contents. * @default 'truncate' */ - overflowMode?: 'wrap' | 'truncate', - highlightMode?: 'normal' | 'inverse' + overflowMode?: 'wrap' | 'truncate' } export interface ListViewItemProps extends Omit, StyleProps { diff --git a/packages/@react-spectrum/s2/stories/HighlightSelectionList.stories.tsx b/packages/@react-spectrum/s2/stories/HighlightSelectionList.stories.tsx index ac78c005c5c..d447c6d982e 100644 --- a/packages/@react-spectrum/s2/stories/HighlightSelectionList.stories.tsx +++ b/packages/@react-spectrum/s2/stories/HighlightSelectionList.stories.tsx @@ -83,7 +83,6 @@ export const AttributesList: StoryObj = { ), args: { selectionStyle: 'highlight', - selectionMode: 'multiple', - highlightMode: 'inverse' + selectionMode: 'multiple' } }; From 9ba74c489cbb7dd98ecdeda2260201404fbc8c99 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Mon, 23 Feb 2026 13:12:41 -0600 Subject: [PATCH 41/60] remove Tree/Table selection changes --- packages/@react-spectrum/s2/src/TableView.tsx | 173 ++--------- packages/@react-spectrum/s2/src/TreeView.tsx | 262 ++--------------- packages/@react-spectrum/s2/src/index.ts | 2 +- .../HighlightSelectionList.stories.tsx | 88 ------ .../HighlightSelectionTable.stories.tsx | 121 -------- .../HighlightSelectionTree.stories.tsx | 272 ------------------ packages/react-aria-components/src/Table.tsx | 16 -- packages/react-aria-components/src/Tree.tsx | 8 +- 8 files changed, 61 insertions(+), 881 deletions(-) delete mode 100644 packages/@react-spectrum/s2/stories/HighlightSelectionList.stories.tsx delete mode 100644 packages/@react-spectrum/s2/stories/HighlightSelectionTable.stories.tsx delete mode 100644 packages/@react-spectrum/s2/stories/HighlightSelectionTree.stories.tsx diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index 560daae9d35..85a9d79c19c 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -16,7 +16,6 @@ import { Button, ButtonContext, CellRenderProps, - CheckboxContext, Collection, CollectionRendererContext, ColumnRenderProps, @@ -119,9 +118,7 @@ interface S2TableProps { /** Handler that is called when more items should be loaded, e.g. while scrolling near the bottom. */ onLoadMore?: () => any, /** Provides the ActionBar to display when rows are selected in the TableView. */ - renderActionBar?: (selectedKeys: 'all' | Set) => ReactElement, - selectionStyle?: 'highlight' | 'checkbox', - isEmphasized?: boolean + renderActionBar?: (selectedKeys: 'all' | Set) => ReactElement } // TODO: Note that loadMore and loadingState are now on the Table instead of on the TableBody @@ -299,9 +296,7 @@ export const TableView = forwardRef(function TableView(props: TableViewProps, re onResizeEnd: propsOnResizeEnd, onAction, onLoadMore, - selectionStyle = 'checkbox', - isEmphasized = false, - selectionMode, + selectionMode = 'none', ...otherProps } = props; @@ -327,13 +322,11 @@ export const TableView = forwardRef(function TableView(props: TableViewProps, re onLoadMore, isInResizeMode, setIsInResizeMode, - selectionStyle, - isEmphasized, selectionMode - }), [isQuiet, density, overflowMode, loadingState, onLoadMore, isInResizeMode, setIsInResizeMode, selectionStyle, isEmphasized, selectionMode]); + }), [isQuiet, density, overflowMode, loadingState, onLoadMore, isInResizeMode, setIsInResizeMode, selectionMode]); let scrollRef = useRef(null); - let isCheckboxSelection = (selectionMode === 'multiple' || selectionMode === 'single') && selectionStyle === 'checkbox'; + let isCheckboxSelection = selectionMode === 'multiple' || selectionMode === 'single'; let {selectedKeys, onSelectionChange, actionBar, actionBarHeight} = useActionBarContainer({...props, scrollRef}); @@ -375,10 +368,10 @@ export const TableView = forwardRef(function TableView(props: TableViewProps, re isCheckboxSelection, isQuiet })} + selectionBehavior="toggle" selectionMode={selectionMode} onRowAction={onAction} {...otherProps} - selectionBehavior={selectionStyle === 'highlight' ? 'replace' : 'toggle'} selectedKeys={selectedKeys} defaultSelectedKeys={undefined} onSelectionChange={onSelectionChange} /> @@ -904,7 +897,7 @@ export interface TableHeaderProps extends Omit, 'style export const TableHeader = /*#__PURE__*/ (forwardRef as forwardRefType)(function TableHeader({columns, dependencies, children}: TableHeaderProps, ref: DOMRef) { let scale = useScale(); let {selectionBehavior, selectionMode} = useTableOptions(); - let {isQuiet, selectionStyle} = useContext(InternalTableContext); + let {isQuiet} = useContext(InternalTableContext); let domRef = useDOMRef(ref); return ( @@ -913,7 +906,7 @@ export const TableHeader = /*#__PURE__*/ (forwardRef as forwardRefType)(function ref={domRef} className={tableHeader}> {/* Add extra columns for selection. */} - {selectionBehavior === 'toggle' && selectionStyle === 'checkbox' && ( + {selectionBehavior === 'toggle' && ( // Also isSticky prop is applied just for the layout, will decide what the RAC api should be later // @ts-ignore @@ -1013,10 +1006,6 @@ const checkboxCellStyle = style({ const cellContent = style({ truncate: true, - '--iconPrimary': { - type: 'fill', - value: 'currentColor' - }, whiteSpace: { default: 'nowrap', overflowMode: { @@ -1076,33 +1065,15 @@ export const Cell = forwardRef(function Cell(props: CellProps, ref: DOMRef {({isFocusVisible}) => ( - // @ts-ignore - + <> + {children} + {isFocusVisible && } + )} ); }); -let InnerCell = function InnerCell(props: {isFocusVisible: boolean, children: ReactNode, isSticky?: boolean, align?: 'start' | 'center' | 'end', isRowHeader?: boolean}) { - let {isFocusVisible, children, isSticky, align, isRowHeader} = props; - let tableVisualOptions = useContext(InternalTableContext); - let {isSelected} = useSlottedContext(CheckboxContext, 'selection') ?? {isSelected: false}; - - return ( - <> - {children} - {isFocusVisible && } - - ); -}; - const editableCell = style({ ...commonCellStyles, @@ -1413,33 +1384,17 @@ const rowBackgroundColor = { default: 'gray-25', isQuiet: '--s2-container-bg' }, - selectionStyle: { - checkbox: { - isFocusVisibleWithin: colorMix('gray-25', 'gray-900', 7), // table-row-hover-color - isHovered: colorMix('gray-25', 'gray-900', 7), // table-row-hover-color - isPressed: colorMix('gray-25', 'gray-900', 10), // table-row-hover-color - isSelected: { - default: selectedBackground, // table-selected-row-background-color, opacity /10 - isFocusVisibleWithin: selectedActiveBackground, // table-selected-row-background-color, opacity /15 - isHovered: selectedActiveBackground, // table-selected-row-background-color, opacity /15 - isPressed: selectedActiveBackground // table-selected-row-background-color, opacity /15 - }, - forcedColors: { - default: 'Background' - } - }, - highlight: { - isFocusVisibleWithin: 'gray-100', - isHovered: 'gray-100', - isPressed: 'gray-100', - isSelected: { - default: 'blue-900/10', - isHovered: 'blue-900/15' - }, - forcedColors: { - default: 'Background' - } - } + isFocusVisibleWithin: colorMix('gray-25', 'gray-900', 7), // table-row-hover-color + isHovered: colorMix('gray-25', 'gray-900', 7), // table-row-hover-color + isPressed: colorMix('gray-25', 'gray-900', 10), // table-row-hover-color + isSelected: { + default: selectedBackground, // table-selected-row-background-color, opacity /10 + isFocusVisibleWithin: selectedActiveBackground, // table-selected-row-background-color, opacity /15 + isHovered: selectedActiveBackground, // table-selected-row-background-color, opacity /15 + isPressed: selectedActiveBackground // table-selected-row-background-color, opacity /15 + }, + forcedColors: { + default: 'Background' } } as const; @@ -1484,17 +1439,8 @@ const row = style({ // } // }, outlineStyle: 'none', - borderTopWidth: { - default: 0, - isSelected: 0 - }, - borderBottomWidth: { - default: 1, - isSelected: { - default: 0, - isNextSelected: 1 - } - }, + borderTopWidth: 0, + borderBottomWidth: 1, borderStartWidth: 0, borderEndWidth: 0, borderStyle: 'solid', @@ -1502,72 +1448,9 @@ const row = style({ default: 'gray-300', forcedColors: 'ButtonBorder' }, - '--rowSelectionIndicatorColor': { - type: 'borderTopColor', - value: { - default: 'gray-300', - isSelected: 'blue-900', - forcedColors: 'ButtonBorder' - } - }, - '--rowSelectionIndicatorBorderTopWidth': { - type: 'borderTopWidth', - value: { - default: 0, - isSelected: 1, - isPreviousSelected: 0 - } - }, - '--rowSelectionIndicatorBorderBottomWidth': { - type: 'borderBottomWidth', - value: { - default: 0, - isSelected: 1, - isNextSelected: 0 - } - }, - '--rowSelectionIndicatorBorderStartWidth': { - type: 'borderStartWidth', - value: { - default: 0, - isSelected: 1 - } - }, - '--rowSelectionIndicatorBorderEndWidth': { - type: 'borderEndWidth', - value: { - default: 0, - isSelected: 1 - } - }, - forcedColorAdjust: 'none', - color: { - selectionStyle: { - highlight: { - default: 'gray-700', - isHovered: 'gray-800', - isPressed: 'gray-900' - } - } - } + forcedColorAdjust: 'none' }); -let rowSelectionIndicator = raw(` - &:before { - content: ""; - display: inline-block; - position: absolute; - inset: 0; - border-color: var(--rowSelectionIndicatorColor); - border-top-width: var(--rowSelectionIndicatorBorderTopWidth); - border-bottom-width: var(--rowSelectionIndicatorBorderBottomWidth); - border-inline-start-width: var(--rowSelectionIndicatorBorderStartWidth); - border-inline-end-width: var(--rowSelectionIndicatorBorderEndWidth); - border-style: solid; - z-index: 3; - }`); -let rowFocusIndicator = raw('&:after { content: ""; display: inline-block; position: sticky; inset-inline-start: 0; width: 3px; height: 100%; margin-inline-end: -3px; margin-block-end: 1px; z-index: 3; background-color: var(--rowFocusIndicatorColor)'); - export interface RowProps extends Pick, 'id' | 'columns' | 'isDisabled' | 'onAction' | 'children' | 'textValue' | 'dependencies' | keyof GlobalDOMAttributes>, LinkDOMProps {} /** @@ -1587,11 +1470,9 @@ export const Row = /*#__PURE__*/ (forwardRef as forwardRefType)(function Row row({ ...renderProps, ...tableVisualOptions - }) - + (renderProps.isSelected ? (' ' + rowSelectionIndicator) : '') - + (renderProps.isFocusVisible ? (' ' + rowFocusIndicator) : '')} + }) + (renderProps.isFocusVisible ? ' ' + raw('&:before { content: ""; display: inline-block; position: sticky; inset-inline-start: 0; width: 3px; height: 100%; margin-inline-end: -3px; margin-block-end: 1px; z-index: 3; background-color: var(--rowFocusIndicatorColor)') : '')} {...otherProps}> - {selectionMode !== 'none' && selectionBehavior === 'toggle' && tableVisualOptions.selectionStyle === 'checkbox' && ( + {selectionMode !== 'none' && selectionBehavior === 'toggle' && ( // Not sure what we want to do with this className, in Cell it currently overrides the className that would have been applied. // The `spread` otherProps must be after className in Cell. // @ts-ignore diff --git a/packages/@react-spectrum/s2/src/TreeView.tsx b/packages/@react-spectrum/s2/src/TreeView.tsx index 75c07e5b863..f0a167192c0 100644 --- a/packages/@react-spectrum/s2/src/TreeView.tsx +++ b/packages/@react-spectrum/s2/src/TreeView.tsx @@ -16,7 +16,6 @@ import {baseColor, colorMix, focusRing, fontRelative, style} from '../style' wit import { Button, ButtonContext, - ContextValue, ListLayout, Provider, TreeItemProps as RACTreeItemProps, @@ -33,30 +32,22 @@ import { import {centerBaseline} from './CenterBaseline'; import {Checkbox} from './Checkbox'; import Chevron from '../ui-icons/Chevron'; -import {DOMRef, DOMRefValue, forwardRefType, GlobalDOMAttributes, Key, LoadingState} from '@react-types/shared'; +import {DOMRef, forwardRefType, GlobalDOMAttributes, Key, LoadingState} from '@react-types/shared'; import {getAllowedOverrides, StylesPropWithHeight, UnsafeStyles} from './style-utils' with {type: 'macro'}; import {IconContext} from './Icon'; -import {ImageContext} from './Image'; // @ts-ignore import intlMessages from '../intl/*.json'; import {ProgressCircle} from './ProgressCircle'; import {raw} from '../style/style-macro' with {type: 'macro'}; -import React, {createContext, forwardRef, JSXElementConstructor, ReactElement, ReactNode, useContext, useRef} from 'react'; +import React, {createContext, forwardRef, JSXElementConstructor, ReactElement, ReactNode, useRef} from 'react'; import {Text, TextContext} from './Content'; import {useDOMRef} from '@react-spectrum/utils'; import {useLocale, useLocalizedStringFormatter} from 'react-aria'; import {useScale} from './utils'; -import {useSpectrumContextProps} from './useSpectrumContextProps'; interface S2TreeProps { /** Handler that is called when a user performs an action on a row. */ - onAction?: (key: Key) => void, - /** Whether to remove the outer border/background from the container. */ - isDetached?: boolean, - /** Whether the tree should be displayed with a [emphasized style](https://spectrum.adobe.com/page/tree-view/#Emphasis). */ - isEmphasized?: boolean, - selectionStyle?: 'highlight' | 'checkbox', - selectionCornerStyle?: 'square' | 'round' + onAction?: (key: Key) => void } export interface TreeViewProps extends Omit, 'style' | 'className' | 'render' | 'onRowAction' | 'selectionBehavior' | 'onScroll' | 'onCellAction' | 'dragAndDropHooks' | keyof GlobalDOMAttributes>, UnsafeStyles, S2TreeProps { @@ -79,10 +70,6 @@ interface TreeRendererContextValue { } const TreeRendererContext = createContext({}); -export const TreeViewContext = createContext>, DOMRefValue>>(null); - - -let InternalTreeContext = createContext<{isDetached?: boolean, isEmphasized?: boolean, selectionStyle: 'highlight' | 'checkbox', selectionCornerStyle: 'square' | 'round'}>({selectionStyle: 'checkbox', selectionCornerStyle: 'round'}); // TODO: the below is needed so the borders of the top and bottom row isn't cut off if the TreeView is wrapped within a container by always reserving the 2px needed for the // keyboard focus ring. Perhaps find a different way of rendering the outlines since the top of the item doesn't @@ -113,8 +100,7 @@ const tree = style({ * A tree view provides users with a way to navigate nested hierarchical information. */ export const TreeView = /*#__PURE__*/ (forwardRef as forwardRefType)(function TreeView(props: TreeViewProps, ref: DOMRef) { - [props, ref] = useSpectrumContextProps(props, ref, TreeViewContext); - let {children, isDetached, isEmphasized, selectionStyle = 'checkbox', selectionCornerStyle = 'round', UNSAFE_className, UNSAFE_style} = props; + let {children, UNSAFE_className, UNSAFE_style} = props; let scale = useScale(); let renderer; @@ -131,60 +117,36 @@ export const TreeView = /*#__PURE__*/ (forwardRef as forwardRefType)(function Tr rowHeight: scale === 'large' ? 50 : 40 }}> - - (UNSAFE_className ?? '') + tree({isDetached, ...renderProps}, props.styles)} - selectionBehavior={selectionStyle === 'highlight' ? 'replace' : 'toggle'} - ref={domRef}> - {props.children} - - + (UNSAFE_className ?? '') + tree({...renderProps}, props.styles)} + selectionBehavior="toggle" + ref={domRef}> + {props.children} + ); }); -const selectedBackground = colorMix('gray-25', 'gray-900', 7); -const selectedActiveBackground = colorMix('gray-25', 'gray-900', 10); - const rowBackgroundColor = { - selectionStyle: { - checkbox: { - default: '--s2-container-bg', - isFocusVisibleWithin: colorMix('gray-25', 'gray-900', 7), - isHovered: colorMix('gray-25', 'gray-900', 7), - isPressed: colorMix('gray-25', 'gray-900', 10), - isSelected: { - default: colorMix('gray-25', 'gray-900', 7), - isEmphasized: selectedBackground, - isFocusVisibleWithin: { - default: colorMix('gray-25', 'gray-900', 10), - isEmphasized: selectedActiveBackground - }, - isHovered: { - default: colorMix('gray-25', 'gray-900', 10), - isEmphasized: selectedActiveBackground - }, - isPressed: { - default: colorMix('gray-25', 'gray-900', 10), - isEmphasized: selectedActiveBackground - } - }, - forcedColors: { - default: 'Background' - } - }, - highlight: { - default: 'transparent' - } + default: '--s2-container-bg', + isFocusVisibleWithin: colorMix('gray-25', 'gray-900', 7), + isHovered: colorMix('gray-25', 'gray-900', 7), + isPressed: colorMix('gray-25', 'gray-900', 10), + isSelected: { + default: colorMix('gray-25', 'gray-900', 7), + isFocusVisibleWithin: colorMix('gray-25', 'gray-900', 10), + isHovered: colorMix('gray-25', 'gray-900', 10), + isPressed: colorMix('gray-25', 'gray-900', 10) + }, + forcedColors: { + default: 'Background' } } as const; const treeRow = style({ - ...focusRing(), - outlineOffset: -2, position: 'relative', display: 'flex', height: 40, @@ -211,33 +173,6 @@ const treeRow = style({ default: 'focus-ring', forcedColors: 'Highlight' } - }, - '--borderRadiusTreeItem': { - type: 'borderTopStartRadius', - value: { - default: 'sm', - isRound: 'default' - } - }, - borderTopStartRadius: { - default: '--borderRadiusTreeItem', - isPreviousSelected: 'none', - isDetached: 'default' - }, - borderTopEndRadius: { - default: '--borderRadiusTreeItem', - isPreviousSelected: 'none', - isDetached: 'default' - }, - borderBottomStartRadius: { - default: '--borderRadiusTreeItem', - isNextSelected: 'none', - isDetached: 'default' - }, - borderBottomEndRadius: { - default: '--borderRadiusTreeItem', - isNextSelected: 'none', - isDetached: 'default' } }); @@ -245,45 +180,20 @@ const treeCellGrid = style({ display: 'grid', width: 'full', height: 'full', + boxSizing: 'border-box', alignContent: 'center', alignItems: 'center', - boxSizing: 'border-box', gridTemplateColumns: ['auto', 'auto', 'auto', 'auto', 'auto', '1fr', 'minmax(0, auto)', 'auto'], gridTemplateRows: '1fr', gridTemplateAreas: [ 'drag-handle checkbox level-padding expand-button icon content actions actionmenu' ], + backgroundColor: '--rowBackgroundColor', paddingEnd: 4, // account for any focus rings on the last item in the cell color: { - default: 'gray-700', - isHovered: 'gray-800', - isSelected: 'gray-900', isDisabled: { default: 'gray-400', forcedColors: 'GrayText' - }, - selectionStyle: { - highlight: { - isSelectionDisabled: { - default: 'gray-400', - forcedColors: 'GrayText' - } - } - } - }, - '--thumbnailBorderColor': { - type: 'color', - value: { - default: 'gray-500', - isHovered: 'gray-800', - isSelected: 'gray-900', - isEmphasized: { - isSelected: 'blue-900' - }, - isDisabled: { - default: 'gray-400', - forcedColors: 'GrayText' - } } }, '--rowSelectedBorderColor': { @@ -300,87 +210,6 @@ const treeCellGrid = style({ default: 'focus-ring', forcedColors: 'Highlight' } - }, - '--borderColor': { - type: 'borderTopColor', - value: { - default: 'transparent', - isSelected: 'blue-900', - forcedColors: 'ButtonBorder' - } - } -}); - -const treeRowBackground = style({ - position: 'absolute', - zIndex: -1, - inset: 0, - backgroundColor: { - default: '--rowBackgroundColor', - isHovered: 'gray-100', - isPressed: 'gray-100', - isSelected: { - default: 'blue-900/10', - isHovered: 'blue-900/15', - isPressed: 'blue-900/15' - }, - isDisabled: { - default: 'gray-100' - }, - forcedColors: { - default: 'Background' - } - }, - borderTopStartRadius: { - default: '--borderRadiusTreeItem', - isPreviousSelected: { - default: '--borderRadiusTreeItem', - isSelected: 'none' - }, - isDetached: 'default' - }, - borderTopEndRadius: { - default: '--borderRadiusTreeItem', - isPreviousSelected: { - default: '--borderRadiusTreeItem', - isSelected: 'none' - }, - isDetached: 'default' - }, - borderBottomStartRadius: { - default: '--borderRadiusTreeItem', - isNextSelected: { - default: '--borderRadiusTreeItem', - isSelected: 'none' - }, - isDetached: 'default' - }, - borderBottomEndRadius: { - default: '--borderRadiusTreeItem', - isNextSelected: { - default: '--borderRadiusTreeItem', - isSelected: 'none' - }, - isDetached: 'default' - }, - borderTopWidth: { - default: 1, - isPreviousSelected: 0 - }, - borderBottomWidth: { - default: 1, - isNextSelected: 0 - }, - borderStartWidth: 1, - borderEndWidth: 1, - borderStyle: 'solid', - borderColor: { - default: 'transparent', - isSelected: '--borderColor', - isDetached: { - default: 'transparent', - isSelected: '--rowSelectedBorderColor' - } } }); @@ -404,21 +233,6 @@ const treeIcon = style({ } }); -const treeThumbnail = style({ - gridArea: 'icon', - marginEnd: 'text-to-visual', - width: 32, - aspectRatio: 'square', - objectFit: 'contain', - borderRadius: 'sm', - borderWidth: 1, - borderColor: '--thumbnailBorderColor', - borderStyle: 'solid', - padding: 2, - backgroundColor: 'white', - boxSizing: 'border-box' -}); - const treeContent = style({ gridArea: 'content', textOverflow: 'ellipsis', @@ -456,17 +270,13 @@ export const TreeViewItem = (props: TreeViewItemProps): ReactNode => { let { href } = props; - let {isEmphasized, selectionStyle, selectionCornerStyle} = useContext(InternalTreeContext); return ( treeRow({ ...renderProps, - isLink: !!href, - isEmphasized, - selectionStyle, - isRound: selectionCornerStyle === 'round' + isLink: !!href }) + (renderProps.isFocusVisible ? ' ' + treeRowFocusIndicator : '')} /> ); }; @@ -480,29 +290,20 @@ export const TreeViewItemContent = (props: TreeViewItemContentProps): ReactNode let { children } = props; - let {isDetached, isEmphasized, selectionCornerStyle, selectionStyle} = useContext(InternalTreeContext); let scale = useScale(); return ( - {({isExpanded, hasChildItems, selectionMode, selectionBehavior, isDisabled, isFocusVisible, isSelected, id, state, isHovered, isSelectionDisabled}) => { + {({isExpanded, hasChildItems, selectionMode, selectionBehavior, isDisabled, isSelected, id, state}) => { let isNextSelected = false; let isNextFocused = false; - let isPreviousSelected = false; - let keyBefore = state.collection.getKeyBefore(id); let keyAfter = state.collection.getKeyAfter(id); - if (keyBefore != null) { - isPreviousSelected = state.selectionManager.isSelected(keyBefore); - } if (keyAfter != null) { isNextSelected = state.selectionManager.isSelected(keyAfter); } let isFirst = state.collection.getFirstKey() === id; - let isRound = selectionCornerStyle === 'round'; - return ( -
      -
      +
      {selectionMode !== 'none' && selectionBehavior === 'toggle' && ( // TODO: add transition?
      @@ -518,13 +319,12 @@ export const TreeViewItemContent = (props: TreeViewItemContentProps): ReactNode {typeof children === 'string' ? {children} : children} diff --git a/packages/@react-spectrum/s2/src/index.ts b/packages/@react-spectrum/s2/src/index.ts index 5451bde9441..d2580be6b0a 100644 --- a/packages/@react-spectrum/s2/src/index.ts +++ b/packages/@react-spectrum/s2/src/index.ts @@ -88,7 +88,7 @@ export {ToastContainer, ToastQueue} from './Toast'; export {ToggleButton, ToggleButtonContext} from './ToggleButton'; export {ToggleButtonGroup, ToggleButtonGroupContext} from './ToggleButtonGroup'; export {Tooltip, TooltipTrigger} from './Tooltip'; -export {TreeView, TreeViewItem, TreeViewItemContent, TreeViewContext, TreeViewLoadMoreItem} from './TreeView'; +export {TreeView, TreeViewItem, TreeViewItemContent, TreeViewLoadMoreItem} from './TreeView'; export {pressScale} from './pressScale'; diff --git a/packages/@react-spectrum/s2/stories/HighlightSelectionList.stories.tsx b/packages/@react-spectrum/s2/stories/HighlightSelectionList.stories.tsx deleted file mode 100644 index d447c6d982e..00000000000 --- a/packages/@react-spectrum/s2/stories/HighlightSelectionList.stories.tsx +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Copyright 2020 Adobe. All rights reserved. - * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. You may obtain a copy - * of the License at http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - * OF ANY KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ -// TODO: pull all the highlight styles out into a separate macro(s) for the background color, text color, etc. -import ABC from '../s2wf-icons/S2_Icon_ABC_20_N.svg'; -import { - ActionButton, - ActionButtonGroup, - ListView, - ListViewItem, - Text -} from '../src'; -import Add from '../s2wf-icons/S2_Icon_Add_20_N.svg'; -import {categorizeArgTypes, getActionArgs} from './utils'; -import InfoCircle from '../s2wf-icons/S2_Icon_InfoCircle_20_N.svg'; -import type {Meta, StoryObj} from '@storybook/react'; -import React from 'react'; -import {style} from '../style/spectrum-theme' with {type: 'macro'}; -import TextNumbers from '../s2wf-icons/S2_Icon_TextNumbers_20_N.svg'; - -const events = ['onSelectionChange']; - -const meta: Meta = { - title: 'Highlight Selection/ListView', - component: ListView, - parameters: { - layout: 'centered' - }, - args: {...getActionArgs(events)}, - argTypes: { - ...categorizeArgTypes('Events', events), - children: {table: {disable: true}} - } -}; - -export default meta; - -interface Item { - id: number, - name: string, - type: 'letter' | 'number' -} - -let items: Item[] = [ - {id: 1, name: 'Count', type: 'number'}, - {id: 2, name: 'City', type: 'letter'}, - {id: 3, name: 'Count of identities', type: 'number'}, - {id: 4, name: 'Current day', type: 'number'}, - {id: 5, name: 'Current month', type: 'letter'}, - {id: 6, name: 'Current week', type: 'number'}, - {id: 7, name: 'Current year', type: 'number'}, - {id: 8, name: 'Current whatever', type: 'number'}, - {id: 9, name: 'Alphabet', type: 'letter'}, - {id: 10, name: 'Numbers', type: 'number'} -]; - -export const AttributesList: StoryObj = { - render: (args) => ( - - {item => ( - - {item.type === 'number' ? : } - {item.name} - - - - - - - - - - )} - - ), - args: { - selectionStyle: 'highlight', - selectionMode: 'multiple' - } -}; diff --git a/packages/@react-spectrum/s2/stories/HighlightSelectionTable.stories.tsx b/packages/@react-spectrum/s2/stories/HighlightSelectionTable.stories.tsx deleted file mode 100644 index 4476cb3b5d4..00000000000 --- a/packages/@react-spectrum/s2/stories/HighlightSelectionTable.stories.tsx +++ /dev/null @@ -1,121 +0,0 @@ -/** - * Copyright 2020 Adobe. All rights reserved. - * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. You may obtain a copy - * of the License at http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - * OF ANY KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ - -import {CalendarDate, getLocalTimeZone} from '@internationalized/date'; -import {categorizeArgTypes, getActionArgs} from './utils'; -import { - Cell, - Column, - Row, - TableBody, - TableHeader, - TableView, - TableViewProps -} from '../src'; -import type {Meta} from '@storybook/react'; -import React, {ReactElement} from 'react'; -import {style} from '../style/spectrum-theme' with {type: 'macro'}; -import UserGroup from '../s2wf-icons/S2_Icon_UserGroup_20_N.svg'; - -const events = ['onSelectionChange']; - -const meta: Meta = { - title: 'Highlight Selection/TableView', - component: TableView, - parameters: { - layout: 'centered' - }, - args: {...getActionArgs(events)}, - argTypes: { - ...categorizeArgTypes('Events', events), - children: {table: {disable: true}} - } -}; - -export default meta; - -let columns = [ - {name: 'Name', id: 'name', isRowHeader: true, minWidth: 400}, - {name: 'Sharing', id: 'sharing', minWidth: 200}, - {name: 'Date modified', id: 'date', minWidth: 200} -]; - -interface Item { - id: number, - name: { - name: string, - meta: string, - description?: string - }, - sharing: string, - date: CalendarDate -} - -let items: Item[] = [ - {id: 1, name: {name: 'Designer resume', meta: 'PDF', description: 'From Molly Holt'}, sharing: 'public', date: new CalendarDate(2020, 7, 6)}, - // eslint-disable-next-line quotes - {id: 2, name: {name: `Career Management for IC's`, meta: 'PDF'}, sharing: 'public', date: new CalendarDate(2020, 7, 6)}, - {id: 3, name: {name: 'CMP Sessions', meta: 'PDF'}, sharing: 'public', date: new CalendarDate(2020, 7, 6)}, - {id: 4, name: {name: 'Clifton Strength Assessment Info', meta: 'Folder'}, sharing: 'none', date: new CalendarDate(2020, 7, 6)}, - {id: 5, name: {name: 'Personal Brand', meta: 'Zip'}, sharing: 'private', date: new CalendarDate(2020, 7, 6)}, - {id: 6, name: {name: 'Personal Brand', meta: 'Zip'}, sharing: 'private', date: new CalendarDate(2020, 7, 6)}, - {id: 7, name: {name: 'Personal Brand', meta: 'Zip'}, sharing: 'private', date: new CalendarDate(2020, 7, 6)}, - {id: 8, name: {name: 'Personal Brand', meta: 'Zip'}, sharing: 'private', date: new CalendarDate(2020, 7, 6)}, - {id: 9, name: {name: 'Personal Brand', meta: 'Zip'}, sharing: 'private', date: new CalendarDate(2020, 7, 6)}, - {id: 10, name: {name: 'Personal Brand', meta: 'Zip'}, sharing: 'private', date: new CalendarDate(2020, 7, 6)} -]; - -export const DocumentsTable = { - render: (args: TableViewProps): ReactElement => ( - - - {(column) => ( - {column.name} - )} - - - {item => ( - - {(column) => { - if (column.id === 'sharing') { - let content = item[column.id] === 'public' ?
      Shared
      : 'Only you'; - if (item[column.id] === 'none') { - content = '-'; - } - return {content}; - } - if (column.id === 'name') { - return ( - -
      -
      {item[column.id].name}
      -
      -
      - ); - } - if (column.id === 'date') { - return {item[column.id].toDate(getLocalTimeZone()).toLocaleDateString('en-US', {year: 'numeric', month: 'long', day: 'numeric'})}; - } - return {item[column.id]}; - }} -
      - )} -
      -
      - ), - args: { - overflowMode: 'wrap', - selectionStyle: 'highlight', - selectionMode: 'multiple', - isEmphasized: true - } -}; diff --git a/packages/@react-spectrum/s2/stories/HighlightSelectionTree.stories.tsx b/packages/@react-spectrum/s2/stories/HighlightSelectionTree.stories.tsx deleted file mode 100644 index d7504847773..00000000000 --- a/packages/@react-spectrum/s2/stories/HighlightSelectionTree.stories.tsx +++ /dev/null @@ -1,272 +0,0 @@ -/** - * Copyright 2020 Adobe. All rights reserved. - * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. You may obtain a copy - * of the License at http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - * OF ANY KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ - -import { - ActionButton, - ActionButtonGroup, - Collection, - Image, - Key, - Text, - TreeView, - TreeViewItem, - TreeViewItemContent, - TreeViewItemProps, - TreeViewLoadMoreItem, - TreeViewLoadMoreItemProps, - TreeViewProps -} from '../src'; -import {categorizeArgTypes, getActionArgs} from './utils'; -import {checkers} from './assets/check'; -import FileTxt from '../s2wf-icons/S2_Icon_FileText_20_N.svg'; -import Folder from '../s2wf-icons/S2_Icon_Folder_20_N.svg'; -import FolderOpen from '../s2wf-icons/S2_Icon_FolderOpen_20_N.svg'; -import Lock from '../s2wf-icons/S2_Icon_Lock_20_N.svg'; -import LockOpen from '../s2wf-icons/S2_Icon_LockOpen_20_N.svg'; -import type {Meta, StoryObj} from '@storybook/react'; -import React, {ReactElement, useState} from 'react'; -import Visibility from '../s2wf-icons/S2_Icon_Visibility_20_N.svg'; -import VisibilityOff from '../s2wf-icons/S2_Icon_VisibilityOff_20_N.svg'; - -const events = ['onSelectionChange']; - -const meta: Meta = { - title: 'Highlight Selection/TreeView', - component: TreeView, - parameters: { - layout: 'centered' - }, - args: {...getActionArgs(events)}, - argTypes: { - ...categorizeArgTypes('Events', events), - children: {table: {disable: true}} - } -}; - -export default meta; - - -interface TreeViewLayersItemType { - id?: string, - name: string, - icon?: ReactElement, - childItems?: TreeViewLayersItemType[], - isLocked?: boolean, - isVisible?: boolean -} - -let layersRows: TreeViewLayersItemType[] = [ - {id: 'layer-1', name: 'Layer', icon: }, - {id: 'layer-2', name: 'Layer', icon: , isVisible: false}, - {id: 'layer-group-1', name: 'Layer group', icon: , isVisible: false, childItems: [ - {id: 'layer-group-1-1', name: 'Layer', icon: }, - {id: 'layer-group-1-2', name: 'Layer', icon: }, - {id: 'layer-group-1-3', name: 'Layer', icon: }, - {id: 'layer-group-1-4', name: 'Layer', icon: }, - {id: 'layer-group-1-group-1', name: 'Layer Group', icon: , childItems: [ - {id: 'layer-group-1-group-1-1', name: 'Layer', icon: }, - {id: 'layer-group-1-group-1-2', name: 'Layer', icon: }, - {id: 'layer-group-1-group-1-3', name: 'Layer', icon: } - ]} - ]}, - {id: 'layer-group-2', name: 'Layer group', icon: , isLocked: true, childItems: [ - {id: 'layer-group-2-1', name: 'Layer', icon: }, - {id: 'layer-group-2-2', name: 'Layer', icon: , isVisible: false}, - {id: 'layer-group-2-3', name: 'Layer', icon: , isLocked: true}, - {id: 'layer-group-2-4', name: 'Layer', icon: }, - {id: 'layer-group-2-group-1', name: 'Layer Group', icon: } - ]}, - {id: 'layer-group-3', name: 'Layer group', icon: , childItems: [ - {id: 'reports-1', name: 'Reports 1', icon: , childItems: [ - {id: 'layer-group-3-1', name: 'Layer', icon: }, - {id: 'layer-group-3-2', name: 'Layer', icon: }, - {id: 'layer-group-3-3', name: 'Layer', icon: }, - {id: 'layer-group-3-4', name: 'Layer', icon: }, - {id: 'layer-group-3-group-1', name: 'Layer Group', icon: , childItems: [ - {id: 'layer-group-3-group-1-1', name: 'Layer', icon: }, - {id: 'layer-group-3-group-1-2', name: 'Layer', icon: }, - {id: 'layer-group-3-group-1-3', name: 'Layer', icon: } - ]} - ]}, - {id: 'layer-group-3-2', name: 'Layer', icon: }, - {id: 'layer-group-3-3', name: 'Layer', icon: }, - {id: 'layer-group-3-4', name: 'Layer', icon: }, - ...Array.from({length: 100}, (_, i) => ({id: `layer-group-3-repeat-${i}`, name: 'Layer', icon: })) - ]}, - {id: 'layer-4', name: 'Layer', icon: , isLocked: true, isVisible: false} -]; - -const TreeExampleLayersItem = (props: Omit & TreeViewLayersItemType & TreeViewLoadMoreItemProps): ReactElement => { - let {childItems, name, icon = , loadingState, onLoadMore, isLocked = false, isVisible = true} = props; - return ( - <> - - - {name} - {icon} - - {isLocked ? : } - {isVisible ? : } - - - - {(item) => ( - - )} - - {onLoadMore && loadingState && } - - - ); -}; - -const TreeExampleLayers = (args: TreeViewProps): ReactElement => ( -
      - - {(item) => ( - - )} - -
      -); - -export const LayersTree: StoryObj = { - render: TreeExampleLayers, - args: { - defaultExpandedKeys: ['layer-group-2'], - selectionMode: 'multiple', - selectionStyle: 'highlight', - selectionCornerStyle: 'round', - isEmphasized: true - } -}; - -interface TreeViewFileItemType { - id?: string, - name: string, - icon?: ReactElement, - childItems?: TreeViewFileItemType[], - isExpanded?: boolean -} - -let rows: TreeViewFileItemType[] = [ - {id: 'documentation', name: 'Documentation', icon: , childItems: [ - {id: 'project-1', name: 'Project 1 Level 1', icon: }, - {id: 'project-2', name: 'Project 2 Level 1', icon: , childItems: [ - {id: 'project-2A', name: 'Project 2A Level 2', icon: }, - {id: 'project-2B', name: 'Project 2B Level 2', icon: }, - {id: 'project-2C', name: 'Project 2C Level 3', icon: } - ]}, - {id: 'project-3', name: 'Project 3', icon: }, - {id: 'project-4', name: 'Project 4', icon: }, - {id: 'project-5', name: 'Project 5', icon: , childItems: [ - {id: 'project-5A', name: 'Project 5A', icon: }, - {id: 'project-5B', name: 'Project 5B', icon: }, - {id: 'project-5C', name: 'Project 5C', icon: } - ]}, - ...Array.from({length: 100}, (_, i) => ({id: `projects-repeat-${i}`, name: `Reports ${i}`, icon: })) - ]}, - {id: 'branding', name: 'Branding', icon: , childItems: [ - {id: 'proposals', name: 'Proposals', icon: }, - {id: 'explorations', name: 'Explorations', icon: }, - {id: 'assets', name: 'Assets', icon: } - ]}, - {id: 'file01', name: 'File 01', icon: }, - {id: 'file02', name: 'File 02', icon: }, - {id: 'file03', name: 'File 03', icon: } -]; - -const TreeExampleFileItem = (props: Omit & TreeViewFileItemType & TreeViewLoadMoreItemProps & {expandedKeys: Set}): ReactElement => { - let {childItems, name, icon = , loadingState, onLoadMore, expandedKeys} = props; - let isExpanded = expandedKeys.has(props.id as Key); - return ( - <> - - - {name} - {isExpanded ? : icon} - - - {(item) => ( - - )} - - {onLoadMore && loadingState && } - - - ); -}; - -const TreeExampleFiles = (args: TreeViewProps): ReactElement => { - let [expandedKeys, setExpandedKeys] = useState>(new Set(['branding'])); - let [items, setItems] = useState(rows); - let onExpandedChange = (keys: Set) => { - setExpandedKeys(keys); - // Iterate over depth first all items in 'rows' that are in the keys set, add a property 'isExpanded' to the item. we must maintain the tree structure. - // This is to work around the fact that we cannot change the icon inside the TreeViewItemContent because it doesn't re-render for the expanded state change. - let newItems = rows.reduce((acc, item) => { - let iterator = (children: TreeViewFileItemType[]) => { - return children.map(child => { - let newChild = {...child}; - if (keys.has(child.id as Key)) { - newChild.isExpanded = true; - } - if (child.childItems) { - newChild.childItems = iterator(child.childItems); - } - return newChild; - }); - }; - let newChildren; - if (item.childItems) { - newChildren = iterator(item.childItems); - } - acc.push({...item, isExpanded: keys.has(item.id as Key), childItems: newChildren}); - return acc; - }, [] as TreeViewFileItemType[]); - setItems(newItems); - }; - return ( -
      - - {(item) => ( - - )} - -
      - ); -}; - -export const FileTree: StoryObj = { - render: TreeExampleFiles, - args: { - selectionMode: 'multiple', - selectionStyle: 'highlight', - selectionCornerStyle: 'square', - isEmphasized: true - } -}; diff --git a/packages/react-aria-components/src/Table.tsx b/packages/react-aria-components/src/Table.tsx index 6bd94d69b5c..c1096ead793 100644 --- a/packages/react-aria-components/src/Table.tsx +++ b/packages/react-aria-components/src/Table.tsx @@ -1090,10 +1090,6 @@ export const TableBody = /*#__PURE__*/ createBranchComponent(TableBodyNode, , /** The unique id of the tree row. */ - id: Key, - /** Whether the tree item has its selection disabled. */ - isSelectionDisabled: boolean + id: Key } export interface TreeItemContentRenderProps extends TreeItemRenderProps {} @@ -692,14 +690,13 @@ export const TreeItem = /*#__PURE__*/ createBranchComponent(TreeItemNode, Date: Mon, 23 Feb 2026 13:13:07 -0600 Subject: [PATCH 42/60] remove unrelated Picker change --- packages/@react-spectrum/s2/src/Picker.tsx | 24 +++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/@react-spectrum/s2/src/Picker.tsx b/packages/@react-spectrum/s2/src/Picker.tsx index afc38783c6f..dab71456292 100644 --- a/packages/@react-spectrum/s2/src/Picker.tsx +++ b/packages/@react-spectrum/s2/src/Picker.tsx @@ -48,7 +48,7 @@ import { } from './Menu'; import CheckmarkIcon from '../ui-icons/Checkmark'; import ChevronIcon from '../ui-icons/Chevron'; -import {control, controlBorderRadius, field, fieldInput, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; +import {control, controlBorderRadius, controlFont, field, fieldInput, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; import {createHideableComponent} from '@react-aria/collections'; import { Divider, @@ -57,6 +57,7 @@ import { listboxItem, LOADER_ROW_HEIGHTS } from './ComboBox'; +import {edgeToText} from '../style/spectrum-theme' with {type: 'macro'}; import { FieldErrorIcon, FieldLabel, @@ -190,6 +191,27 @@ const quietFocusLine = style({ } }); +export let menu = style({ + outlineStyle: 'none', + display: 'grid', + width: 'full', + gridTemplateColumns: { + size: { + S: [edgeToText(24), 'auto', 'auto', 'minmax(0, 1fr)', 'auto', 'auto', 'auto', edgeToText(24)], + M: [edgeToText(32), 'auto', 'auto', 'minmax(0, 1fr)', 'auto', 'auto', 'auto', edgeToText(32)], + L: [edgeToText(40), 'auto', 'auto', 'minmax(0, 1fr)', 'auto', 'auto', 'auto', edgeToText(40)], + XL: [edgeToText(48), 'auto', 'auto', 'minmax(0, 1fr)', 'auto', 'auto', 'auto', edgeToText(48)] + } + }, + boxSizing: 'border-box', + maxHeight: 'inherit', + overflow: 'auto', + padding: 8, + fontFamily: 'sans', + fontSize: controlFont(), + gridAutoRows: 'min-content' +}); + const invalidBorder = style({ ...controlBorderRadius(), position: 'absolute', From cc16924bdbd9ec236e2420e2a1ec1d8982b631d2 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Mon, 23 Feb 2026 14:35:50 -0600 Subject: [PATCH 43/60] remove press scaling --- packages/@react-spectrum/s2/src/ListView.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index 3a7eb84b225..f28cb269812 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -45,7 +45,6 @@ import {IconContext} from './Icon'; import {ImageContext} from './Image'; // @ts-ignore import intlMessages from '../intl/*.json'; -import {pressScale} from './pressScale'; import {ProgressCircle} from './ProgressCircle'; import {Text, TextContext} from './Content'; import {useDOMRef} from '@react-spectrum/utils'; @@ -568,7 +567,7 @@ export function ListViewItem(props: ListViewItemProps): ReactNode { {...otherProps} textValue={textValue} ref={ref} - style={pressScale(ref, props.UNSAFE_style)} + style={props.UNSAFE_style} className={renderProps => (props.UNSAFE_className || '') + listitem({ ...renderProps, isLink, From 493d2b8c7713220f0444c5ed1255de9e2ca89374 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Mon, 23 Feb 2026 16:07:08 -0600 Subject: [PATCH 44/60] keep bottom border width static to avoid 1px shift when highlight selection occurs. --- packages/@react-spectrum/s2/src/ListView.tsx | 39 +++++++++++++------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index f28cb269812..5ba5c6b50c5 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -46,6 +46,7 @@ import {ImageContext} from './Image'; // @ts-ignore import intlMessages from '../intl/*.json'; import {ProgressCircle} from './ProgressCircle'; +import {raw} from '../style/style-macro' with {type: 'macro'}; import {Text, TextContext} from './Content'; import {useDOMRef} from '@react-spectrum/utils'; import {useLocale, useLocalizedStringFormatter} from 'react-aria'; @@ -301,15 +302,7 @@ const listitem = style -
      +
      {selectionMode !== 'none' && selectionBehavior === 'toggle' && ( )} From 595f9c9f896971bd8b478c7e7724cf1be4b87b7e Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Mon, 23 Feb 2026 16:21:16 -0600 Subject: [PATCH 45/60] add remaining states to chromatic --- .../s2/chromatic/ListView.stories.tsx | 65 ++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/packages/@react-spectrum/s2/chromatic/ListView.stories.tsx b/packages/@react-spectrum/s2/chromatic/ListView.stories.tsx index 31791101f43..23d16579e05 100644 --- a/packages/@react-spectrum/s2/chromatic/ListView.stories.tsx +++ b/packages/@react-spectrum/s2/chromatic/ListView.stories.tsx @@ -10,7 +10,8 @@ * governing permissions and limitations under the License. */ -import {ActionButton, ActionButtonGroup, ActionMenu, Content, Heading, IllustratedMessage, ListView, ListViewItem, MenuItem, Text} from '../src'; +import {ActionButton, ActionButtonGroup, ActionMenu, Content, Heading, IllustratedMessage, Image, ListView, ListViewItem, MenuItem, Text} from '../src'; +import {checkers} from '../stories/assets/check'; import Delete from '../s2wf-icons/S2_Icon_Delete_20_N.svg'; import Edit from '../s2wf-icons/S2_Icon_Edit_20_N.svg'; import File from '../s2wf-icons/S2_Icon_File_20_N.svg'; @@ -75,6 +76,19 @@ export const Quiet: Story = { } }; +export const WithImages: Story = { + render: (args) => ( + + {(item) => ( + + {item.name} + {item.name} + + )} + + ) +}; + export const OverflowTruncate: Story = { render: (args) => ( @@ -140,6 +154,29 @@ export const DisabledBehaviorSelection: Story = { } }; +export const CheckboxSelection: Story = { + ...Example, + args: { + ...Example.args, + selectionStyle: 'checkbox', + selectedKeys: ['photoshop', 'illustrator'] + } +}; + +export const Links: Story = { + render: (args) => ( + + Adobe + Google + Apple + New York Times + + ), + args: { + selectionMode: 'none' + } +}; + export const WithActions: Story = { render: (args) => ( @@ -173,6 +210,32 @@ export const WithActions: Story = { } }; +export const Loading: Story = { + render: (args) => ( + + {[]} + + ), + args: { + loadingState: 'loading' + } +}; + +export const LoadingMore: Story = { + render: (args) => ( + {}}> + {(item) => ( + + {item.name} + + )} + + ), + args: { + loadingState: 'loadingMore' + } +}; + export const EmptyState: Story = { render: (args) => ( Date: Mon, 23 Feb 2026 16:31:12 -0600 Subject: [PATCH 46/60] remove ListViewLoadMoreItem --- packages/@react-spectrum/s2/src/ListView.tsx | 20 -------------------- packages/@react-spectrum/s2/src/index.ts | 4 ++-- packages/dev/s2-docs/pages/s2/ListView.mdx | 9 ++------- 3 files changed, 4 insertions(+), 29 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index 5ba5c6b50c5..6fcab078783 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -27,7 +27,6 @@ import { GridListItemProps, GridListItemRenderProps, GridListLoadMoreItem, - GridListLoadMoreItemProps, GridListProps, GridListRenderProps, ListLayout, @@ -88,11 +87,6 @@ export interface ListViewItemProps extends Omit { - /** The current loading state of the ListView. */ - loadingState?: LoadingState -} - interface ListViewRendererContextValue { renderer?: (item) => ReactElement> } @@ -648,17 +642,3 @@ export function ListViewItem(props: ListViewItemProps): ReactNode { ); } -export const ListViewLoadMoreItem = (props: ListViewLoadMoreItemProps): ReactNode => { - let {loadingState, onLoadMore} = props; - let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2'); - let isLoading = loadingState === 'loading' || loadingState === 'loadingMore'; - return ( - -
      - -
      -
      - ); -}; diff --git a/packages/@react-spectrum/s2/src/index.ts b/packages/@react-spectrum/s2/src/index.ts index d2580be6b0a..15f0db30c0f 100644 --- a/packages/@react-spectrum/s2/src/index.ts +++ b/packages/@react-spectrum/s2/src/index.ts @@ -57,7 +57,7 @@ export {Image, ImageContext} from './Image'; export {ImageCoordinator} from './ImageCoordinator'; export {InlineAlert, InlineAlertContext} from './InlineAlert'; export {Link, LinkContext} from './Link'; -export {ListView, ListViewItem, ListViewLoadMoreItem} from './ListView'; +export {ListView, ListViewItem} from './ListView'; export {MenuItem, MenuTrigger, Menu, MenuSection, SubmenuTrigger, MenuContext} from './Menu'; export {Meter, MeterContext} from './Meter'; export {NotificationBadge, NotificationBadgeContext} from './NotificationBadge'; @@ -137,7 +137,7 @@ export type {InlineAlertProps} from './InlineAlert'; export type {ImageProps} from './Image'; export type {ImageCoordinatorProps} from './ImageCoordinator'; export type {LinkProps} from './Link'; -export type {ListViewProps, ListViewItemProps, ListViewLoadMoreItemProps} from './ListView'; +export type {ListViewProps, ListViewItemProps} from './ListView'; export type {MenuTriggerProps, MenuProps, MenuItemProps, MenuSectionProps, SubmenuTriggerProps} from './Menu'; export type {MeterProps} from './Meter'; export type {NotificationBadgeProps} from './NotificationBadge'; diff --git a/packages/dev/s2-docs/pages/s2/ListView.mdx b/packages/dev/s2-docs/pages/s2/ListView.mdx index 15ba6a22b19..db59effc2f8 100644 --- a/packages/dev/s2-docs/pages/s2/ListView.mdx +++ b/packages/dev/s2-docs/pages/s2/ListView.mdx @@ -1,7 +1,7 @@ import {Layout} from '../../src/Layout'; export default Layout; -import {ListView, ListViewItem, ListViewLoadMoreItem, Text, ActionMenu, MenuItem, ActionButton, ActionButtonGroup, Image, IllustratedMessage, Heading, Content, Link, Breadcrumbs, Breadcrumb} from '@react-spectrum/s2'; +import {ListView, ListViewItem, Text, ActionMenu, MenuItem, ActionButton, ActionButtonGroup, Image, IllustratedMessage, Heading, Content, Link, Breadcrumbs, Breadcrumb} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['list', 'data', 'collection', 'table', 'grid']; @@ -334,7 +334,7 @@ function NavigationExample() { ## API -```tsx links={{ListView: '#listview', ListViewItem: '#listviewitem', ListViewLoadMoreItem: '#listviewloadmoreitem', ActionMenu: 'ActionMenu', ActionButtonGroup: 'ActionButtonGroup', Icon: 'icons', Text: 'https://developer.mozilla.org/en-US/docs/Web/HTML/Element/span', Image: 'https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img'}} +```tsx links={{ListView: '#listview', ListViewItem: '#listviewitem', ActionMenu: 'ActionMenu', ActionButtonGroup: 'ActionButtonGroup', Icon: 'icons', Text: 'https://developer.mozilla.org/en-US/docs/Web/HTML/Element/span', Image: 'https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img'}} @@ -343,7 +343,6 @@ function NavigationExample() { or - ``` @@ -354,7 +353,3 @@ function NavigationExample() { ### ListViewItem - -### ListViewLoadMoreItem - - From 878d4c4a999e2687b939426a6b4238add83fa183 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Mon, 23 Feb 2026 16:36:41 -0600 Subject: [PATCH 47/60] fix prop types --- packages/@react-spectrum/s2/src/ListView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index 6fcab078783..7fd9ef0567c 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -52,7 +52,7 @@ import {useLocale, useLocalizedStringFormatter} from 'react-aria'; import {useScale} from './utils'; import {useSpectrumContextProps} from './useSpectrumContextProps'; -export interface ListViewProps extends Omit, 'className' | 'style' | 'children' | 'selectionBehavior' | 'dragAndDropHooks' | 'layout' | 'render' | keyof GlobalDOMAttributes>, DOMProps, UnsafeStyles, ListViewStylesProps, SlotProps { +export interface ListViewProps extends Omit, 'className' | 'style' | 'children' | 'selectionBehavior' | 'dragAndDropHooks' | 'layout' | 'render' | 'keyboardNavigationBehavior' | keyof GlobalDOMAttributes>, DOMProps, UnsafeStyles, ListViewStylesProps, SlotProps { /** Spectrum-defined styles, returned by the `style()` macro. */ styles?: StylesPropWithHeight, /** The current loading state of the ListView. */ @@ -78,7 +78,7 @@ interface ListViewStylesProps { overflowMode?: 'wrap' | 'truncate' } -export interface ListViewItemProps extends Omit, StyleProps { +export interface ListViewItemProps extends Omit, StyleProps { /** * The contents of the item. */ From e95108dc20e1a85a38cff30eebbb1c219e160ad2 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Mon, 23 Feb 2026 16:38:19 -0600 Subject: [PATCH 48/60] update migration guide --- packages/dev/s2-docs/pages/s2/migrating.mdx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/dev/s2-docs/pages/s2/migrating.mdx b/packages/dev/s2-docs/pages/s2/migrating.mdx index 2aa45260848..e0574bc28fb 100644 --- a/packages/dev/s2-docs/pages/s2/migrating.mdx +++ b/packages/dev/s2-docs/pages/s2/migrating.mdx @@ -241,6 +241,7 @@ No updates needed. - If within `Breadcrumbs`: Update `Item` to be a `Breadcrumb` - If within `Picker`: Update `Item` to be a `PickerItem` - If within `ComboBox`: Update `Item` to be a `ComboBoxItem` +- If within `ListView`: Update `Item` to be a `ListViewItem` - If within `TabList`: Update `Item` to be a `Tab` - If within `TabPanels`: Update `Item` to be a `TabPanel` and remove surrounding `TabPanels` - Update `key` to be `id` (and keep `key` if rendered inside `array.map`) @@ -250,6 +251,11 @@ No updates needed. - Change `variant="overBackground"` to `staticColor="white"` - If `a` was used inside `Link` (legacy API), remove the `a` and apply props (i.e `href`) directly to `Link` +### ListView + +- Comment out `density` (it has not been implemented yet) +- Comment out `dragAndDropHooks` (it has not been implemented yet) + ### Menu - Update `Item` to be a `MenuItem` From 823d82673ee012327366210aa162dbb300a332b9 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Mon, 23 Feb 2026 16:57:35 -0600 Subject: [PATCH 49/60] remove bottom border radius for last item --- packages/@react-spectrum/s2/src/ListView.tsx | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index 7fd9ef0567c..8cbf648257c 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -314,12 +314,6 @@ const listitem = style Date: Mon, 23 Feb 2026 16:58:41 -0600 Subject: [PATCH 50/60] prop types --- packages/@react-spectrum/s2/src/ListView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index 8cbf648257c..93e346f69ef 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -36,7 +36,7 @@ import { Virtualizer } from 'react-aria-components'; import Chevron from '../ui-icons/Chevron'; -import {controlFont, getAllowedOverrides, StyleProps, StylesPropWithHeight, UnsafeStyles} from './style-utils' with {type: 'macro'}; +import {controlFont, getAllowedOverrides, StylesPropWithHeight, UnsafeStyles} from './style-utils' with {type: 'macro'}; import {createContext, forwardRef, JSXElementConstructor, ReactElement, ReactNode, useContext, useRef} from 'react'; import {DOMProps, DOMRef, DOMRefValue, forwardRefType, GlobalDOMAttributes, LoadingState} from '@react-types/shared'; import {edgeToText} from '../style/spectrum-theme' with {type: 'macro'}; @@ -78,7 +78,7 @@ interface ListViewStylesProps { overflowMode?: 'wrap' | 'truncate' } -export interface ListViewItemProps extends Omit, StyleProps { +export interface ListViewItemProps extends Omit { /** * The contents of the item. */ From 8ad1fb87fa68f6e77222bcb84d5d0b7da259237a Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Mon, 23 Feb 2026 17:08:11 -0600 Subject: [PATCH 51/60] remove style props from list item --- packages/@react-spectrum/s2/src/ListView.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index 93e346f69ef..ac228b9b2a2 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -315,7 +315,7 @@ const listitem = style (props.UNSAFE_className || '') + listitem({ + className={renderProps => listitem({ ...renderProps, isLink, isQuiet, @@ -565,7 +564,7 @@ export function ListViewItem(props: ListViewItemProps): ReactNode { selectionStyle, isPrevNotSelected: !renderProps.isPrevSelected, isNextNotSelected: !renderProps.isNextSelected - }, props.styles)}> + })}> {(renderProps) => { let {children} = props; let {selectionMode, selectionBehavior, isDisabled} = renderProps; From f2365d7e5f390bb007010fc8291cd16b0aaf72a4 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Tue, 24 Feb 2026 15:17:06 -0600 Subject: [PATCH 52/60] remove seam mask element in favor of extending row background --- packages/@react-spectrum/s2/src/ListView.tsx | 36 ++++++++------------ 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index ac228b9b2a2..f716a568313 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -45,7 +45,6 @@ import {ImageContext} from './Image'; // @ts-ignore import intlMessages from '../intl/*.json'; import {ProgressCircle} from './ProgressCircle'; -import {raw} from '../style/style-macro' with {type: 'macro'}; import {Text, TextContext} from './Content'; import {useDOMRef} from '@react-spectrum/utils'; import {useLocale, useLocalizedStringFormatter} from 'react-aria'; @@ -331,7 +330,20 @@ const listRowBackground = style({ position: 'absolute', zIndex: -1, - inset: 0, + top: 0, + left: 0, + right: 0, + bottom: { + default: 0, + isSelected: { + selectionStyle: { + highlight: { + default: 0, + isNextSelected: '[-1px]' + } + } + } + }, backgroundColor: { default: '--rowBackgroundColor', isHovered: { @@ -401,23 +413,6 @@ const listRowBackground = style
      {selectionMode !== 'none' && selectionBehavior === 'toggle' && ( From 9849d9f76ef049c95a40af8a775f0dcdd247d9a6 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Tue, 24 Feb 2026 15:30:54 -0600 Subject: [PATCH 53/60] make docs example wide --- packages/dev/s2-docs/pages/s2/ListView.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dev/s2-docs/pages/s2/ListView.mdx b/packages/dev/s2-docs/pages/s2/ListView.mdx index db59effc2f8..9a2e461d3c0 100644 --- a/packages/dev/s2-docs/pages/s2/ListView.mdx +++ b/packages/dev/s2-docs/pages/s2/ListView.mdx @@ -14,7 +14,7 @@ export const description = 'Displays a list of items with selection and actions. {docs.exports.ListView.description} -```tsx render docs={docs.exports.ListView} links={docs.links} props={['selectionMode', 'selectionStyle', 'isQuiet']} initialProps={{'aria-label': 'Files', selectionMode: 'multiple'}} type="s2" +```tsx render docs={docs.exports.ListView} links={docs.links} props={['selectionMode', 'selectionStyle', 'isQuiet']} initialProps={{'aria-label': 'Files', selectionMode: 'multiple'}} type="s2" wide import {ListView, ListViewItem, Text} from '@react-spectrum/s2'; import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; From 71bb52d754f6bfea1edf42ffc493255dad852d2f Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Tue, 24 Feb 2026 15:41:53 -0600 Subject: [PATCH 54/60] update docs content --- packages/dev/s2-docs/pages/s2/ListView.mdx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/dev/s2-docs/pages/s2/ListView.mdx b/packages/dev/s2-docs/pages/s2/ListView.mdx index 9a2e461d3c0..e512bfbfbdc 100644 --- a/packages/dev/s2-docs/pages/s2/ListView.mdx +++ b/packages/dev/s2-docs/pages/s2/ListView.mdx @@ -39,7 +39,7 @@ import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; ## Content -`ListView` follows the [Collection Components API](collections.html?component=ListView), accepting both static and dynamic collections. +`ListView` follows the [Collection Components API](collections.html?component=ListView), accepting both static and dynamic collections. This example shows a dynamic collection, passing a list of objects to the items prop, and a function to render the children. ```tsx render type="s2" "use client"; @@ -63,9 +63,7 @@ let files = [ ### Slots -`ListViewItem` supports icons, `Text`, [Image](Image) content, [ActionMenu](ActionMenu), and [ActionButtonGroup](ActionButtonGroup) as children. - -Use `textValue` when item content includes non-text children so typeahead and accessibility can still use a clear text label. +`ListViewItem` supports icons, `Text`, [Image](Image), [ActionMenu](ActionMenu), and [ActionButtonGroup](ActionButtonGroup) as children, and must have a `textValue` prop for accessibility. ```tsx render type="s2" "use client"; @@ -165,7 +163,7 @@ function AsyncListView() { ### Links -Use the `href` prop on a `ListViewItem` to create links. See the [getting started guide](getting-started) to learn how to integrate with your framework. +Use the `href` prop on a `ListViewItem` to create a link. See the [getting started guide](getting-started) to learn how to integrate with your framework. Link interactions vary depending on the selection behavior. See the [selection guide](selection) for more details. ```tsx render docs={docs.exports.ListView} links={docs.links} props={['selectionMode', 'isQuiet']} initialProps={{'aria-label': 'Bookmarks', selectionMode: 'none'}} type="s2" import {ListView, ListViewItem} from '@react-spectrum/s2'; @@ -212,7 +210,7 @@ import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; ## Selection and actions -Use the `selectionMode` prop to enable single or multiple selection. Use `selectionStyle="checkbox"` (default) for toggle selection with checkboxes, or `selectionStyle="highlight"` for row highlight selection behavior. The `onAction` event handles item actions. Items can be disabled with the `isDisabled` prop. See the [selection guide](selection?component=ListView) for more details. +Use the `selectionMode` prop to enable single or multiple selection. The selected items can be controlled via the `selectedKeys` prop, matching the `id` prop of the items. The `onAction` event handles item actions. Items can be disabled with the `isDisabled` prop. See the [selection guide](selection?component=ListView) for more details. ```tsx render docs={docs.exports.ListView} links={docs.links} props={['selectionMode', 'selectionStyle', 'isQuiet', 'disabledBehavior', 'disallowEmptySelection']} initialProps={{'aria-label': 'Files', selectionMode: 'multiple'}} type="s2" "use client"; From ac82942a1bf89fc0fc4cebb9e9570c13467b0502 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Tue, 24 Feb 2026 16:05:53 -0600 Subject: [PATCH 55/60] add ActionBar support --- packages/@react-spectrum/s2/src/ListView.tsx | 81 +++++++++----- .../s2/stories/ListView.stories.tsx | 103 +++++++++++++++++- packages/dev/s2-docs/pages/s2/ListView.mdx | 29 ++++- 3 files changed, 183 insertions(+), 30 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index f716a568313..564a638131f 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -29,6 +29,7 @@ import { GridListLoadMoreItem, GridListProps, GridListRenderProps, + Key, ListLayout, Provider, SlotProps, @@ -46,6 +47,7 @@ import {ImageContext} from './Image'; import intlMessages from '../intl/*.json'; import {ProgressCircle} from './ProgressCircle'; import {Text, TextContext} from './Content'; +import {useActionBarContainer} from './ActionBar'; import {useDOMRef} from '@react-spectrum/utils'; import {useLocale, useLocalizedStringFormatter} from 'react-aria'; import {useScale} from './utils'; @@ -59,7 +61,9 @@ export interface ListViewProps extends Omit, 'className' | ' /** Handler that is called when more items should be loaded, e.g. while scrolling near the bottom. */ onLoadMore?: () => void, /** The children of the ListView. */ - children: ReactNode | ((item: T) => ReactNode) + children: ReactNode | ((item: T) => ReactNode), + /** Provides the ActionBar to display when items are selected in the ListView. */ + renderActionBar?: (selectedKeys: 'all' | Set) => ReactElement } interface ListViewStylesProps { @@ -95,6 +99,17 @@ export const ListViewContext = createContext({}); +const listViewWrapper = style({ + minHeight: 0, + minWidth: 0, + display: 'flex', + isolation: 'isolate', + disableTapHighlight: true, + position: 'relative', + // Clip ActionBar animation. + overflow: 'clip' +}, getAllowedOverrides({height: true})); + const listView = style({ ...focusRing(), outlineOffset: { @@ -124,7 +139,7 @@ const listView = style({ isQuiet: 0 }, borderStyle: 'solid' -}, getAllowedOverrides({height: true})); +}); /** * A ListView displays a list of interactive items, and allows a user to navigate, select, or perform an action. @@ -145,6 +160,7 @@ export const ListView = /*#__PURE__*/ (forwardRef as forwardRefType)(function Li } let domRef = useDOMRef(ref); + let scrollRef = useRef(null); let isLoading = loadingState === 'loading' || loadingState === 'loadingMore'; let renderEmptyState: ListViewProps['renderEmptyState'] | undefined; @@ -198,30 +214,45 @@ export const ListView = /*#__PURE__*/ (forwardRef as forwardRefType)(function Li ); } + let {selectedKeys, onSelectionChange, actionBar, actionBarHeight} = useActionBarContainer({...props, scrollRef}); + return ( - - - - (props.UNSAFE_className || '') + listView({ - ...renderProps, - isQuiet - }, props.styles)}> - {wrappedChildren} - - - - +
      + + + + 0 ? actionBarHeight + 8 : 0, + scrollPaddingBottom: actionBarHeight > 0 ? actionBarHeight + 8 : 0 + }} + className={(renderProps) => listView({ + ...renderProps, + isQuiet + })} + selectedKeys={selectedKeys} + defaultSelectedKeys={undefined} + onSelectionChange={onSelectionChange}> + {wrappedChildren} + + + + + {actionBar} +
      ); }); diff --git a/packages/@react-spectrum/s2/stories/ListView.stories.tsx b/packages/@react-spectrum/s2/stories/ListView.stories.tsx index c3d01926c54..9860af0b1d4 100644 --- a/packages/@react-spectrum/s2/stories/ListView.stories.tsx +++ b/packages/@react-spectrum/s2/stories/ListView.stories.tsx @@ -11,9 +11,10 @@ */ import {action} from 'storybook/actions'; -import {ActionButton, ActionButtonGroup, ActionMenu, Breadcrumb, Breadcrumbs, Content, Heading, IllustratedMessage, Image, ListView, ListViewItem, MenuItem, Text} from '../'; +import {ActionBar, ActionButton, ActionButtonGroup, ActionMenu, Breadcrumb, Breadcrumbs, Content, Heading, IllustratedMessage, Image, ListView, ListViewItem, MenuItem, Text} from '../'; import {categorizeArgTypes} from './utils'; import {chain} from '@react-aria/utils'; +import Copy from '../s2wf-icons/S2_Icon_Copy_20_N.svg'; import Delete from '../s2wf-icons/S2_Icon_Delete_20_N.svg'; import Edit from '../s2wf-icons/S2_Icon_Edit_20_N.svg'; import File from '../s2wf-icons/S2_Icon_File_20_N.svg'; @@ -488,3 +489,103 @@ export const Navigation: Story = { render: () => , name: 'hasChildItems navigation' }; + +function ActionBarExample(props) { + let [selectedKeys, setSelectedKeys] = useState(new Set()); + + return ( + { + let selection = keys === 'all' ? 'all' : [...keys].join(', '); + return ( + + action('edit')(selection)}> + + Edit + + action('copy')(selection)}> + + Copy + + action('delete')(selection)}> + + Delete + + + ); + }} + styles={style({height: 320})} + {...props}> + {(item: Item) => ( + + {item.type === 'folder' ? : } + {item.name} + + )} + + ); +} + +export const WithActionBar: Story = { + render: (args) => , + args: { + 'aria-label': 'Files with action bar' + }, + name: 'with ActionBar' +}; + +function ActionBarEmphasizedExample(props) { + let [selectedKeys, setSelectedKeys] = useState(new Set()); + + return ( + { + let selection = keys === 'all' ? 'all' : [...keys].join(', '); + return ( + + action('edit')(selection)}> + + Edit + + action('copy')(selection)}> + + Copy + + action('delete')(selection)}> + + Delete + + + ); + }} + styles={style({height: 320})} + {...props}> + {(item: Item) => ( + + {item.type === 'folder' ? : } + {item.name} + + )} + + ); +} + +export const WithActionBarEmphasized: Story = { + render: (args) => , + args: { + 'aria-label': 'Files with emphasized action bar' + }, + name: 'with ActionBar (emphasized)' +}; diff --git a/packages/dev/s2-docs/pages/s2/ListView.mdx b/packages/dev/s2-docs/pages/s2/ListView.mdx index e512bfbfbdc..b6b516d79c9 100644 --- a/packages/dev/s2-docs/pages/s2/ListView.mdx +++ b/packages/dev/s2-docs/pages/s2/ListView.mdx @@ -1,7 +1,7 @@ import {Layout} from '../../src/Layout'; export default Layout; -import {ListView, ListViewItem, Text, ActionMenu, MenuItem, ActionButton, ActionButtonGroup, Image, IllustratedMessage, Heading, Content, Link, Breadcrumbs, Breadcrumb} from '@react-spectrum/s2'; +import {ListView, ListViewItem, Text, ActionMenu, MenuItem, ActionButton, ActionButtonGroup, ActionBar, Image, IllustratedMessage, Heading, Content, Link, Breadcrumbs, Breadcrumb} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; export const tags = ['list', 'data', 'collection', 'table', 'grid']; @@ -210,11 +210,14 @@ import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; ## Selection and actions -Use the `selectionMode` prop to enable single or multiple selection. The selected items can be controlled via the `selectedKeys` prop, matching the `id` prop of the items. The `onAction` event handles item actions. Items can be disabled with the `isDisabled` prop. See the [selection guide](selection?component=ListView) for more details. +Use the `selectionMode` prop to enable single or multiple selection. The selected items can be controlled via the `selectedKeys` prop, matching the `id` prop of the items. Return an [ActionBar](ActionBar) from `renderActionBar` to handle bulk actions, and use `onAction` for item navigation. Items can be disabled with the `isDisabled` prop. See the [selection guide](selection?component=ListView) for more details. ```tsx render docs={docs.exports.ListView} links={docs.links} props={['selectionMode', 'selectionStyle', 'isQuiet', 'disabledBehavior', 'disallowEmptySelection']} initialProps={{'aria-label': 'Files', selectionMode: 'multiple'}} type="s2" "use client"; -import {ListView, ListViewItem, type Selection} from '@react-spectrum/s2'; +import {ListView, ListViewItem, ActionBar, ActionButton, Text, type Selection} 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'}; import {useState} from 'react'; @@ -232,8 +235,26 @@ function Example(props) { selectedKeys={selected} onSelectionChange={setSelected} onAction={key => alert(`Action on ${key}`)} + renderActionBar={(selectedKeys) => { ///- end highlight -/// - > + let selection = selectedKeys === 'all' ? 'all' : [...selectedKeys].join(', '); + return ( + + alert(`Edit ${selection}`)}> + + Edit + + alert(`Copy ${selection}`)}> + + Copy + + alert(`Delete ${selection}`)}> + + Delete + + + ); + }}> Brand guidelines.pdf Icon set.svg Homepage comp.fig From 72e15fc6f0d7dc9198f245df3bd417b02c0329ff Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Tue, 24 Feb 2026 16:21:10 -0600 Subject: [PATCH 56/60] fix gap for checkbox selection --- packages/@react-spectrum/s2/src/ListView.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index 564a638131f..15f3303d9cc 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -368,6 +368,7 @@ const listRowBackground = style Date: Tue, 24 Feb 2026 16:28:21 -0600 Subject: [PATCH 57/60] docs update --- packages/dev/s2-docs/pages/s2/ListView.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dev/s2-docs/pages/s2/ListView.mdx b/packages/dev/s2-docs/pages/s2/ListView.mdx index b6b516d79c9..dd029c338be 100644 --- a/packages/dev/s2-docs/pages/s2/ListView.mdx +++ b/packages/dev/s2-docs/pages/s2/ListView.mdx @@ -210,7 +210,7 @@ import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; ## Selection and actions -Use the `selectionMode` prop to enable single or multiple selection. The selected items can be controlled via the `selectedKeys` prop, matching the `id` prop of the items. Return an [ActionBar](ActionBar) from `renderActionBar` to handle bulk actions, and use `onAction` for item navigation. Items can be disabled with the `isDisabled` prop. See the [selection guide](selection?component=ListView) for more details. +Use the `selectionMode` prop to enable single or multiple selection. The selected items can be controlled via the `selectedKeys` prop, matching the `id` prop of the items. The `onAction` event handles item actions. Items can be disabled with the `isDisabled` prop. See the [selection guide](selection?component=ListView) for more details. ```tsx render docs={docs.exports.ListView} links={docs.links} props={['selectionMode', 'selectionStyle', 'isQuiet', 'disabledBehavior', 'disallowEmptySelection']} initialProps={{'aria-label': 'Files', selectionMode: 'multiple'}} type="s2" "use client"; From 8c47d52a9dc0d0f344ec84b7801aeccd00ea06bf Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Tue, 24 Feb 2026 16:34:10 -0600 Subject: [PATCH 58/60] style updates --- packages/@react-spectrum/s2/src/ListView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index 15f3303d9cc..955b81fb996 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -330,7 +330,7 @@ const listitem = style Date: Tue, 24 Feb 2026 16:49:03 -0600 Subject: [PATCH 59/60] remove renderer context --- packages/@react-spectrum/s2/src/ListView.tsx | 58 ++++++++------------ 1 file changed, 23 insertions(+), 35 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index 955b81fb996..081254ac65e 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2024 Adobe. All rights reserved. + * Copyright 2026 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 @@ -38,7 +38,7 @@ import { } from 'react-aria-components'; import Chevron from '../ui-icons/Chevron'; import {controlFont, getAllowedOverrides, StylesPropWithHeight, UnsafeStyles} from './style-utils' with {type: 'macro'}; -import {createContext, forwardRef, JSXElementConstructor, ReactElement, ReactNode, useContext, useRef} from 'react'; +import {createContext, forwardRef, ReactElement, ReactNode, useContext, useRef} from 'react'; import {DOMProps, DOMRef, DOMRefValue, forwardRefType, GlobalDOMAttributes, LoadingState} from '@react-types/shared'; import {edgeToText} from '../style/spectrum-theme' with {type: 'macro'}; import {IconContext} from './Icon'; @@ -90,11 +90,6 @@ export interface ListViewItemProps extends Omit ReactElement> -} -const ListViewRendererContext = createContext({}); - export const ListViewContext = createContext>, DOMRefValue>>(null); let InternalListViewContext = createContext<{isQuiet?: boolean, selectionStyle?: 'highlight' | 'checkbox', overflowMode?: 'wrap' | 'truncate', scale?: 'medium' | 'large'}>({}); @@ -154,11 +149,6 @@ export const ListView = /*#__PURE__*/ (forwardRef as forwardRefType)(function Li let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2'); let rowHeight = scale === 'large' ? 50 : 40; - let renderer; - if (typeof children === 'function') { - renderer = children; - } - let domRef = useDOMRef(ref); let scrollRef = useRef(null); @@ -227,29 +217,27 @@ export const ListView = /*#__PURE__*/ (forwardRef as forwardRefType)(function Li estimatedRowHeight: rowHeight, loaderHeight: 60 }}> - - - 0 ? actionBarHeight + 8 : 0, - scrollPaddingBottom: actionBarHeight > 0 ? actionBarHeight + 8 : 0 - }} - className={(renderProps) => listView({ - ...renderProps, - isQuiet - })} - selectedKeys={selectedKeys} - defaultSelectedKeys={undefined} - onSelectionChange={onSelectionChange}> - {wrappedChildren} - - - + + 0 ? actionBarHeight + 8 : 0, + scrollPaddingBottom: actionBarHeight > 0 ? actionBarHeight + 8 : 0 + }} + className={(renderProps) => listView({ + ...renderProps, + isQuiet + })} + selectedKeys={selectedKeys} + defaultSelectedKeys={undefined} + onSelectionChange={onSelectionChange}> + {wrappedChildren} + + {actionBar}
      From 18bd84f8eeecd09d8a3961d300c4da79a5538435 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Tue, 24 Feb 2026 17:54:04 -0600 Subject: [PATCH 60/60] merge selection for checkbox selection --- packages/@react-spectrum/s2/src/ListView.tsx | 22 +++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index 081254ac65e..84c239c97e5 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -244,6 +244,9 @@ export const ListView = /*#__PURE__*/ (forwardRef as forwardRefType)(function Li ); }); +const selectedBackground = colorMix('gray-25', 'gray-900', 7); +const selectedActiveBackground = colorMix('gray-25', 'gray-900', 10); + const listitem = style