@dxos/react-list 0.8.4-main.dedc0f3 → 0.8.4-main.dfabb4ec29

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/package.json CHANGED
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "name": "@dxos/react-list",
3
- "version": "0.8.4-main.dedc0f3",
3
+ "version": "0.8.4-main.dfabb4ec29",
4
4
  "description": "List primitive components for React.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/dxos/dxos"
10
+ },
7
11
  "license": "MIT",
8
12
  "author": "DXOS.org",
9
- "sideEffects": true,
13
+ "sideEffects": false,
10
14
  "type": "module",
11
15
  "exports": {
12
16
  ".": {
@@ -17,34 +21,29 @@
17
21
  }
18
22
  },
19
23
  "types": "dist/types/src/index.d.ts",
20
- "typesVersions": {
21
- "*": {}
22
- },
23
24
  "files": [
24
25
  "dist",
25
26
  "src"
26
27
  ],
27
28
  "dependencies": {
28
- "@preact-signals/safe-react": "^0.9.0",
29
29
  "@radix-ui/react-collapsible": "1.1.3",
30
30
  "@radix-ui/react-context": "1.1.1",
31
31
  "@radix-ui/react-primitive": "2.0.2",
32
32
  "@radix-ui/react-slot": "1.1.2",
33
33
  "@radix-ui/react-use-controllable-state": "1.1.0",
34
- "lodash.omit": "^4.5.0",
35
- "@dxos/react-hooks": "0.8.4-main.dedc0f3"
34
+ "@dxos/react-hooks": "0.8.4-main.dfabb4ec29"
36
35
  },
37
36
  "devDependencies": {
38
37
  "@radix-ui/react-checkbox": "1.1.4",
39
- "@types/lodash.omit": "^4.5.7",
40
- "@types/react": "~18.2.0",
41
- "@types/react-dom": "~18.2.0",
42
- "react": "~18.2.0",
43
- "react-dom": "~18.2.0"
38
+ "@storybook/react-vite": "^10.3.6",
39
+ "@types/react": "~19.2.7",
40
+ "@types/react-dom": "~19.2.3",
41
+ "react": "~19.2.3",
42
+ "react-dom": "~19.2.3"
44
43
  },
45
44
  "peerDependencies": {
46
- "react": "~18.2.0",
47
- "react-dom": "~18.2.0"
45
+ "react": "~19.2.3",
46
+ "react-dom": "~19.2.3"
48
47
  },
