@dxos/react-ui-list 0.9.1-main.c7dcc2e112 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/browser/index.mjs +628 -167
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +628 -167
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/aspects/useListDisclosure.d.ts +4 -4
- package/dist/types/src/aspects/useListDisclosure.d.ts.map +1 -1
- package/dist/types/src/aspects/useListNavigation.d.ts +9 -7
- package/dist/types/src/aspects/useListNavigation.d.ts.map +1 -1
- package/dist/types/src/aspects/useListSelection.d.ts +3 -3
- package/dist/types/src/aspects/useListSelection.d.ts.map +1 -1
- package/dist/types/src/components/Accordion/Accordion.d.ts +1 -1
- package/dist/types/src/components/Accordion/AccordionItem.d.ts +3 -1
- package/dist/types/src/components/Accordion/AccordionItem.d.ts.map +1 -1
- package/dist/types/src/components/Combobox/Combobox.d.ts +10 -3
- package/dist/types/src/components/Combobox/Combobox.d.ts.map +1 -1
- package/dist/types/src/components/DropIndicator/DropIndicator.d.ts +12 -0
- package/dist/types/src/components/DropIndicator/DropIndicator.d.ts.map +1 -0
- package/dist/types/src/components/DropIndicator/index.d.ts +2 -0
- package/dist/types/src/components/DropIndicator/index.d.ts.map +1 -0
- package/dist/types/src/components/Empty/Empty.d.ts +17 -0
- package/dist/types/src/components/Empty/Empty.d.ts.map +1 -0
- package/dist/types/src/components/Empty/Empty.stories.d.ts +18 -0
- package/dist/types/src/components/Empty/Empty.stories.d.ts.map +1 -0
- package/dist/types/src/components/Empty/index.d.ts +2 -0
- package/dist/types/src/components/Empty/index.d.ts.map +1 -0
- package/dist/types/src/components/List.theme.d.ts +193 -0
- package/dist/types/src/components/List.theme.d.ts.map +1 -0
- package/dist/types/src/components/Listbox/ListItemContent.d.ts +27 -0
- package/dist/types/src/components/Listbox/ListItemContent.d.ts.map +1 -0
- package/dist/types/src/components/Listbox/ListItemContent.stories.d.ts +24 -0
- package/dist/types/src/components/Listbox/ListItemContent.stories.d.ts.map +1 -0
- package/dist/types/src/components/Listbox/Listbox.d.ts +19 -2
- package/dist/types/src/components/Listbox/Listbox.d.ts.map +1 -1
- package/dist/types/src/components/Listbox/Listbox.stories.d.ts +4 -0
- package/dist/types/src/components/Listbox/Listbox.stories.d.ts.map +1 -1
- package/dist/types/src/components/OrderedList/OrderedList.d.ts +1 -1
- package/dist/types/src/components/OrderedList/OrderedList.d.ts.map +1 -1
- package/dist/types/src/components/OrderedList/OrderedListItem.d.ts.map +1 -1
- package/dist/types/src/components/OrderedList/OrderedListRoot.d.ts +1 -1
- package/dist/types/src/components/OrderedList/OrderedListRoot.d.ts.map +1 -1
- package/dist/types/src/components/Picker/Picker.d.ts +12 -4
- package/dist/types/src/components/Picker/Picker.d.ts.map +1 -1
- package/dist/types/src/components/Picker/index.d.ts +1 -1
- package/dist/types/src/components/Picker/index.d.ts.map +1 -1
- package/dist/types/src/components/Tree/Tree.d.ts +1 -1
- package/dist/types/src/components/Tree/Tree.d.ts.map +1 -1
- package/dist/types/src/components/Tree/Tree.stories.d.ts +5 -2
- package/dist/types/src/components/Tree/Tree.stories.d.ts.map +1 -1
- package/dist/types/src/components/Tree/TreeContext.d.ts +2 -0
- package/dist/types/src/components/Tree/TreeContext.d.ts.map +1 -1
- package/dist/types/src/components/Tree/TreeDropIndicator.d.ts +10 -0
- package/dist/types/src/components/Tree/TreeDropIndicator.d.ts.map +1 -0
- package/dist/types/src/components/Tree/TreeItem.d.ts.map +1 -1
- package/dist/types/src/components/Tree/testing.d.ts +4 -1
- package/dist/types/src/components/Tree/testing.d.ts.map +1 -1
- package/dist/types/src/components/Treegrid/Treegrid.d.ts +33 -0
- package/dist/types/src/components/Treegrid/Treegrid.d.ts.map +1 -0
- package/dist/types/src/components/Treegrid/Treegrid.stories.d.ts +11 -0
- package/dist/types/src/components/Treegrid/Treegrid.stories.d.ts.map +1 -0
- package/dist/types/src/components/Treegrid/Treegrid.theme.d.ts +37 -0
- package/dist/types/src/components/Treegrid/Treegrid.theme.d.ts.map +1 -0
- package/dist/types/src/components/Treegrid/index.d.ts +2 -0
- package/dist/types/src/components/Treegrid/index.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +3 -0
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +23 -22
- package/src/aspects/useListDisclosure.ts +8 -8
- package/src/aspects/useListNavigation.ts +9 -7
- package/src/aspects/useListSelection.ts +4 -4
- package/src/components/Accordion/AccordionItem.tsx +31 -13
- package/src/components/Combobox/Combobox.tsx +46 -38
- package/src/components/DropIndicator/DropIndicator.tsx +74 -0
- package/src/components/DropIndicator/index.ts +5 -0
- package/src/components/Empty/Empty.stories.tsx +37 -0
- package/src/components/Empty/Empty.tsx +41 -0
- package/src/components/Empty/index.ts +5 -0
- package/src/components/List.theme.ts +99 -0
- package/src/components/Listbox/ListItemContent.stories.tsx +41 -0
- package/src/components/Listbox/ListItemContent.tsx +49 -0
- package/src/components/Listbox/Listbox.stories.tsx +106 -7
- package/src/components/Listbox/Listbox.tsx +85 -42
- package/src/components/OrderedList/OrderedList.tsx +1 -1
- package/src/components/OrderedList/OrderedListItem.tsx +13 -13
- package/src/components/OrderedList/OrderedListRoot.tsx +6 -3
- package/src/components/Picker/Picker.tsx +25 -15
- package/src/components/Picker/index.ts +1 -1
- package/src/components/Tree/Tree.stories.tsx +39 -15
- package/src/components/Tree/Tree.tsx +13 -3
- package/src/components/Tree/TreeContext.tsx +2 -0
- package/src/components/Tree/TreeDropIndicator.tsx +72 -0
- package/src/components/Tree/TreeItem.tsx +43 -3
- package/src/components/Tree/TreeItemHeading.tsx +2 -2
- package/src/components/Tree/testing.ts +19 -14
- package/src/components/Treegrid/Treegrid.stories.tsx +132 -0
- package/src/components/Treegrid/Treegrid.theme.ts +38 -0
- package/src/components/Treegrid/Treegrid.tsx +187 -0
- package/src/components/Treegrid/index.ts +5 -0
- package/src/components/index.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-list",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "A list component.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -32,37 +32,38 @@
|
|
|
32
32
|
"@fluentui/react-tabster": "9.26.11",
|
|
33
33
|
"@radix-ui/react-accordion": "1.2.3",
|
|
34
34
|
"@radix-ui/react-context": "1.1.1",
|
|
35
|
+
"@radix-ui/react-primitive": "2.0.2",
|
|
35
36
|
"@radix-ui/react-slot": "1.1.2",
|
|
36
37
|
"@radix-ui/react-use-controllable-state": "1.1.0",
|
|
37
|
-
"@dxos/debug": "0.
|
|
38
|
-
"@dxos/echo": "0.
|
|
39
|
-
"@dxos/invariant": "0.
|
|
40
|
-
"@dxos/
|
|
41
|
-
"@dxos/react-ui": "0.
|
|
42
|
-
"@dxos/
|
|
43
|
-
"@dxos/
|
|
44
|
-
"@dxos/ui-types": "0.
|
|
45
|
-
"@dxos/util": "0.
|
|
38
|
+
"@dxos/debug": "0.10.0",
|
|
39
|
+
"@dxos/echo": "0.10.0",
|
|
40
|
+
"@dxos/invariant": "0.10.0",
|
|
41
|
+
"@dxos/log": "0.10.0",
|
|
42
|
+
"@dxos/react-ui": "0.10.0",
|
|
43
|
+
"@dxos/ui-theme": "0.10.0",
|
|
44
|
+
"@dxos/react-list": "0.10.0",
|
|
45
|
+
"@dxos/ui-types": "0.10.0",
|
|
46
|
+
"@dxos/util": "0.10.0"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
48
49
|
"@testing-library/jest-dom": "^6.9.1",
|
|
49
|
-
"@testing-library/react": "^16.3.
|
|
50
|
-
"@types/react": "~19.2.
|
|
50
|
+
"@testing-library/react": "^16.3.2",
|
|
51
|
+
"@types/react": "~19.2.17",
|
|
51
52
|
"@types/react-dom": "~19.2.3",
|
|
52
|
-
"effect": "3.21.
|
|
53
|
-
"react": "~19.2.
|
|
54
|
-
"react-dom": "~19.2.
|
|
53
|
+
"effect": "3.21.4",
|
|
54
|
+
"react": "~19.2.7",
|
|
55
|
+
"react-dom": "~19.2.7",
|
|
55
56
|
"resize-observer-polyfill": "^1.5.1",
|
|
56
57
|
"vite": "^8.0.16",
|
|
57
|
-
"@dxos/random": "0.
|
|
58
|
-
"@dxos/storybook-utils": "0.
|
|
58
|
+
"@dxos/random": "0.10.0",
|
|
59
|
+
"@dxos/storybook-utils": "0.10.0"
|
|
59
60
|
},
|
|
60
61
|
"peerDependencies": {
|
|
61
|
-
"effect": "3.21.
|
|
62
|
-
"react": "~19.2.
|
|
63
|
-
"react-dom": "~19.2.
|
|
64
|
-
"@dxos/react-ui": "0.
|
|
65
|
-
"@dxos/ui-theme": "0.
|
|
62
|
+
"effect": "3.21.4",
|
|
63
|
+
"react": "~19.2.7",
|
|
64
|
+
"react-dom": "~19.2.7",
|
|
65
|
+
"@dxos/react-ui": "0.10.0",
|
|
66
|
+
"@dxos/ui-theme": "0.10.0"
|
|
66
67
|
},
|
|
67
68
|
"publishConfig": {
|
|
68
69
|
"access": "public"
|
|
@@ -29,15 +29,15 @@ export type DisclosureItemBinding = {
|
|
|
29
29
|
panelId: string;
|
|
30
30
|
/** Spread onto the trigger element (button, title row, …). */
|
|
31
31
|
triggerProps: {
|
|
32
|
-
id: string;
|
|
32
|
+
'id': string;
|
|
33
33
|
'aria-expanded': boolean;
|
|
34
34
|
'aria-controls': string;
|
|
35
|
-
onClick: (event: MouseEvent) => void;
|
|
35
|
+
'onClick': (event: MouseEvent) => void;
|
|
36
36
|
};
|
|
37
37
|
/** Spread onto the disclosed panel; carries role=region for SR navigation. */
|
|
38
38
|
panelProps: {
|
|
39
|
-
id: string;
|
|
40
|
-
role: 'region';
|
|
39
|
+
'id': string;
|
|
40
|
+
'role': 'region';
|
|
41
41
|
'aria-labelledby': string;
|
|
42
42
|
};
|
|
43
43
|
};
|
|
@@ -141,14 +141,14 @@ export const useListDisclosure: {
|
|
|
141
141
|
triggerId,
|
|
142
142
|
panelId,
|
|
143
143
|
triggerProps: {
|
|
144
|
-
id: triggerId,
|
|
144
|
+
'id': triggerId,
|
|
145
145
|
'aria-expanded': expanded,
|
|
146
146
|
'aria-controls': panelId,
|
|
147
|
-
onClick: () => setExpanded(id, !expanded),
|
|
147
|
+
'onClick': () => setExpanded(id, !expanded),
|
|
148
148
|
},
|
|
149
149
|
panelProps: {
|
|
150
|
-
id: panelId,
|
|
151
|
-
role: 'region',
|
|
150
|
+
'id': panelId,
|
|
151
|
+
'role': 'region',
|
|
152
152
|
'aria-labelledby': triggerId,
|
|
153
153
|
},
|
|
154
154
|
};
|
|
@@ -45,17 +45,17 @@ export type UseListNavigationReturn = {
|
|
|
45
45
|
* stable contract.
|
|
46
46
|
*/
|
|
47
47
|
containerProps: TabsterDOMAttribute & {
|
|
48
|
-
role: ContainerRole;
|
|
48
|
+
'role': ContainerRole;
|
|
49
49
|
'aria-orientation'?: 'vertical' | 'horizontal';
|
|
50
|
-
onFocus: (event: FocusEvent<HTMLElement>) => void;
|
|
50
|
+
'onFocus': (event: FocusEvent<HTMLElement>) => void;
|
|
51
51
|
};
|
|
52
52
|
/**
|
|
53
53
|
* Apply to each item. Returns role, tabIndex, and aria-disabled. Disabled options remain
|
|
54
54
|
* focusable so screen readers can announce them, per WAI-ARIA listbox guidance.
|
|
55
55
|
*/
|
|
56
56
|
itemProps: (opts?: { disabled?: boolean }) => {
|
|
57
|
-
role: ItemRole;
|
|
58
|
-
tabIndex: number;
|
|
57
|
+
'role': ItemRole;
|
|
58
|
+
'tabIndex': number;
|
|
59
59
|
'aria-disabled'?: true;
|
|
60
60
|
};
|
|
61
61
|
};
|
|
@@ -96,9 +96,11 @@ const findListboxEntryTarget = (container: HTMLElement): HTMLElement | null => {
|
|
|
96
96
|
* and adds a focus-on-entry redirect (Tabster handles traversal once focus is on a
|
|
97
97
|
* child; first-entry is the consumer's responsibility).
|
|
98
98
|
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
99
|
+
* The canonical roving-tabindex keyboard aspect. Currently consumed by `Listbox` and
|
|
100
|
+
* `OrderedList`; `Tree` (Treegrid), `Picker`/`Combobox` (input-driven virtual focus), and
|
|
101
|
+
* `Mosaic.Stack` still ship bespoke navigation — see `react-ui-list/AUDIT.md` for the
|
|
102
|
+
* convergence analysis. Non-list focus zones — e.g. Composer's multi-pane chrome — keep
|
|
103
|
+
* their own Tabster wiring (`Focus.Group`).
|
|
102
104
|
*/
|
|
103
105
|
export const useListNavigation = ({
|
|
104
106
|
mode,
|
|
@@ -30,8 +30,8 @@ export type SelectionItemBinding = {
|
|
|
30
30
|
/** Spread onto the row element to bind click + focus + ARIA. */
|
|
31
31
|
rowProps: {
|
|
32
32
|
'aria-selected': boolean;
|
|
33
|
-
onClick: (event: MouseEvent) => void;
|
|
34
|
-
onFocus?: (event: FocusEvent) => void;
|
|
33
|
+
'onClick': (event: MouseEvent) => void;
|
|
34
|
+
'onFocus'?: (event: FocusEvent) => void;
|
|
35
35
|
};
|
|
36
36
|
};
|
|
37
37
|
|
|
@@ -46,7 +46,7 @@ const isMulti = (value: SingleValue | MultiValue): value is MultiValue => value
|
|
|
46
46
|
* controllable value semantics; emits `aria-selected` + click/focus handlers per row.
|
|
47
47
|
*
|
|
48
48
|
* `single` mode: at most one selected id, selection follows focus by default. Matches the
|
|
49
|
-
*
|
|
49
|
+
* `Listbox` behaviour and the WAI-ARIA listbox single-select pattern.
|
|
50
50
|
*
|
|
51
51
|
* `multi` mode: tracks a `Set<string>`. Selection does NOT follow focus by default — multi
|
|
52
52
|
* select usually pairs with an explicit toggle affordance (checkbox or keyboard Space) rather
|
|
@@ -129,7 +129,7 @@ export const useListSelection: {
|
|
|
129
129
|
},
|
|
130
130
|
rowProps: {
|
|
131
131
|
'aria-selected': selected,
|
|
132
|
-
onClick: () => {
|
|
132
|
+
'onClick': () => {
|
|
133
133
|
if (disabled) {
|
|
134
134
|
return;
|
|
135
135
|
}
|
|
@@ -9,12 +9,16 @@ import React, { type PropsWithChildren } from 'react';
|
|
|
9
9
|
import { Icon, type ThemedClassName } from '@dxos/react-ui';
|
|
10
10
|
import { mx } from '@dxos/ui-theme';
|
|
11
11
|
|
|
12
|
+
import { listTheme } from '../List.theme';
|
|
13
|
+
|
|
12
14
|
// See `AccordionRoot.tsx` for the rationale on `ListItemRecord = any`.
|
|
13
15
|
type ListItemRecord = any;
|
|
14
16
|
import { useAccordionContext } from './AccordionRoot';
|
|
15
17
|
|
|
16
18
|
const ACCORDION_ITEM_NAME = 'AccordionItem';
|
|
17
19
|
|
|
20
|
+
const styles = listTheme.styles();
|
|
21
|
+
|
|
18
22
|
type AccordionItemContext<T extends ListItemRecord> = {
|
|
19
23
|
item: T;
|
|
20
24
|
};
|
|
@@ -31,29 +35,43 @@ export const AccordionItem = <T extends ListItemRecord>({ children, classNames,
|
|
|
31
35
|
|
|
32
36
|
return (
|
|
33
37
|
<AccordionItemProvider {...{ item }}>
|
|
34
|
-
<AccordionPrimitive.Item value={getId(item)} className={mx(
|
|
38
|
+
<AccordionPrimitive.Item value={getId(item)} className={styles.accordionItem({ class: mx(classNames) })}>
|
|
35
39
|
{children}
|
|
36
40
|
</AccordionPrimitive.Item>
|
|
37
41
|
</AccordionItemProvider>
|
|
38
42
|
);
|
|
39
43
|
};
|
|
40
44
|
|
|
41
|
-
export type AccordionItemHeaderProps = ThemedClassName<
|
|
45
|
+
export type AccordionItemHeaderProps = ThemedClassName<
|
|
46
|
+
AccordionPrimitive.AccordionHeaderProps & {
|
|
47
|
+
icon?: string;
|
|
48
|
+
/** Apply `dx-hover` row styling on the trigger (off by default; mirrors `Listbox.Item`). */
|
|
49
|
+
hover?: boolean;
|
|
50
|
+
}
|
|
51
|
+
>;
|
|
42
52
|
|
|
43
|
-
export const AccordionItemHeader = ({ classNames, children, icon, ...props }: AccordionItemHeaderProps) => {
|
|
53
|
+
export const AccordionItemHeader = ({ classNames, children, icon, hover, ...props }: AccordionItemHeaderProps) => {
|
|
44
54
|
return (
|
|
45
55
|
<AccordionPrimitive.Header {...props} className={mx(classNames)}>
|
|
46
56
|
{/* `justify-between` pins the toggle caret to the trailing edge of the row regardless of
|
|
47
57
|
the header content's intrinsic width — so the affordance lives at a predictable
|
|
48
58
|
right-end position. The content wrapper grabs the remaining space. */}
|
|
49
|
-
<AccordionPrimitive.Trigger className=
|
|
50
|
-
{icon
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
<AccordionPrimitive.Trigger className={styles.accordionTrigger({ class: hover && 'dx-hover' })}>
|
|
60
|
+
{/* Leading icon and caret center within a single line-height band (`h-6`) so they sit on
|
|
61
|
+
the same centerline as the first line of the content, which may span multiple lines. */}
|
|
62
|
+
{icon && (
|
|
63
|
+
<span className={styles.accordionTriggerIcon()}>
|
|
64
|
+
<Icon icon={icon} size={4} />
|
|
65
|
+
</span>
|
|
66
|
+
)}
|
|
67
|
+
<div className={styles.accordionTriggerContent()}>{children}</div>
|
|
68
|
+
<span className={styles.accordionTriggerIcon()}>
|
|
69
|
+
<Icon
|
|
70
|
+
icon='ph--caret-right--regular'
|
|
71
|
+
size={4}
|
|
72
|
+
classNames='transition-transform duration-200 group-data-[state=open]:rotate-90'
|
|
73
|
+
/>
|
|
74
|
+
</span>
|
|
57
75
|
</AccordionPrimitive.Trigger>
|
|
58
76
|
</AccordionPrimitive.Header>
|
|
59
77
|
);
|
|
@@ -63,8 +81,8 @@ export type AccordionItemBodyProps = ThemedClassName<PropsWithChildren>;
|
|
|
63
81
|
|
|
64
82
|
export const AccordionItemBody = ({ children, classNames }: AccordionItemBodyProps) => {
|
|
65
83
|
return (
|
|
66
|
-
<AccordionPrimitive.Content className=
|
|
67
|
-
<div className={mx(
|
|
84
|
+
<AccordionPrimitive.Content className={styles.accordionBody()}>
|
|
85
|
+
<div className={styles.accordionBodyContent({ class: mx(classNames) })}>{children}</div>
|
|
68
86
|
</AccordionPrimitive.Content>
|
|
69
87
|
);
|
|
70
88
|
};
|
|
@@ -40,8 +40,11 @@ import {
|
|
|
40
40
|
import { composable, composableProps } from '@dxos/react-ui';
|
|
41
41
|
import { mx } from '@dxos/ui-theme';
|
|
42
42
|
|
|
43
|
+
import { listTheme } from '../List.theme';
|
|
43
44
|
import { Picker, type PickerInputProps, type PickerItemProps } from '../Picker';
|
|
44
45
|
|
|
46
|
+
const styles = listTheme.styles();
|
|
47
|
+
|
|
45
48
|
const COMBOBOX_NAME = 'Combobox';
|
|
46
49
|
const COMBOBOX_CONTENT_NAME = 'ComboboxContent';
|
|
47
50
|
const COMBOBOX_ITEM_NAME = 'ComboboxItem';
|
|
@@ -125,19 +128,24 @@ const ComboboxRoot = ({
|
|
|
125
128
|
// Filtering is caller-driven: pass already-matching <Combobox.Item> children.
|
|
126
129
|
//
|
|
127
130
|
|
|
128
|
-
type ComboboxContentProps = PopoverContentProps
|
|
131
|
+
type ComboboxContentProps = PopoverContentProps & {
|
|
132
|
+
/** Snap the highlight to the first item whenever the list changes (type-to-filter lists). */
|
|
133
|
+
resetSelectionOnChange?: boolean;
|
|
134
|
+
};
|
|
129
135
|
|
|
130
|
-
const ComboboxContent = composable<HTMLDivElement, ComboboxContentProps>(
|
|
131
|
-
|
|
136
|
+
const ComboboxContent = composable<HTMLDivElement, ComboboxContentProps>(
|
|
137
|
+
({ children, resetSelectionOnChange, ...props }, forwardedRef) => {
|
|
138
|
+
const { modalId } = useComboboxContext(COMBOBOX_CONTENT_NAME);
|
|
132
139
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
140
|
+
return (
|
|
141
|
+
<Popover.Content {...composableProps(props, { id: modalId })} ref={forwardedRef}>
|
|
142
|
+
<Popover.Viewport classNames='w-(--radix-popover-trigger-width)'>
|
|
143
|
+
<Picker.Root resetSelectionOnChange={resetSelectionOnChange}>{children}</Picker.Root>
|
|
144
|
+
</Popover.Viewport>
|
|
145
|
+
</Popover.Content>
|
|
146
|
+
);
|
|
147
|
+
},
|
|
148
|
+
);
|
|
141
149
|
|
|
142
150
|
ComboboxContent.displayName = COMBOBOX_CONTENT_NAME;
|
|
143
151
|
|
|
@@ -171,7 +179,7 @@ const ComboboxTrigger = forwardRef<HTMLButtonElement, ComboboxTriggerProps>(
|
|
|
171
179
|
>
|
|
172
180
|
{children ?? (
|
|
173
181
|
<>
|
|
174
|
-
<span className={
|
|
182
|
+
<span className={styles.comboboxTriggerText({ class: !value && 'text-subdued' })}>
|
|
175
183
|
{value || placeholder}
|
|
176
184
|
</span>
|
|
177
185
|
<Icon icon='ph--caret-down--bold' size={3} />
|
|
@@ -202,13 +210,7 @@ type ComboboxInputProps = ThemedClassName<
|
|
|
202
210
|
>;
|
|
203
211
|
|
|
204
212
|
const ComboboxInput = forwardRef<HTMLInputElement, ComboboxInputProps>(({ classNames, ...props }, forwardedRef) => {
|
|
205
|
-
return (
|
|
206
|
-
<Picker.Input
|
|
207
|
-
{...props}
|
|
208
|
-
classNames={['m-form-chrome mb-0 w-[calc(100%-2*var(--spacing-form-chrome))]', classNames]}
|
|
209
|
-
ref={forwardedRef}
|
|
210
|
-
/>
|
|
211
|
-
);
|
|
213
|
+
return <Picker.Input {...props} classNames={styles.comboboxInput({ class: classNames })} ref={forwardedRef} />;
|
|
212
214
|
});
|
|
213
215
|
|
|
214
216
|
ComboboxInput.displayName = 'Combobox.Input';
|
|
@@ -223,7 +225,7 @@ const ComboboxList = forwardRef<HTMLDivElement, ComboboxListProps>(
|
|
|
223
225
|
({ classNames, children, ...props }, forwardedRef) => {
|
|
224
226
|
return (
|
|
225
227
|
<ScrollArea.Root
|
|
226
|
-
{...composableProps(props, { classNames:
|
|
228
|
+
{...composableProps(props, { classNames: styles.comboboxList({ class: classNames }) })}
|
|
227
229
|
role='listbox'
|
|
228
230
|
centered
|
|
229
231
|
padding
|
|
@@ -248,6 +250,8 @@ type ComboboxItemProps = ThemedClassName<
|
|
|
248
250
|
value: string;
|
|
249
251
|
/** Display label (used when `children` are not provided). */
|
|
250
252
|
label?: string;
|
|
253
|
+
/** Optional secondary line shown beneath the label (muted, smaller). */
|
|
254
|
+
description?: string;
|
|
251
255
|
/** Optional icon id (Phosphor) shown before the label. */
|
|
252
256
|
icon?: string;
|
|
253
257
|
/** Additional class names for the icon. */
|
|
@@ -272,6 +276,7 @@ const ComboboxItem = forwardRef<HTMLDivElement, ComboboxItemProps>(
|
|
|
272
276
|
onSelect,
|
|
273
277
|
value,
|
|
274
278
|
label,
|
|
279
|
+
description,
|
|
275
280
|
icon,
|
|
276
281
|
iconClassNames,
|
|
277
282
|
checked,
|
|
@@ -299,21 +304,24 @@ const ComboboxItem = forwardRef<HTMLDivElement, ComboboxItemProps>(
|
|
|
299
304
|
disabled={disabled}
|
|
300
305
|
onSelect={handleSelect}
|
|
301
306
|
ref={forwardedRef}
|
|
302
|
-
classNames={
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
'flex w-full gap-2 items-center',
|
|
309
|
-
disabled && 'hover:bg-transparent data-[selected=true]:bg-transparent',
|
|
310
|
-
classNames,
|
|
311
|
-
]}
|
|
307
|
+
classNames={styles.comboboxItem({
|
|
308
|
+
// `px-3 py-1`, `cursor-pointer`, `select-none` and the `dx-hover` / `dx-selected`
|
|
309
|
+
// pairing come from `Picker.Item`'s defaults; the slot only adds row-shape (flex /
|
|
310
|
+
// icons + label). Disabled overrides are layered on per-instance.
|
|
311
|
+
class: mx(disabled && 'hover:bg-transparent data-[selected=true]:bg-transparent', classNames),
|
|
312
|
+
})}
|
|
312
313
|
>
|
|
313
314
|
{children ?? (
|
|
314
315
|
<>
|
|
315
316
|
{icon && <Icon icon={icon} classNames={iconClassNames} />}
|
|
316
|
-
|
|
317
|
+
{description ? (
|
|
318
|
+
<span className='w-0 grow flex flex-col'>
|
|
319
|
+
<span className='truncate'>{label}</span>
|
|
320
|
+
<span className={styles.comboboxItemDescription()}>{description}</span>
|
|
321
|
+
</span>
|
|
322
|
+
) : (
|
|
323
|
+
<span className='w-0 grow truncate'>{label}</span>
|
|
324
|
+
)}
|
|
317
325
|
{suffix && <span className='shrink-0 text-description'>{suffix}</span>}
|
|
318
326
|
{checked && <Icon icon='ph--check--regular' />}
|
|
319
327
|
</>
|
|
@@ -375,14 +383,14 @@ export const Combobox = {
|
|
|
375
383
|
};
|
|
376
384
|
|
|
377
385
|
export type {
|
|
378
|
-
|
|
379
|
-
ComboboxPortalProps,
|
|
386
|
+
ComboboxArrowProps,
|
|
380
387
|
ComboboxContentProps,
|
|
381
|
-
|
|
382
|
-
ComboboxVirtualTriggerProps,
|
|
388
|
+
ComboboxEmptyProps,
|
|
383
389
|
ComboboxInputProps,
|
|
384
|
-
ComboboxListProps,
|
|
385
390
|
ComboboxItemProps,
|
|
386
|
-
|
|
387
|
-
|
|
391
|
+
ComboboxListProps,
|
|
392
|
+
ComboboxPortalProps,
|
|
393
|
+
ComboboxRootProps,
|
|
394
|
+
ComboboxTriggerProps,
|
|
395
|
+
ComboboxVirtualTriggerProps,
|
|
388
396
|
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Edge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/types';
|
|
6
|
+
import React, { type CSSProperties, type HTMLAttributes } from 'react';
|
|
7
|
+
|
|
8
|
+
// Tailwind port of `@atlaskit/pragmatic-drag-and-drop-react-drop-indicator/box`. The atlaskit
|
|
9
|
+
// React component `require`s a `.compiled.css` file, which crashes the node test loader (and
|
|
10
|
+
// thus every node-tested plugin that transitively imports this list); this port is CSS-free
|
|
11
|
+
// and theme-aware (`bg-accent-bg`). `react-ui-mosaic` keeps the atlaskit component. See
|
|
12
|
+
// `react-ui-list/AUDIT.md` D4.
|
|
13
|
+
|
|
14
|
+
type Orientation = 'horizontal' | 'vertical';
|
|
15
|
+
|
|
16
|
+
const edgeToOrientationMap: Record<Edge, Orientation> = {
|
|
17
|
+
top: 'horizontal',
|
|
18
|
+
bottom: 'horizontal',
|
|
19
|
+
left: 'vertical',
|
|
20
|
+
right: 'vertical',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const orientationStyles: Record<Orientation, HTMLAttributes<HTMLElement>['className']> = {
|
|
24
|
+
horizontal:
|
|
25
|
+
'h-(--line-thickness) left-[calc(var(--line-inset)+var(--terminal-radius))] right-(--line-inset) before:left-(--terminal-inset)',
|
|
26
|
+
vertical:
|
|
27
|
+
'w-(--line-thickness) top-[calc(var(--line-inset)+var(--terminal-radius))] bottom-(--line-inset) before:top-(--terminal-inset)',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const edgeStyles: Record<Edge, HTMLAttributes<HTMLElement>['className']> = {
|
|
31
|
+
top: 'top-(--line-offset) before:top-(--offset-terminal)',
|
|
32
|
+
right: 'right-(--line-offset) before:right-(--offset-terminal)',
|
|
33
|
+
bottom: 'bottom-(--line-offset) before:bottom-(--offset-terminal)',
|
|
34
|
+
left: 'left-(--line-offset) before:left-(--offset-terminal)',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const strokeSize = 2;
|
|
38
|
+
const terminalSize = 8;
|
|
39
|
+
const offsetToAlignTerminalWithLine = (strokeSize - terminalSize) / 2;
|
|
40
|
+
|
|
41
|
+
/** Props for {@link DropIndicator}. */
|
|
42
|
+
export type DropIndicatorProps = {
|
|
43
|
+
edge: Edge;
|
|
44
|
+
gap?: number;
|
|
45
|
+
terminalInset?: number;
|
|
46
|
+
lineInset?: number;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** Themed box-edge drop indicator (line + circular terminal) for pragmatic-dnd reorder. */
|
|
50
|
+
export const DropIndicator = ({
|
|
51
|
+
edge,
|
|
52
|
+
gap = 0,
|
|
53
|
+
lineInset = 0,
|
|
54
|
+
terminalInset = lineInset - terminalSize,
|
|
55
|
+
}: DropIndicatorProps) => {
|
|
56
|
+
const orientation = edgeToOrientationMap[edge];
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div
|
|
60
|
+
style={
|
|
61
|
+
{
|
|
62
|
+
'--line-thickness': `${strokeSize}px`,
|
|
63
|
+
'--line-offset': `calc(-0.5 * (${gap}px + ${strokeSize}px))`,
|
|
64
|
+
'--line-inset': `${lineInset}px`,
|
|
65
|
+
'--terminal-size': `${terminalSize}px`,
|
|
66
|
+
'--terminal-radius': `${terminalSize / 2}px`,
|
|
67
|
+
'--terminal-inset': `${terminalInset}px`,
|
|
68
|
+
'--offset-terminal': `${offsetToAlignTerminalWithLine}px`,
|
|
69
|
+
} as CSSProperties
|
|
70
|
+
}
|
|
71
|
+
className={`absolute z-10 pointer-events-none bg-accent-bg before:content-[''] before:w-(--terminal-size) before:h-(--terminal-size) box-border before:box-border before:absolute before:border-[length:--line-thickness] before:border-solid before:border-accent-bg before:rounded-full ${orientationStyles[orientation]} ${edgeStyles[edge]}`}
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
+
|
|
7
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
8
|
+
|
|
9
|
+
import { Empty, type EmptyProps } from './Empty';
|
|
10
|
+
|
|
11
|
+
const meta = {
|
|
12
|
+
title: 'ui/react-ui-list/Empty',
|
|
13
|
+
component: Empty,
|
|
14
|
+
decorators: [withTheme(), withLayout({ layout: 'column' })],
|
|
15
|
+
} satisfies Meta<typeof Empty>;
|
|
16
|
+
|
|
17
|
+
export default meta;
|
|
18
|
+
|
|
19
|
+
type Story = StoryObj<EmptyProps>;
|
|
20
|
+
|
|
21
|
+
/** No label — falls back to the generic message. */
|
|
22
|
+
export const Fallback: Story = {};
|
|
23
|
+
|
|
24
|
+
/** Caller-supplied (already-translated) label. */
|
|
25
|
+
export const WithLabel: Story = {
|
|
26
|
+
args: {
|
|
27
|
+
label: 'No automations',
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Label plus a leading icon. */
|
|
32
|
+
export const WithIcon: Story = {
|
|
33
|
+
args: {
|
|
34
|
+
label: 'No automations',
|
|
35
|
+
icon: 'ph--lightning--regular',
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { type ReactNode } from 'react';
|
|
6
|
+
|
|
7
|
+
import { Icon, composable, composableProps, useTranslation } from '@dxos/react-ui';
|
|
8
|
+
import { osTranslations } from '@dxos/ui-theme';
|
|
9
|
+
import { type ComposableProps } from '@dxos/ui-types';
|
|
10
|
+
|
|
11
|
+
export type EmptyProps = ComposableProps<{
|
|
12
|
+
/** Message to show; caller is responsible for translating it. Falls back to a generic message when omitted. */
|
|
13
|
+
label?: ReactNode;
|
|
14
|
+
/** Optional Phosphor icon name shown above the message. */
|
|
15
|
+
icon?: string;
|
|
16
|
+
}>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Empty-state placeholder for a list/collection: a subdued, centered message shown in place of the list
|
|
20
|
+
* when there are no items. Pass a domain-specific {@link EmptyProps.label} (already translated); when none is
|
|
21
|
+
* given it falls back to a generic "No items" message from the shared `os` translation namespace.
|
|
22
|
+
*/
|
|
23
|
+
export const Empty = composable<HTMLDivElement, EmptyProps>(({ label, icon, ...props }, forwardedRef) => {
|
|
24
|
+
const { t } = useTranslation(osTranslations);
|
|
25
|
+
// `defaultValue` keeps the fallback working even before a host registers the key, and leaves it translatable.
|
|
26
|
+
const message = label ?? t('empty.label', { defaultValue: 'No items' });
|
|
27
|
+
return (
|
|
28
|
+
<div
|
|
29
|
+
{...composableProps<HTMLDivElement>(props, {
|
|
30
|
+
classNames: 'flex flex-col items-center justify-center gap-2 p-4 text-sm text-center text-description',
|
|
31
|
+
role: 'status',
|
|
32
|
+
})}
|
|
33
|
+
ref={forwardedRef}
|
|
34
|
+
>
|
|
35
|
+
{icon && <Icon icon={icon} size={6} classNames='text-subdued' />}
|
|
36
|
+
<span>{message}</span>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
Empty.displayName = 'Empty';
|