diff --git a/packages/code-studio/src/styleguide/ListViews.tsx b/packages/code-studio/src/styleguide/ListViews.tsx index 685b2ee3e3..7fe0a64fce 100644 --- a/packages/code-studio/src/styleguide/ListViews.tsx +++ b/packages/code-studio/src/styleguide/ListViews.tsx @@ -15,8 +15,9 @@ import { RadioGroup, RadioItem, useSpectrumThemeProvider, + ListActionGroup, } from '@deephaven/components'; -import { vsAccount, vsPerson } from '@deephaven/icons'; +import { vsAccount, vsEdit, vsPerson, vsTrash } from '@deephaven/icons'; import { LIST_VIEW_ROW_HEIGHTS } from '@deephaven/utils'; import { generateNormalizedItems, sampleSectionIdAndClasses } from './utils'; @@ -90,6 +91,13 @@ export function ListViews(): JSX.Element { ); const [showIcons, setShowIcons] = useState(true); + const [lastActionKey, setLastActionKey] = useState(''); + const [lastActionItemKey, setLastActionItemKey] = useState(''); + + const onAction = useCallback((actionKey: ItemKey, itemKey: ItemKey): void => { + setLastActionKey(actionKey); + setLastActionItemKey(itemKey); + }, []); const onChange = useCallback((keys: 'all' | Iterable): void => { setSelectedKeys(keys); @@ -211,7 +219,7 @@ export function ListViews(): JSX.Element { - + + + + + + Edit + + + + + + Delete + + + } /> + {lastActionKey} {lastActionItemKey} diff --git a/packages/components/src/spectrum/ActionGroup.tsx b/packages/components/src/spectrum/ActionGroup.tsx new file mode 100644 index 0000000000..456b87769a --- /dev/null +++ b/packages/components/src/spectrum/ActionGroup.tsx @@ -0,0 +1,61 @@ +import { useMemo } from 'react'; +import { + ActionGroup as SpectrumActionGroup, + SpectrumActionGroupProps, +} from '@adobe/react-spectrum'; +import cl from 'classnames'; +import { ItemsOrPrimitiveChildren } from './shared'; +import { MultipleItemSelectionProps, wrapItemChildren } from './utils'; + +export type ActionGroupProps = Omit< + SpectrumActionGroupProps, + | 'children' + | 'selectedKeys' + | 'defaultSelectedKeys' + | 'disabledKeys' + | 'onSelectionChange' +> & + MultipleItemSelectionProps & { + children: ItemsOrPrimitiveChildren; + }; + +/** + * Augmented version of the Spectrum ActionGroup component that supports + * primitive item children. + */ +export function ActionGroup({ + defaultSelectedKeys, + disabledKeys, + children, + selectedKeys, + UNSAFE_className, + onChange, + onSelectionChange, + ...props +}: ActionGroupProps): JSX.Element { + const wrappedChildren = useMemo( + () => + typeof children === 'function' + ? children + : wrapItemChildren(children, null), + [children] + ); + + return ( + ['defaultSelectedKeys'] + } + disabledKeys={disabledKeys as SpectrumActionGroupProps['disabledKeys']} + selectedKeys={selectedKeys as SpectrumActionGroupProps['selectedKeys']} + onSelectionChange={onChange ?? onSelectionChange} + > + {wrappedChildren} + + ); +} + +export default ActionGroup; diff --git a/packages/components/src/spectrum/ActionMenu.tsx b/packages/components/src/spectrum/ActionMenu.tsx new file mode 100644 index 0000000000..b629a0de13 --- /dev/null +++ b/packages/components/src/spectrum/ActionMenu.tsx @@ -0,0 +1,48 @@ +import { useMemo } from 'react'; +import { + ActionMenu as SpectrumActionMenu, + SpectrumActionMenuProps, +} from '@adobe/react-spectrum'; +import cl from 'classnames'; +import { ItemsOrPrimitiveChildren } from './shared'; +import { ItemKey, wrapItemChildren } from './utils'; + +export type ActionMenuProps = Omit< + SpectrumActionMenuProps, + 'children' | 'disabledKeys' +> & { + disabledKeys?: Iterable; + children: ItemsOrPrimitiveChildren; +}; + +/** + * Augmented version of the Spectrum ActionMenu component that supports + * primitive item children. + */ +export function ActionMenu({ + disabledKeys, + children, + UNSAFE_className, + ...props +}: ActionMenuProps): JSX.Element { + const wrappedChildren = useMemo( + () => + typeof children === 'function' + ? children + : wrapItemChildren(children, null), + [children] + ); + + return ( + ['disabledKeys']} + > + {wrappedChildren} + + ); +} + +export default ActionMenu; diff --git a/packages/components/src/spectrum/ListActionGroup.tsx b/packages/components/src/spectrum/ListActionGroup.tsx new file mode 100644 index 0000000000..d1882ad56e --- /dev/null +++ b/packages/components/src/spectrum/ListActionGroup.tsx @@ -0,0 +1,27 @@ +import { ActionGroupProps } from './ActionGroup'; +import { ItemKey, ItemSelection } from './utils'; + +export interface ListActionGroupProps + extends Omit< + ActionGroupProps, + 'onAction' | 'onChange' | 'onSelectionChange' + > { + /** + * Handler that is called when an item is pressed. + */ + onAction: (actionKey: ItemKey, listItemKey: ItemKey) => void; + + /** + * Handler that is called when the selection change. + */ + onChange?: (selection: ItemSelection, listItemKey: ItemKey) => void; +} + +/** + * This component doesn't actually render anything. It is a prop container that + * gets passed to `NormalizedListView`. The actual `ActionGroup` elements will + * be created from this component's props on each item in the list view. + */ +export function ListActionGroup(_props: ListActionGroupProps): null { + return null; +} diff --git a/packages/components/src/spectrum/ListActionMenu.tsx b/packages/components/src/spectrum/ListActionMenu.tsx new file mode 100644 index 0000000000..c5ebfa4191 --- /dev/null +++ b/packages/components/src/spectrum/ListActionMenu.tsx @@ -0,0 +1,24 @@ +import { ActionMenuProps } from './ActionMenu'; +import { ItemKey } from './utils'; + +export interface ListActionMenuProps + extends Omit, 'onAction' | 'onOpenChange'> { + /** + * Handler that is called when an item is pressed. + */ + onAction: (actionKey: ItemKey, listItemKey: ItemKey) => void; + + /** + * Handler that is called when the the menu is opened or closed. + */ + onOpenChange?: (isOpen: boolean, listItemKey: ItemKey) => void; +} + +/** + * This component doesn't actually render anything. It is a prop container that + * gets passed to `NormalizedListView`. The actual `ActionMenu` elements will + * be created from this component's props on each item in the list view. + */ +export function ListActionMenu(_props: ListActionMenuProps): null { + return null; +} diff --git a/packages/components/src/spectrum/buttons.ts b/packages/components/src/spectrum/buttons.ts index 1603d77e18..0a8b8d491a 100644 --- a/packages/components/src/spectrum/buttons.ts +++ b/packages/components/src/spectrum/buttons.ts @@ -1,8 +1,6 @@ export { ActionButton, type SpectrumActionButtonProps as ActionButtonProps, - ActionGroup, - type SpectrumActionGroupProps as ActionGroupProps, LogicButton, type SpectrumLogicButtonProps as LogicButtonProps, ToggleButton, diff --git a/packages/components/src/spectrum/collections.ts b/packages/components/src/spectrum/collections.ts index a1d502975c..c60372d216 100644 --- a/packages/components/src/spectrum/collections.ts +++ b/packages/components/src/spectrum/collections.ts @@ -1,8 +1,6 @@ export { ActionBar, type SpectrumActionBarProps as ActionBarProps, - ActionMenu, - type SpectrumActionMenuProps as ActionMenuProps, MenuTrigger, type SpectrumMenuTriggerProps as MenuTriggerProps, TagGroup, diff --git a/packages/components/src/spectrum/index.ts b/packages/components/src/spectrum/index.ts index 79dee1031d..3ba45b3e04 100644 --- a/packages/components/src/spectrum/index.ts +++ b/packages/components/src/spectrum/index.ts @@ -17,6 +17,10 @@ export * from './status'; /** * Custom DH components wrapping React Spectrum components. */ +export * from './ActionMenu'; +export * from './ActionGroup'; +export * from './ListActionGroup'; +export * from './ListActionMenu'; export * from './listView'; export * from './picker'; export * from './Heading'; diff --git a/packages/components/src/spectrum/listView/ListView.tsx b/packages/components/src/spectrum/listView/ListView.tsx index 1bb75d8f73..21b68c752e 100644 --- a/packages/components/src/spectrum/listView/ListView.tsx +++ b/packages/components/src/spectrum/listView/ListView.tsx @@ -3,8 +3,7 @@ import { SpectrumListViewProps } from '@adobe/react-spectrum'; import cl from 'classnames'; import { EMPTY_FUNCTION } from '@deephaven/utils'; import { - ItemKey, - ItemSelection, + MultipleItemSelectionProps, NormalizedItem, normalizeTooltipOptions, TooltipOptions, @@ -13,38 +12,22 @@ import { import { ListViewWrapper, ListViewWrapperProps } from './ListViewWrapper'; import { ItemElementOrPrimitive } from '../shared'; -export type ListViewProps = { +export type ListViewProps = MultipleItemSelectionProps & { children: ItemElementOrPrimitive | ItemElementOrPrimitive[]; /** Can be set to true or a TooltipOptions to enable item tooltips */ tooltip?: boolean | TooltipOptions; - selectedKeys?: 'all' | Iterable; - defaultSelectedKeys?: 'all' | Iterable; - disabledKeys?: Iterable; - /** - * Handler that is called when the selection change. - * Note that under the hood, this is just an alias for Spectrum's - * `onSelectionChange`. We are renaming for better consistency with other - * components. - */ - onChange?: (keys: ItemSelection) => void; /** Handler that is called when the picker is scrolled. */ onScroll?: (event: Event) => void; - - /** - * Handler that is called when the selection changes. - * @deprecated Use `onChange` instead - */ - onSelectionChange?: (keys: ItemSelection) => void; } & Omit< - SpectrumListViewProps, - | 'children' - | 'items' - | 'selectedKeys' - | 'defaultSelectedKeys' - | 'disabledKeys' - | 'onSelectionChange' ->; + SpectrumListViewProps, + | 'children' + | 'items' + | 'selectedKeys' + | 'defaultSelectedKeys' + | 'disabledKeys' + | 'onSelectionChange' + >; export function ListView({ children, diff --git a/packages/components/src/spectrum/listView/ListViewNormalized.tsx b/packages/components/src/spectrum/listView/ListViewNormalized.tsx index e2efe3ecaa..e5301bccb6 100644 --- a/packages/components/src/spectrum/listView/ListViewNormalized.tsx +++ b/packages/components/src/spectrum/listView/ListViewNormalized.tsx @@ -1,6 +1,7 @@ import { useMemo } from 'react'; import cl from 'classnames'; import { + ListActions, NormalizedItem, normalizeTooltipOptions, useRenderNormalizedItem, @@ -13,6 +14,7 @@ export interface ListViewNormalizedProps extends Omit { normalizedItems: NormalizedItem[]; showItemIcons: boolean; + actions?: ListActions; } /** @@ -34,6 +36,7 @@ export function ListViewNormalized({ defaultSelectedKeys, disabledKeys, showItemIcons, + actions, UNSAFE_className, onChange, onSelectionChange, @@ -53,6 +56,7 @@ export function ListViewNormalized({ showItemDescriptions: false, showItemIcons, tooltipOptions, + actions, }); // Spectrum doesn't re-render if only the `renderNormalizedItems` function diff --git a/packages/components/src/spectrum/listView/ListViewWrapper.scss b/packages/components/src/spectrum/listView/ListViewWrapper.scss index 8fc00e8e04..4598068286 100644 --- a/packages/components/src/spectrum/listView/ListViewWrapper.scss +++ b/packages/components/src/spectrum/listView/ListViewWrapper.scss @@ -28,22 +28,37 @@ } .dh-list-view-wrapper-density-compact { - svg[class*='react-spectrum-ListViewItem-thumbnail'] { + // Ensure icons don't change the item height + svg[class*='spectrum-Icon'] { height: var(--dh-list-view-item-icon-compact); width: var(--dh-list-view-item-icon-compact); } + // Ensure action buttons don't change the item height + button { + height: var(--dh-list-view-item-icon-compact); + } } .dh-list-view-wrapper-density-regular { - svg[class*='react-spectrum-ListViewItem-thumbnail'] { + // Ensure icons don't change the item height + svg[class*='spectrum-Icon'] { height: var(--dh-list-view-item-icon-regular); width: var(--dh-list-view-item-icon-regular); } + // Ensure action buttons don't change the item height + button { + height: var(--dh-list-view-item-icon-regular); + } } .dh-list-view-wrapper-density-spacious { - svg[class*='react-spectrum-ListViewItem-thumbnail'] { + // Ensure icons don't change the item height + svg[class*='spectrum-Icon'] { height: var(--dh-list-view-item-icon-spacious); width: var(--dh-list-view-item-icon-spacious); } + // Ensure action buttons don't change the item height + button { + height: var(--dh-list-view-item-icon-spacious); + } } diff --git a/packages/components/src/spectrum/shared.ts b/packages/components/src/spectrum/shared.ts index 2014d9cd3c..ebe95706e5 100644 --- a/packages/components/src/spectrum/shared.ts +++ b/packages/components/src/spectrum/shared.ts @@ -27,14 +27,21 @@ export type ItemElementOrPrimitive = | boolean | ItemElement; +export type ItemsChildren = + | ItemElement + | ItemElement[] + | ItemRenderer; + +export type ItemsOrPrimitiveChildren = + | ItemElementOrPrimitive + | ItemElementOrPrimitive[] + | ItemRenderer; + /** * Spectrum SectionProps augmented with support for primitive item children. */ export type SectionProps = Omit, 'children'> & { - children: - | ItemElementOrPrimitive - | ItemElementOrPrimitive[] - | ItemRenderer; + children: ItemsOrPrimitiveChildren; }; /** diff --git a/packages/components/src/spectrum/utils/itemUtils.test.tsx b/packages/components/src/spectrum/utils/itemUtils.test.tsx index b04329c64f..3389b8c905 100644 --- a/packages/components/src/spectrum/utils/itemUtils.test.tsx +++ b/packages/components/src/spectrum/utils/itemUtils.test.tsx @@ -9,14 +9,13 @@ import { NormalizedItem, NormalizedSection, normalizeTooltipOptions, - ItemElementOrPrimitive, ItemOrSection, SectionElement, itemSelectionToStringSet, getPositionOfSelectedItemElement, isItemElementWithDescription, } from './itemUtils'; -import { Item, Section } from '../shared'; +import { Item, ItemElementOrPrimitive, Section } from '../shared'; import { Text } from '../Text'; import ItemContent from '../ItemContent'; @@ -29,9 +28,7 @@ describe('getItemKey', () => { [{ key: 'top-level.key', item: { key: 'item.key' } }, 'item.key'], [{ key: 'top-level.key', item: {} }, 'top-level.key'], [{ key: 'top-level.key' }, 'top-level.key'], - [{ item: { key: 'item.key' } }, 'item.key'], - [{}, undefined], - ] as NormalizedItem[])( + ] as [NormalizedItem, string][])( 'should return the item.key or fallback to the top-level key: %s, %s', (given, expected) => { const actual = getItemKey(given); @@ -224,8 +221,8 @@ describe('isItemOrSection', () => { describe('isNormalizedSection', () => { it.each([ - [{ item: {} } as NormalizedItem, false], - [{ item: { items: [] } } as NormalizedSection, true], + [{ key: 'mock.key', item: {} } as NormalizedItem, false], + [{ key: 'mock.key', item: { items: [] } } as NormalizedSection, true], ])('should return true for a normalized section: %s', (obj, expected) => { expect(isNormalizedSection(obj)).toBe(expected); }); diff --git a/packages/components/src/spectrum/utils/itemUtils.ts b/packages/components/src/spectrum/utils/itemUtils.ts index d58d2436c0..515f182bea 100644 --- a/packages/components/src/spectrum/utils/itemUtils.ts +++ b/packages/components/src/spectrum/utils/itemUtils.ts @@ -59,6 +59,26 @@ export type ItemSelection = SelectionT; */ export type ItemSelectionChangeHandler = (key: ItemKey) => void; +export interface MultipleItemSelectionProps { + selectedKeys?: 'all' | Iterable; + defaultSelectedKeys?: 'all' | Iterable; + disabledKeys?: Iterable; + + /** + * Handler that is called when the selection change. + * Note that under the hood, this is just an alias for Spectrum's + * `onSelectionChange`. We are renaming for better consistency with other + * components. + */ + onChange?: (keys: ItemSelection) => void; + + /** + * Handler that is called when the selection changes. + * @deprecated Use `onChange` instead + */ + onSelectionChange?: (keys: ItemSelection) => void; +} + export interface NormalizedItemData { key?: ItemKey; content: ReactNode; @@ -81,12 +101,9 @@ export interface NormalizedSectionData { * `KeyedItem` interface to be compatible with Windowed data utils * (e.g. `useViewportData`). */ -export type NormalizedItem = KeyedItem; +export type NormalizedItem = KeyedItem; -export type NormalizedSection = KeyedItem< - NormalizedSectionData, - Key | undefined ->; +export type NormalizedSection = KeyedItem; export type NormalizedItemOrSection = TItemOrSection extends SectionElement ? NormalizedSection : NormalizedItem; @@ -107,9 +124,9 @@ export type TooltipOptions = { placement: PopperOptions['placement'] }; export function getItemKey< TItem extends NormalizedItem | NormalizedSection, TKey extends TItem extends NormalizedItem - ? ItemKey | undefined + ? ItemKey : TItem extends NormalizedSection - ? Key | undefined + ? Key : undefined, >(item: TItem | null | undefined): TKey { return (item?.item?.key ?? item?.key) as TKey; diff --git a/packages/components/src/spectrum/utils/itemWrapperUtils.tsx b/packages/components/src/spectrum/utils/itemWrapperUtils.tsx index 9f571649fc..489ba26ff0 100644 --- a/packages/components/src/spectrum/utils/itemWrapperUtils.tsx +++ b/packages/components/src/spectrum/utils/itemWrapperUtils.tsx @@ -1,4 +1,4 @@ -import { cloneElement, ReactElement, ReactNode } from 'react'; +import { cloneElement, ReactNode } from 'react'; import { Item } from '@adobe/react-spectrum'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { dh as dhIcons } from '@deephaven/icons'; @@ -14,7 +14,6 @@ import { SectionElement, TooltipOptions, } from './itemUtils'; -import { ItemProps } from '../shared'; import { ItemContent } from '../ItemContent'; import { Icon } from '../icons'; import { Text } from '../Text'; @@ -53,10 +52,10 @@ export function wrapIcon( * @param tooltipOptions The tooltip options to use when wrapping items * @returns The wrapped items or sections */ -export function wrapItemChildren( - itemsOrSections: ItemOrSection | ItemOrSection[], +export function wrapItemChildren( + itemsOrSections: ItemOrSection | ItemOrSection[], tooltipOptions: TooltipOptions | null -): ItemElement | SectionElement | (ItemElement | SectionElement)[] { +): ItemElement | SectionElement | (ItemElement | SectionElement)[] { const itemsOrSectionsArray = ensureArray(itemsOrSections); const result = itemsOrSectionsArray.map(item => { @@ -92,10 +91,7 @@ export function wrapItemChildren( key: item.key ?? (typeof item.props.title === 'string' ? item.props.title : undefined), - children: wrapItemChildren( - item.props.children, - tooltipOptions - ) as ReactElement>[], + children: wrapItemChildren(item.props.children, tooltipOptions), }); } diff --git a/packages/components/src/spectrum/utils/useRenderNormalizedItem.test.tsx b/packages/components/src/spectrum/utils/useRenderNormalizedItem.test.tsx index e8fd30a033..c4a2c7b4cf 100644 --- a/packages/components/src/spectrum/utils/useRenderNormalizedItem.test.tsx +++ b/packages/components/src/spectrum/utils/useRenderNormalizedItem.test.tsx @@ -1,11 +1,16 @@ import React, { Key } from 'react'; import { Item } from '@adobe/react-spectrum'; import { renderHook } from '@testing-library/react-hooks'; +import { isElementOfType } from '@deephaven/react-hooks'; import { TestUtils } from '@deephaven/utils'; import { ItemContent } from '../ItemContent'; import { useRenderNormalizedItem } from './useRenderNormalizedItem'; import { getItemKey, NormalizedItem } from './itemUtils'; import { wrapIcon, wrapPrimitiveWithText } from './itemWrapperUtils'; +import { ListActionGroup } from '../ListActionGroup'; +import { ActionGroup } from '../ActionGroup'; +import { ListActionMenu } from '../ListActionMenu'; +import ActionMenu from '../ActionMenu'; jest.mock('./itemWrapperUtils'); @@ -16,19 +21,81 @@ beforeEach(() => { expect.hasAssertions(); }); +const onAction = jest.fn(); +const onChange = jest.fn(); +const onOpenChange = jest.fn(); + +const listActionGroup = ( + + Item 1 + +); + +const listActionMenu = ( + + Item 1 + +); + +const expectedActions = new Map([ + [undefined, null], + [ + listActionGroup, + // eslint-disable-next-line react/jsx-key + , + ], + [ + listActionMenu, + // eslint-disable-next-line react/jsx-key + , + ], +]); + describe.each([ - [true, true, null], - [true, true, { placement: 'top' }], - [true, false, null], - [true, false, { placement: 'top' }], - [false, true, null], - [false, true, { placement: 'top' }], - [false, false, null], - [false, false, { placement: 'top' }], + [true, true, null, undefined], + [true, true, { placement: 'top' }, undefined], + [true, false, null, undefined], + [true, false, { placement: 'top' }, undefined], + [false, true, null, undefined], + [false, true, { placement: 'top' }, undefined], + [false, false, null, undefined], + [false, false, { placement: 'top' }, undefined], + // ListActionGroup + [true, true, null, listActionGroup], + [true, true, { placement: 'top' }, listActionGroup], + [true, false, null, listActionGroup], + [true, false, { placement: 'top' }, listActionGroup], + [false, true, null, listActionGroup], + [false, true, { placement: 'top' }, listActionGroup], + [false, false, null, listActionGroup], + [false, false, { placement: 'top' }, listActionGroup], + // ListActionMenu + [true, true, null, listActionMenu], + [true, true, { placement: 'top' }, listActionMenu], + [true, false, null, listActionMenu], + [true, false, { placement: 'top' }, listActionMenu], + [false, true, null, listActionMenu], + [false, true, { placement: 'top' }, listActionMenu], + [false, false, null, listActionMenu], + [false, false, { placement: 'top' }, listActionMenu], ] as const)( 'useRenderNormalizedItem: %s, %s, %s', - (showItemIcons, showItemDescriptions, tooltipOptions) => { + (showItemIcons, showItemDescriptions, tooltipOptions, actions) => { beforeEach(() => { + asMock(onAction).mockName('onAction'); + asMock(onChange).mockName('onChange'); + asMock(onOpenChange).mockName('onOpenChange'); + asMock(wrapIcon).mockImplementation((a, b) => `wrapIcon(${a}, ${b})`); asMock(wrapPrimitiveWithText).mockImplementation( (a, b) => `wrapPrimitiveWithText(${a}, ${b})` @@ -87,6 +154,7 @@ describe.each([ showItemDescriptions, showItemIcons, tooltipOptions, + actions, }) ); @@ -106,15 +174,42 @@ describe.each([ ); } + const itemKey = getItemKey(normalizedItem) as Key; + expect(actual).toEqual( - + {showItemIcons ? icon : null} {content} {showItemDescriptions ? description : null} + {expectedActions.get(actions)} ); + + if (actions === listActionGroup) { + const actionGroup = actual.props.children.props.children[3]; + expect(isElementOfType(actionGroup, ActionGroup)).toBe(true); + + const actionKey = 'actionKey'; + actionGroup.props.onAction(actionKey); + expect(onAction).toHaveBeenCalledWith(actionKey, itemKey); + + const actionKeys = ['actionKey1', 'actionKey2']; + actionGroup.props.onChange(actionKeys); + expect(onChange).toHaveBeenCalledWith(actionKeys, itemKey); + } else if (actions === listActionMenu) { + const actionMenu = actual.props.children.props.children[3]; + expect(isElementOfType(actionMenu, ActionMenu)).toBe(true); + + const actionKey = 'actionKey'; + actionMenu.props.onAction(actionKey); + expect(onAction).toHaveBeenCalledWith(actionKey, itemKey); + + const isOpen = true; + actionMenu.props.onOpenChange(isOpen); + expect(onOpenChange).toHaveBeenCalledWith(isOpen, itemKey); + } } ); } diff --git a/packages/components/src/spectrum/utils/useRenderNormalizedItem.tsx b/packages/components/src/spectrum/utils/useRenderNormalizedItem.tsx index 07b0b9ebc8..74258085c1 100644 --- a/packages/components/src/spectrum/utils/useRenderNormalizedItem.tsx +++ b/packages/components/src/spectrum/utils/useRenderNormalizedItem.tsx @@ -1,5 +1,10 @@ -import { Key, useCallback } from 'react'; +import { isElementOfType } from '@deephaven/react-hooks'; +import { Key, ReactElement, useCallback } from 'react'; +import ActionGroup from '../ActionGroup'; +import ActionMenu from '../ActionMenu'; import { ItemContent } from '../ItemContent'; +import { ListActionGroup, ListActionGroupProps } from '../ListActionGroup'; +import { ListActionMenu, ListActionMenuProps } from '../ListActionMenu'; import { Item } from '../shared'; import { getItemKey, @@ -10,11 +15,16 @@ import { } from './itemUtils'; import { wrapIcon, wrapPrimitiveWithText } from './itemWrapperUtils'; +export type ListActions = + | ReactElement> + | ReactElement>; + export interface UseRenderNormalizedItemOptions { itemIconSlot: ItemIconSlot; showItemDescriptions: boolean; showItemIcons: boolean; tooltipOptions: TooltipOptions | null; + actions?: ListActions; } /** @@ -24,6 +34,7 @@ export interface UseRenderNormalizedItemOptions { * @param showItemDescriptions Whether to show item descriptions * @param showItemIcons Whether to show item icons * @param tooltipOptions Tooltip options to use when rendering the item + * @param actions Optional actions to render with the item * @returns Render function for normalized items */ export function useRenderNormalizedItem({ @@ -31,12 +42,13 @@ export function useRenderNormalizedItem({ showItemDescriptions, showItemIcons, tooltipOptions, + actions, }: UseRenderNormalizedItemOptions): ( normalizedItem: NormalizedItem ) => JSX.Element { return useCallback( (normalizedItem: NormalizedItem) => { - const key = getItemKey(normalizedItem); + const itemKey = getItemKey(normalizedItem); const content = wrapPrimitiveWithText(normalizedItem.item?.content); const textValue = normalizedItem.item?.textValue ?? ''; @@ -48,6 +60,30 @@ export function useRenderNormalizedItem({ ? wrapIcon(normalizedItem.item?.icon, itemIconSlot) : null; + let action = null; + + if (isElementOfType(actions, ListActionGroup)) { + action = ( + actions.props.onAction(key, itemKey)} + onChange={keys => actions.props.onChange?.(keys, itemKey)} + /> + ); + } else if (isElementOfType(actions, ListActionMenu)) { + action = ( + actions.props.onAction(key, itemKey)} + onOpenChange={isOpen => + actions.props.onOpenChange?.(isOpen, itemKey) + } + /> + ); + } + return ( ` // elements that back the Spectrum Picker. These are not visible in the UI, // but are used for accessibility purposes, so we set to an arbitrary @@ -70,11 +106,12 @@ export function useRenderNormalizedItem({ {icon} {content} {description} + {action} ); }, - [itemIconSlot, showItemDescriptions, showItemIcons, tooltipOptions] + [actions, itemIconSlot, showItemDescriptions, showItemIcons, tooltipOptions] ); } diff --git a/tests/styleguide.spec.ts-snapshots/list-views-chromium-linux.png b/tests/styleguide.spec.ts-snapshots/list-views-chromium-linux.png index 3d39ecd0cd..7473634456 100644 Binary files a/tests/styleguide.spec.ts-snapshots/list-views-chromium-linux.png and b/tests/styleguide.spec.ts-snapshots/list-views-chromium-linux.png differ diff --git a/tests/styleguide.spec.ts-snapshots/list-views-firefox-linux.png b/tests/styleguide.spec.ts-snapshots/list-views-firefox-linux.png index 84d3365972..40c0b82fa0 100644 Binary files a/tests/styleguide.spec.ts-snapshots/list-views-firefox-linux.png and b/tests/styleguide.spec.ts-snapshots/list-views-firefox-linux.png differ diff --git a/tests/styleguide.spec.ts-snapshots/list-views-webkit-linux.png b/tests/styleguide.spec.ts-snapshots/list-views-webkit-linux.png index 8ab5880137..101486f317 100644 Binary files a/tests/styleguide.spec.ts-snapshots/list-views-webkit-linux.png and b/tests/styleguide.spec.ts-snapshots/list-views-webkit-linux.png differ