49
48
  "publishConfig": {
50
49
  "access": "public"
@@ -0,0 +1,149 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ // Stories for the elemental `@dxos/react-list` primitive — ARIA + structure
6
+ // only. These show the default-styling-free behavior of `List` / `ListItem`
7
+ // in isolation. For the styled, ARIA-correct, keyboard-navigable layer most
8
+ // app code reaches for, see `@dxos/react-ui-list`'s `RowList` story.
9
+
10
+ import { type Decorator, type Meta, type StoryObj } from '@storybook/react-vite';
11
+ import React, { useState } from 'react';
12
+
13
+ import { List } from './List';
14
+ import { ListItem, ListItemCollapsibleContent, ListItemHeading, ListItemOpenTrigger } from './ListItem';
15
+
16
+ type Item = { id: string; label: string };
17
+
18
+ const items: Item[] = Array.from({ length: 5 }, (_, i) => ({
19
+ id: `item-${i}`,
20
+ label: `Item ${i + 1}`,
21
+ }));
22
+
23
+ //
24
+ // Static unordered list — no selection.
25
+ //
26
+
27
+ const StaticStory = () => (
28
+ <List variant='unordered' className='dx-container border border-separator p-2'>
29
+ {items.map((item) => (
30
+ <ListItem key={item.id} className='py-1'>
31
+ <ListItemHeading>{item.label}</ListItemHeading>
32
+ </ListItem>
33
+ ))}
34
+ </List>
35
+ );
36
+
37
+ //
38
+ // Single-select listbox. Pairs `aria-selected` (set by the primitive when
39
+ // `selectable={true}`) with the canonical `dx-selected` / `dx-hover`
40
+ // utilities to demonstrate the ARIA ↔ dx-* grammar in its rawest form.
41
+ //
42
+
43
+ const SingleSelectStory = () => {
44
+ const [selected, setSelected] = useState<string>(items[0].id);
45
+ return (
46
+ <List
47
+ variant='unordered'
48
+ selectable
49
+ aria-label='Single-select example'
50
+ className='dx-container border border-separator'
51
+ >
52
+ {items.map((item) => (
53
+ <ListItem
54
+ key={item.id}
55
+ selected={item.id === selected}
56
+ onClick={() => setSelected(item.id)}
57
+ className='dx-hover dx-selected px-3 py-2 cursor-pointer outline-none'
58
+ >
59
+ <ListItemHeading>{item.label}</ListItemHeading>
60
+ </ListItem>
61
+ ))}
62
+ </List>
63
+ );
64
+ };
65
+
66
+ //
67
+ // Multi-select listbox. `multiSelectable` adds `aria-multiselectable="true"`
68
+ // on the listbox itself; selection state is per-item.
69
+ //
70
+
71
+ const MultiSelectStory = () => {
72
+ const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set([items[1].id]));
73
+ const toggle = (id: string) => {
74
+ setSelectedIds((prev) => {
75
+ const next = new Set(prev);
76
+ if (next.has(id)) {
77
+ next.delete(id);
78
+ } else {
79
+ next.add(id);
80
+ }
81
+ return next;
82
+ });
83
+ };
84
+ return (
85
+ <List
86
+ variant='unordered'
87
+ selectable
88
+ multiSelectable
89
+ aria-label='Multi-select example'
90
+ className='dx-container border border-separator'
91
+ >
92
+ {items.map((item) => (
93
+ <ListItem
94
+ key={item.id}
95
+ selected={selectedIds.has(item.id)}
96
+ onClick={() => toggle(item.id)}
97
+ className='dx-hover dx-selected px-3 py-2 cursor-pointer outline-none'
98
+ >
99
+ <ListItemHeading>{item.label}</ListItemHeading>
100
+ </ListItem>
101
+ ))}
102
+ </List>
103
+ );
104
+ };
105
+
106
+ //
107
+ // Collapsible item — opens / closes a Radix `Collapsible`-backed body.
108
+ // Useful for accordion-style headings without reaching for the full
109
+ // `react-ui-list` `Accordion` component.
110
+ //
111
+
112
+ const CollapsibleStory = () => (
113
+ <List variant='unordered' className='dx-container border border-separator divide-y divide-separator'>
114
+ {items.slice(0, 3).map((item) => (
115
+ <ListItem key={item.id} collapsible defaultOpen={item.id === items[0].id}>
116
+ <ListItemOpenTrigger asChild>
117
+ <ListItemHeading className='cursor-pointer px-3 py-2 select-none'>{item.label}</ListItemHeading>
118
+ </ListItemOpenTrigger>
119
+ <ListItemCollapsibleContent>
120
+ <div className='px-3 pb-2 text-description text-sm'>Details for {item.label}.</div>
121
+ </ListItemCollapsibleContent>
122
+ </ListItem>
123
+ ))}
124
+ </List>
125
+ );
126
+
127
+ // Inline column decorator — `react-list` deliberately doesn't depend on
128
+ // `@dxos/react-ui` (a `withTheme/withLayout` import would create a cycle
129
+ // since `react-ui` already depends on this primitive). Storybook's global
130
+ // `withThemeByClassName` already applies the theme class at the root.
131
+ const withColumn: Decorator = (Story) => (
132
+ <div className='flex flex-col gap-4 p-4 h-full max-w-md'>
133
+ <Story />
134
+ </div>
135
+ );
136
+
137
+ const meta = {
138
+ title: 'ui/react-list/List',
139
+ decorators: [withColumn],
140
+ } satisfies Meta;
141
+
142
+ export default meta;
143
+
144
+ type Story = StoryObj<typeof meta>;
145
+
146
+ export const Static: Story = { render: StaticStory };
147
+ export const SingleSelect: Story = { render: SingleSelectStory };
148
+ export const MultiSelect: Story = { render: MultiSelectStory };
149
+ export const Collapsible: Story = { render: CollapsibleStory };
package/src/List.tsx CHANGED
@@ -2,12 +2,36 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
+ // Elemental list / listbox primitive.
6
+ //
7
+ // This is the ARIA-only foundation of the DXOS list stack. It renders a
8
+ // semantically-correct `<ol>` / `<ul>` (or, when `selectable={true}`, a
9
+ // `role="listbox"` element with `role="option"` children carrying
10
+ // `aria-selected`). It applies no styling, no keyboard navigation, and
11
+ // no `dx-*` utility classes — those are layered above in
12
+ // `@dxos/react-ui-list`.
13
+ //
14
+ // Layering:
15
+ // - `@dxos/react-list` — this package; ARIA + structure only.
16
+ // - `@dxos/react-ui-list` — adds `dx-*` styling, keyboard nav, and
17
+ // opinionated `RowList`/`CardList` containers.
18
+ // - `@dxos/react-ui-mosaic` — virtualized / draggable / card-board
19
+ // layouts; composes the above where useful.
20
+ //
21
+ // Most app code should reach for `@dxos/react-ui-list`. Use this primitive
22
+ // directly only when building a *new* selectable surface that needs full
23
+ // control over styling and keyboard handling (e.g. a custom Combobox).
24
+ //
25
+ // See:
26
+ // - `packages/ui/ui-theme/src/css/components/selected.md` for the
27
+ // `aria-selected` ↔ `dx-selected` pairing rules.
28
+ // - `packages/ui/react-ui-list/AUDIT.md` for why this layering exists.
29
+ // - https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/listbox_role
30
+
5
31
  import { type Scope, createContextScope } from '@radix-ui/react-context';
