diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx new file mode 100644 index 00000000000..ed6871eaac3 --- /dev/null +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -0,0 +1,313 @@ +/* + * 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 {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, + GridListItemProps, + GridListItemRenderProps, + GridListProps, + GridListRenderProps, + ListLayout, + Provider, + SlotProps, + Virtualizer +} 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 {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'; + +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. + */ + autoFocus?: boolean, + children: ReactNode | ((item: T) => ReactNode) +} + +interface ListViewStylesProps { + isQuiet?: boolean, + isEmphasized?: boolean, + selectionStyle?: 'highlight' | 'checkbox', + highlightMode?: 'normal' | 'inverse' +} + +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, isEmphasized?: boolean, highlightMode?: 'normal' | 'inverse'}>({}); + +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({height: true})); + +export const ListView = /*#__PURE__*/ (forwardRef as forwardRefType)(function ListView( + props: ListViewProps, + ref: DOMRef +) { + [props, ref] = useSpectrumContextProps(props, ref, ListViewContext); + let {children, isQuiet, isEmphasized, highlightMode} = 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', + isHovered: 'gray-100', + isSelected: 'gray-100', + highlightMode: { + normal: { + isEmphasized: { + isSelected: 'blue-900/10' + } + }, + inverse: { + isEmphasized: { + isSelected: 'blue-800' + } + } + } + }, + color: { + default: baseColor('neutral-subdued'), + isHovered: 'gray-800', + isSelected: { + highlightMode: { + normal: 'gray-900', + inverse: 'gray-25' + } + }, + isDisabled: { + default: 'disabled', + forcedColors: 'GrayText' + } + }, + position: 'relative', + gridColumnStart: 1, + gridColumnEnd: -1, + display: 'grid', + gridTemplateAreas: [ + '. checkmark icon label actions chevron .', + '. . . description actions chevron .' + ], + gridTemplateColumns: [edgeToText(40), 'auto', 'auto', 'minmax(0, 1fr)', 'auto', 'auto', edgeToText(40)], + 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' +}, getAllowedOverrides()); + +export let label = style({ + gridArea: 'label', + alignSelf: 'center', + font: controlFont(), + color: 'inherit', + fontWeight: { + default: 'normal', + isSelected: 'bold' + }, + // TODO: token values for padding not defined yet, revisit + marginTop: '--labelPadding', + truncate: true +}); + +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', + marginStart: 'text-to-visual' +}); + +export function ListViewItem(props: ListViewItemProps): ReactNode { + let ref = useRef(null); + let isLink = props.href != null; + // let isLinkOut = isLink && props.target === '_blank'; + let {isQuiet, isEmphasized, highlightMode} = useContext(InternalListViewContext); + let textValue = props.textValue || (typeof props.children === 'string' ? props.children : undefined); + // let {direction} = useLocale(); + return ( + (props.UNSAFE_className || '') + listitem({ + ...renderProps, + isLink, + isQuiet, + isEmphasized, + highlightMode + }, 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 21b9ecf27c3..0c61db575d0 100644 --- a/packages/@react-spectrum/s2/src/Picker.tsx +++ b/packages/@react-spectrum/s2/src/Picker.tsx @@ -33,7 +33,7 @@ import { Virtualizer } from 'react-aria-components'; import {AsyncLoadable, FocusableRef, FocusableRefValue, GlobalDOMAttributes, HelpTextProps, LoadingState, PressEvent, RefObject, SpectrumLabelableProps} from '@react-types/shared'; -import {baseColor, edgeToText, focusRing, style} from '../style' with {type: 'macro'}; +import {baseColor, focusRing, style} from '../style' with {type: 'macro'}; import {box, iconStyles as checkboxIconStyles} from './Checkbox'; import {centerBaseline} from './CenterBaseline'; import { @@ -47,7 +47,7 @@ import { } from './Menu'; import CheckmarkIcon from '../ui-icons/Checkmark'; import ChevronIcon from '../ui-icons/Chevron'; -import {control, controlBorderRadius, controlFont, field, fieldInput, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; +import {control, controlBorderRadius, field, fieldInput, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; import {createHideableComponent} from '@react-aria/collections'; import { Divider, @@ -184,27 +184,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/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index 6057c98d6e6..a7ceceb4ef0 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,10 @@ 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, + highlightMode?: 'normal' | 'inverse' } // TODO: Note that loadMore and loadingState are now on the Table instead of on the TableBody @@ -282,6 +286,9 @@ export const TableView = forwardRef(function TableView(props: TableViewProps, re onResizeEnd: propsOnResizeEnd, onAction, onLoadMore, + selectionStyle = 'checkbox', + isEmphasized = false, + highlightMode = 'normal', ...otherProps } = props; @@ -306,11 +313,14 @@ export const TableView = forwardRef(function TableView(props: TableViewProps, re loadingState, onLoadMore, isInResizeMode, - setIsInResizeMode - }), [isQuiet, density, overflowMode, loadingState, onLoadMore, isInResizeMode, setIsInResizeMode]); + setIsInResizeMode, + selectionStyle, + isEmphasized, + highlightMode + }), [isQuiet, density, overflowMode, loadingState, onLoadMore, isInResizeMode, setIsInResizeMode, selectionStyle, isEmphasized, highlightMode]); 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}); @@ -352,9 +362,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} /> @@ -872,7 +882,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 +891,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 @@ -977,6 +987,10 @@ const checkboxCellStyle = style({ const cellContent = style({ truncate: true, + '--iconPrimary': { + type: 'fill', + value: 'currentColor' + }, whiteSpace: { default: 'nowrap', overflowMode: { @@ -1000,9 +1014,26 @@ 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' + } + } + } } }); @@ -1036,15 +1067,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 +1102,40 @@ 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', + highlightMode: { + normal: { + isEmphasized: 'blue-900/10' + }, + inverse: { + isEmphasized: 'blue-800' + } + } + }, + forcedColors: { + default: 'Background' + } + } } } as const; @@ -1117,7 +1189,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 +1222,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 5d1f0c26e5d..487da480938 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, @@ -32,9 +33,10 @@ 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, forwardRefType, GlobalDOMAttributes, Key, LoadingState} from '@react-types/shared'; +import {DOMRef, DOMRefValue, 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'; @@ -44,6 +46,7 @@ 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 { // Only detatched is supported right now with the current styles from Spectrum @@ -53,7 +56,10 @@ interface S2TreeProps { /** Handler that is called when a user performs an action on a row. */ onAction?: (key: Key) => 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', + highlightMode?: 'normal' | 'inverse' } export interface TreeViewProps extends Omit, 'style' | 'className' | 'onRowAction' | 'selectionBehavior' | 'onScroll' | 'onCellAction' | 'dragAndDropHooks' | keyof GlobalDOMAttributes>, UnsafeStyles, S2TreeProps { @@ -76,8 +82,10 @@ interface TreeRendererContextValue { } const TreeRendererContext = createContext({}); +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', 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 @@ -108,7 +116,8 @@ 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) { - let {children, isDetached, isEmphasized, UNSAFE_className, UNSAFE_style} = props; + [props, ref] = useSpectrumContextProps(props, ref, TreeViewContext); + let {children, isDetached, isEmphasized, selectionStyle = 'checkbox', selectionCornerStyle = 'round', UNSAFE_className, UNSAFE_style, highlightMode = 'normal'} = props; let scale = useScale(); let renderer; @@ -126,12 +135,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} @@ -145,28 +154,50 @@ 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: { + highlightMode: { + normal: 'blue-900/10', + inverse: 'blue-800/85' + } + } + }, + forcedColors: { + default: 'Background' + } } - }, - forcedColors: { - default: 'Background' } } as const; @@ -208,14 +239,40 @@ 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: { + highlightMode: { + normal: 'gray-900', + inverse: 'gray-25' + } + }, 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: 'bold' + }, '--rowSelectedBorderColor': { type: 'outlineColor', value: { @@ -248,6 +305,27 @@ const treeCellGrid = style({ } }); +const treeRowBackground = style({ + position: 'absolute', + zIndex: -1, + inset: 0, + backgroundColor: '--rowBackgroundColor', + borderTopStartRadius: { + isRoundTop: 'default' + }, + borderTopEndRadius: { + isRoundTop: 'default' + }, + borderBottomStartRadius: { + isRoundBottom: 'default' + }, + borderBottomEndRadius: { + isRoundBottom: 'default' + }, + borderWidth: 0, + borderStyle: 'solid' +}); + const treeCheckbox = style({ gridArea: 'checkbox', marginStart: 12, @@ -264,6 +342,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', @@ -312,15 +405,18 @@ export const TreeViewItem = (props: TreeViewItemProps): ReactNode => { let { href } = props; - let {isDetached, isEmphasized} = useContext(InternalTreeContext); + let {isDetached, isEmphasized, selectionStyle, highlightMode} = useContext(InternalTreeContext); return ( treeRow({ ...renderProps, - isLink: !!href, isEmphasized - }) + (renderProps.isFocusVisible && !isDetached ? ' ' + treeRowFocusIndicator : '')} /> + isLink: !!href, + isEmphasized, + selectionStyle, + highlightMode + }) + (renderProps.isFocusVisible && !isDetached && selectionStyle !== 'highlight' ? ' ' + treeRowFocusIndicator : '')} /> ); }; @@ -333,21 +429,34 @@ export const TreeViewItemContent = (props: TreeViewItemContentProps): ReactNode let { children } = props; - let {isDetached, isEmphasized} = useContext(InternalTreeContext); + let {isDetached, isEmphasized, selectionStyle, selectionCornerStyle, highlightMode} = 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; + 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?
@@ -365,12 +474,13 @@ 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 bccfb56a2f3..1cb987329b3 100644 --- a/packages/@react-spectrum/s2/src/index.ts +++ b/packages/@react-spectrum/s2/src/index.ts @@ -57,6 +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 {MenuItem, MenuTrigger, Menu, MenuSection, SubmenuTrigger, MenuContext} from './Menu'; export {Meter, MeterContext} from './Meter'; export {NotificationBadge, NotificationBadgeContext} from './NotificationBadge'; @@ -87,7 +88,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, TreeViewLoadMoreItem} from './TreeView'; +export {TreeView, TreeViewItem, TreeViewItemContent, TreeViewContext, TreeViewLoadMoreItem} from './TreeView'; export {pressScale} from './pressScale'; @@ -138,6 +139,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/HighlightSelectionList.stories.tsx b/packages/@react-spectrum/s2/stories/HighlightSelectionList.stories.tsx new file mode 100644 index 00000000000..aa6cae100e3 --- /dev/null +++ b/packages/@react-spectrum/s2/stories/HighlightSelectionList.stories.tsx @@ -0,0 +1,90 @@ +/** + * 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', + highlightMode: 'inverse', + isEmphasized: true + } +}; 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..09e76f893a2 --- /dev/null +++ b/packages/@react-spectrum/s2/stories/HighlightSelectionTable.stories.tsx @@ -0,0 +1,122 @@ +/** + * 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', + highlightMode: 'inverse', + isEmphasized: true + } +}; diff --git a/packages/@react-spectrum/s2/stories/HighlightSelectionTree.stories.tsx b/packages/@react-spectrum/s2/stories/HighlightSelectionTree.stories.tsx new file mode 100644 index 00000000000..057290d74ff --- /dev/null +++ b/packages/@react-spectrum/s2/stories/HighlightSelectionTree.stories.tsx @@ -0,0 +1,273 @@ +/** + * 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', + highlightMode: 'inverse', + 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', + highlightMode: 'inverse', + isEmphasized: true + } +}; 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..e30795267ba --- /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 {ActionButton, ActionButtonGroup, Image, ListView, ListViewItem, Text} from '../'; +import {categorizeArgTypes} from './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 {Key} from 'react-aria'; +import type {Meta, StoryObj} from '@storybook/react'; +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-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 diff --git a/packages/react-aria-components/src/GridList.tsx b/packages/react-aria-components/src/GridList.tsx index d3a87620b09..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. */ @@ -345,6 +348,8 @@ export const GridListItem = /*#__PURE__*/ createLeafComponent(ItemNode, function defaultClassName: 'react-aria-GridListItem', values: { ...states, + isFirstItem: item.key === state.collection.getFirstKey(), + isLastItem: item.key === state.collection.getLastKey(), isHovered, isFocusVisible, selectionMode: state.selectionManager.selectionMode,