Skip to content

Commit 709d10d

Browse files
committed
List action support in ListViewNormalized (#1913)
1 parent ddc87dc commit 709d10d

9 files changed

Lines changed: 73 additions & 48 deletions

File tree

packages/code-studio/src/styleguide/ListViews.tsx

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ import {
1515
RadioGroup,
1616
RadioItem,
1717
useSpectrumThemeProvider,
18+
ListActionGroup,
1819
} from '@deephaven/components';
19-
import { vsAccount, vsPerson } from '@deephaven/icons';
20+
import { vsAccount, vsEdit, vsPerson, vsTrash } from '@deephaven/icons';
2021
import { LIST_VIEW_ROW_HEIGHTS } from '@deephaven/utils';
2122
import { generateNormalizedItems, sampleSectionIdAndClasses } from './utils';
2223

@@ -90,6 +91,13 @@ export function ListViews(): JSX.Element {
9091
);
9192

9293
const [showIcons, setShowIcons] = useState(true);
94+
const [lastActionKey, setLastActionKey] = useState<ItemKey>('');
95+
const [lastActionItemKey, setLastActionItemKey] = useState<ItemKey>('');
96+
97+
const onAction = useCallback((actionKey: ItemKey, itemKey: ItemKey): void => {
98+
setLastActionKey(actionKey);
99+
setLastActionItemKey(itemKey);
100+
}, []);
93101

94102
const onChange = useCallback((keys: 'all' | Iterable<ItemKey>): void => {
95103
setSelectedKeys(keys);
@@ -211,7 +219,7 @@ export function ListViews(): JSX.Element {
211219
</Checkbox>
212220
</Flex>
213221

214-
<LabeledFlexContainer label="Controlled">
222+
<LabeledFlexContainer label="Controlled" gridColumn="span 2">
215223
<ListViewNormalized
216224
aria-label="Controlled"
217225
density={density}
@@ -220,7 +228,29 @@ export function ListViews(): JSX.Element {
220228
selectedKeys={selectedKeys}
221229
showItemIcons={showIcons}
222230
onChange={onChange}
231+
actions={
232+
<ListActionGroup
233+
overflowMode="collapse"
234+
buttonLabelBehavior="collapse"
235+
maxWidth={80}
236+
onAction={onAction}
237+
>
238+
<Item key="Edit">
239+
<Icon>
240+
<FontAwesomeIcon icon={vsEdit} />
241+
</Icon>
242+
<Text>Edit</Text>
243+
</Item>
244+
<Item key="Delete">
245+
<Icon>
246+
<FontAwesomeIcon icon={vsTrash} />
247+
</Icon>
248+
<Text>Delete</Text>
249+
</Item>
250+
</ListActionGroup>
251+
}
223252
/>
253+
{lastActionKey} {lastActionItemKey}
224254
</LabeledFlexContainer>
225255
</Grid>
226256
</div>

packages/components/src/spectrum/listView/ListActionGroup.tsx renamed to packages/components/src/spectrum/ListActionGroup.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
import { ActionGroupProps } from '../ActionGroup';
2-
import { ItemKey, ItemSelection } from '../utils';
1+
import { ActionGroupProps } from './ActionGroup';
2+
import { ItemKey, ItemSelection } from './utils';
33

44
export interface ListActionGroupProps<T>
5-
extends Omit<ActionGroupProps<T>, 'onAction' | 'onChange'> {
5+
extends Omit<
6+
ActionGroupProps<T>,
7+
'onAction' | 'onChange' | 'onSelectionChange'
8+
> {
69
/**
710
* Handler that is called when an item is pressed.
811
*/

packages/components/src/spectrum/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export * from './status';
1818
* Custom DH components wrapping React Spectrum components.
1919
*/
2020
export * from './ActionGroup';
21+
export * from './ListActionGroup';
2122
export * from './listView';
2223
export * from './picker';
2324
export * from './Heading';

packages/components/src/spectrum/listView/ListViewNormalized.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useMemo } from 'react';
22
import cl from 'classnames';
33
import {
4+
ListActions,
45
NormalizedItem,
56
normalizeTooltipOptions,
67
useRenderNormalizedItem,
@@ -13,6 +14,7 @@ export interface ListViewNormalizedProps
1314
extends Omit<ListViewProps, 'children'> {
1415
normalizedItems: NormalizedItem[];
1516
showItemIcons: boolean;
17+
actions?: ListActions<unknown>;
1618
}
1719

1820
/**
@@ -34,6 +36,7 @@ export function ListViewNormalized({
3436
defaultSelectedKeys,
3537
disabledKeys,
3638
showItemIcons,
39+
actions,
3740
UNSAFE_className,
3841
onChange,
3942
onSelectionChange,
@@ -53,6 +56,7 @@ export function ListViewNormalized({
5356
showItemDescriptions: false,
5457
showItemIcons,
5558
tooltipOptions,
59+
actions,
5660
});
5761

5862
// Spectrum doesn't re-render if only the `renderNormalizedItems` function

packages/components/src/spectrum/listView/ListViewWrapper.scss

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,21 +28,21 @@
2828
}
2929

3030
.dh-list-view-wrapper-density-compact {
31-
svg[class*='react-spectrum-ListViewItem-thumbnail'] {
31+
svg[class*='spectrum-Icon'] {
3232
height: var(--dh-list-view-item-icon-compact);
3333
width: var(--dh-list-view-item-icon-compact);
3434
}
3535
}
3636

3737
.dh-list-view-wrapper-density-regular {
38-
svg[class*='react-spectrum-ListViewItem-thumbnail'] {
38+
svg[class*='spectrum-Icon'] {
3939
height: var(--dh-list-view-item-icon-regular);
4040
width: var(--dh-list-view-item-icon-regular);
4141
}
4242
}
4343

4444
.dh-list-view-wrapper-density-spacious {
45-
svg[class*='react-spectrum-ListViewItem-thumbnail'] {
45+
svg[class*='spectrum-Icon'] {
4646
height: var(--dh-list-view-item-icon-spacious);
4747
width: var(--dh-list-view-item-icon-spacious);
4848
}

packages/components/src/spectrum/shared.ts

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -37,28 +37,6 @@ export type ItemsOrPrimitiveChildren<T> =
3737
| ItemElementOrPrimitive<T>[]
3838
| ItemRenderer<T>;
3939

40-
// export type AugmentItemChildrenWithPrimitives<
41-
// T,
42-
// U extends T extends {
43-
// children: ItemsChildren<infer A>;
44-
// }
45-
// ? A
46-
// : never = T extends {
47-
// children: ItemsChildren<infer A>;
48-
// }
49-
// ? A
50-
// : never,
51-
// > = Omit<T, 'children'> & {
52-
// children: ItemsOrPrimitiveChildren<U>;
53-
// };
54-
55-
// /**
56-
// * Spectrum SectionProps augmented with support for primitive item children.
57-
// */
58-
// export type SectionProps<T> = AugmentItemChildrenWithPrimitives<
59-
// SpectrumSectionProps<T>
60-
// >;
61-
6240
/**
6341
* Spectrum SectionProps augmented with support for primitive item children.
6442
*/

packages/components/src/spectrum/utils/itemUtils.test.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,13 @@ import {
99
NormalizedItem,
1010
NormalizedSection,
1111
normalizeTooltipOptions,
12-
ItemElementOrPrimitive,
1312
ItemOrSection,
1413
SectionElement,
1514
itemSelectionToStringSet,
1615
getPositionOfSelectedItemElement,
1716
isItemElementWithDescription,
1817
} from './itemUtils';
19-
import { Item, Section } from '../shared';
18+
import { Item, ItemElementOrPrimitive, Section } from '../shared';
2019
import { Text } from '../Text';
2120
import ItemContent from '../ItemContent';
2221

@@ -29,9 +28,7 @@ describe('getItemKey', () => {
2928
[{ key: 'top-level.key', item: { key: 'item.key' } }, 'item.key'],
3029
[{ key: 'top-level.key', item: {} }, 'top-level.key'],
3130
[{ key: 'top-level.key' }, 'top-level.key'],
32-
[{ item: { key: 'item.key' } }, 'item.key'],
33-
[{}, undefined],
34-
] as NormalizedItem[])(
31+
] as [NormalizedItem, string][])(
3532
'should return the item.key or fallback to the top-level key: %s, %s',
3633
(given, expected) => {
3734
const actual = getItemKey(given);
@@ -224,8 +221,8 @@ describe('isItemOrSection', () => {
224221

225222
describe('isNormalizedSection', () => {
226223
it.each([
227-
[{ item: {} } as NormalizedItem, false],
228-
[{ item: { items: [] } } as NormalizedSection, true],
224+
[{ key: 'mock.key', item: {} } as NormalizedItem, false],
225+
[{ key: 'mock.key', item: { items: [] } } as NormalizedSection, true],
229226
])('should return true for a normalized section: %s', (obj, expected) => {
230227
expect(isNormalizedSection(obj)).toBe(expected);
231228
});

packages/components/src/spectrum/utils/itemUtils.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,9 @@ export interface NormalizedSectionData {
101101
* `KeyedItem` interface to be compatible with Windowed data utils
102102
* (e.g. `useViewportData`).
103103
*/
104-
export type NormalizedItem = KeyedItem<NormalizedItemData, ItemKey | undefined>;
104+
export type NormalizedItem = KeyedItem<NormalizedItemData, ItemKey>;
105105

106-
export type NormalizedSection = KeyedItem<
107-
NormalizedSectionData,
108-
Key | undefined
109-
>;
106+
export type NormalizedSection = KeyedItem<NormalizedSectionData, Key>;
110107

111108
export type NormalizedItemOrSection<TItemOrSection extends ItemOrSection> =
112109
TItemOrSection extends SectionElement ? NormalizedSection : NormalizedItem;
@@ -127,9 +124,9 @@ export type TooltipOptions = { placement: PopperOptions['placement'] };
127124
export function getItemKey<
128125
TItem extends NormalizedItem | NormalizedSection,
129126
TKey extends TItem extends NormalizedItem
130-
? ItemKey | undefined
127+
? ItemKey
131128
: TItem extends NormalizedSection
132-
? Key | undefined
129+
? Key
133130
: undefined,
134131
>(item: TItem | null | undefined): TKey {
135132
return (item?.item?.key ?? item?.key) as TKey;

packages/components/src/spectrum/utils/useRenderNormalizedItem.tsx

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import { Key, useCallback } from 'react';
1+
import { Key, ReactElement, useCallback } from 'react';
2+
import ActionGroup from '../ActionGroup';
23
import { ItemContent } from '../ItemContent';
4+
import { ListActionGroupProps } from '../ListActionGroup';
35
import { Item } from '../shared';
46
import {
57
getItemKey,
@@ -10,11 +12,14 @@ import {
1012
} from './itemUtils';
1113
import { wrapIcon, wrapPrimitiveWithText } from './itemWrapperUtils';
1214

15+
export type ListActions<T> = ReactElement<ListActionGroupProps<T>>;
16+
1317
export interface UseRenderNormalizedItemOptions {
1418
itemIconSlot: ItemIconSlot;
1519
showItemDescriptions: boolean;
1620
showItemIcons: boolean;
1721
tooltipOptions: TooltipOptions | null;
22+
actions?: ListActions<unknown>;
1823
}
1924

2025
/**
@@ -24,19 +29,21 @@ export interface UseRenderNormalizedItemOptions {
2429
* @param showItemDescriptions Whether to show item descriptions
2530
* @param showItemIcons Whether to show item icons
2631
* @param tooltipOptions Tooltip options to use when rendering the item
32+
* @param actions Optional actions to render with the item
2733
* @returns Render function for normalized items
2834
*/
2935
export function useRenderNormalizedItem({
3036
itemIconSlot,
3137
showItemDescriptions,
3238
showItemIcons,
3339
tooltipOptions,
40+
actions,
3441
}: UseRenderNormalizedItemOptions): (
3542
normalizedItem: NormalizedItem
3643
) => JSX.Element {
3744
return useCallback(
3845
(normalizedItem: NormalizedItem) => {
39-
const key = getItemKey(normalizedItem);
46+
const itemKey = getItemKey(normalizedItem);
4047
const content = wrapPrimitiveWithText(normalizedItem.item?.content);
4148
const textValue = normalizedItem.item?.textValue ?? '';
4249

@@ -57,7 +64,7 @@ export function useRenderNormalizedItem({
5764
// key. We can't really get around setting in order to support Windowed
5865
// data, so we'll need to do some manual conversion of keys to strings
5966
// in other components that use this hook.
60-
key={key as Key}
67+
key={itemKey as Key}
6168
// The `textValue` prop gets used to provide the content of `<option>`
6269
// elements that back the Spectrum Picker. These are not visible in the UI,
6370
// but are used for accessibility purposes, so we set to an arbitrary
@@ -70,11 +77,19 @@ export function useRenderNormalizedItem({
7077
{icon}
7178
{content}
7279
{description}
80+
{actions?.props == null ? null : (
81+
<ActionGroup
82+
// eslint-disable-next-line react/jsx-props-no-spreading
83+
{...actions.props}
84+
onAction={key => actions.props.onAction(key, itemKey)}
85+
onChange={keys => actions.props.onChange?.(keys, itemKey)}
86+
/>
87+
)}
7388
</ItemContent>
7489
</Item>
7590
);
7691
},
77-
[itemIconSlot, showItemDescriptions, showItemIcons, tooltipOptions]
92+
[actions, itemIconSlot, showItemDescriptions, showItemIcons, tooltipOptions]
7893
);
7994
}
8095

0 commit comments

Comments
 (0)