6
32
  import { Primitive } from '@radix-ui/react-primitive';
7
33
  import React, { type ComponentPropsWithRef, forwardRef } from 'react';
8
34
 
9
- // TODO(thure): A lot of the accessible affordances for this kind of thing need to be implemented per https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/listbox_role
10
-
11
35
  const LIST_NAME = 'List';
12
36
 
13
37
  type ListScopedProps<P> = P & { __listScope?: Scope };
@@ -17,7 +41,19 @@ type ListVariant = 'ordered' | 'unordered';
17
41
  type ListItemSizes = 'one' | 'many';
18
42
 
19
43
  type ListProps = ComponentPropsWithRef<typeof Primitive.ol> & {
44
+ /**
45
+ * If true, render as `role="listbox"` and let `ListItem` children become
46
+ * `role="option"` + `aria-selected`. If false (default) the list is a
47
+ * plain `<ol>` / `<ul>` with no selection semantics — pick this for
48
+ * static lists.
49
+ */
20
50
  selectable?: boolean;
51
+ /**
52
+ * If true, the listbox advertises multi-select via
53
+ * `aria-multiselectable="true"`. Defaults to false (single-select).
54
+ * Has no effect unless `selectable` is also true.
55
+ */
56
+ multiSelectable?: boolean;
21
57
  variant?: ListVariant;
22
58
  itemSizes?: ListItemSizes;
23
59
  };
@@ -33,10 +69,28 @@ type ListContextValue = {
33
69
  const [ListProvider, useListContext] = createListContext<ListContextValue>(LIST_NAME);
34
70
 
35
71
  const List = forwardRef<HTMLOListElement, ListProps>((props: ListScopedProps<ListProps>, forwardedRef) => {
36
- const { __listScope, variant = 'ordered', selectable = false, itemSizes, children, ...rootProps } = props;
72
+ const {
73
+ __listScope,
74
+ variant = 'ordered',
75
+ selectable = false,
76
+ multiSelectable = false,
77
+ itemSizes,
78
+ children,
79
+ ...rootProps
80
+ } = props;
37
81
  const ListRoot = variant === 'ordered' ? Primitive.ol : Primitive.ul;
38
82
  return (
39
- <ListRoot {...(selectable && { role: 'listbox', 'aria-multiselectable': true })} {...rootProps} ref={forwardedRef}>
83
+ <ListRoot
84
+ // `aria-multiselectable` is only meaningful on `role="listbox"`,
85
+ // and even there is omitted in the single-select default to keep
86
+ // assistive tech announcements concise.
87
+ {...(selectable && {
88
+ role: 'listbox',
89
+ ...(multiSelectable && { 'aria-multiselectable': true as const }),
90
+ })}
91
+ {...rootProps}
92
+ ref={forwardedRef}
93
+ >
40
94
  <ListProvider
41
95
  {...{
42
96
  scope: __listScope,
package/src/ListItem.tsx CHANGED
@@ -13,7 +13,7 @@ import React, {
13
13
  type ComponentProps,
14
14
  type ComponentPropsWithoutRef,
15
15
  type Dispatch,
16
- type ElementRef,
16
+ type ComponentRef,
17
17
  type ForwardRefExoticComponent,
18
18
  type RefAttributes,
19
19
  type SetStateAction,
@@ -44,7 +44,7 @@ type ListItemProps = Omit<ListItemData, 'id'> & { collapsible?: boolean } & RefA
44
44
  defaultSelected?: CheckboxProps['defaultChecked'];
45
45
  };
46
46
 
47
- type ListItemElement = ElementRef<'li'>;
47
+ type ListItemElement = ComponentRef<'li'>;
48
48
 
49
49
  const [createListItemContext, createListItemScope] = createContextScope(LIST_ITEM_NAME, []);
50
50
 
@@ -65,11 +65,11 @@ type ListItemHeadingProps = ListItemScopedProps<Omit<ComponentPropsWithoutRef<'p
65
65
  const ListItemHeading = forwardRef<HTMLDivElement, ListItemHeadingProps>(
66
66
  ({ children, asChild, __listItemScope, ...props }, forwardedRef) => {
67
67
  const { headingId } = useListItemContext(LIST_ITEM_NAME, __listItemScope);
68
- const Root = asChild ? Slot : Primitive.div;
68
+ const Comp = asChild ? Slot : Primitive.div;
69
69
  return (
70
- <Root {...props} id={headingId} ref={forwardedRef}>
70
+ <Comp {...props} id={headingId} ref={forwardedRef}>
71
71
  {children}
72
- </Root>
72
+ </Comp>
73
73
  );
74
74
  },
75
75
  );