@dxos/react-ui-list 0.8.4-main.c85a9c8dae → 0.8.4-main.d05673bc65

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-list",
3
- "version": "0.8.4-main.c85a9c8dae",
3
+ "version": "0.8.4-main.d05673bc65",
4
4
  "description": "A list component.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -33,15 +33,16 @@
33
33
  "@effect-atom/atom-react": "^0.5.0",
34
34
  "@radix-ui/react-accordion": "1.2.3",
35
35
  "@radix-ui/react-context": "1.1.1",
36
- "@dxos/debug": "0.8.4-main.c85a9c8dae",
37
- "@dxos/invariant": "0.8.4-main.c85a9c8dae",
38
- "@dxos/echo": "0.8.4-main.c85a9c8dae",
39
- "@dxos/log": "0.8.4-main.c85a9c8dae",
40
- "@dxos/react-ui": "0.8.4-main.c85a9c8dae",
41
- "@dxos/react-ui-text-tooltip": "0.8.4-main.c85a9c8dae",
42
- "@dxos/ui-theme": "0.8.4-main.c85a9c8dae",
43
- "@dxos/ui-types": "0.8.4-main.c85a9c8dae",
44
- "@dxos/util": "0.8.4-main.c85a9c8dae"
36
+ "@radix-ui/react-slot": "1.1.2",
37
+ "@dxos/debug": "0.8.4-main.d05673bc65",
38
+ "@dxos/echo": "0.8.4-main.d05673bc65",
39
+ "@dxos/invariant": "0.8.4-main.d05673bc65",
40
+ "@dxos/react-ui": "0.8.4-main.d05673bc65",
41
+ "@dxos/ui-theme": "0.8.4-main.d05673bc65",
42
+ "@dxos/log": "0.8.4-main.d05673bc65",
43
+ "@dxos/react-ui-text-tooltip": "0.8.4-main.d05673bc65",
44
+ "@dxos/util": "0.8.4-main.d05673bc65",
45
+ "@dxos/ui-types": "0.8.4-main.d05673bc65"
45
46
  },
46
47
  "devDependencies": {
47
48
  "@types/react": "~19.2.7",
@@ -50,15 +51,15 @@
50
51
  "react": "~19.2.3",
51
52
  "react-dom": "~19.2.3",
52
53
  "vite": "^7.1.11",
53
- "@dxos/random": "0.8.4-main.c85a9c8dae",
54
- "@dxos/storybook-utils": "0.8.4-main.c85a9c8dae"
54
+ "@dxos/random": "0.8.4-main.d05673bc65",
55
+ "@dxos/storybook-utils": "0.8.4-main.d05673bc65"
55
56
  },
56
57
  "peerDependencies": {
57
58
  "effect": "3.19.16",
58
59
  "react": "~19.2.3",
59
60
  "react-dom": "~19.2.3",
60
- "@dxos/react-ui": "0.8.4-main.c85a9c8dae",
61
- "@dxos/ui-theme": "0.8.4-main.c85a9c8dae"
61
+ "@dxos/react-ui": "0.8.4-main.d05673bc65",
62
+ "@dxos/ui-theme": "0.8.4-main.d05673bc65"
62
63
  },
63
64
  "publishConfig": {
64
65
  "access": "public"
@@ -53,7 +53,7 @@ const DefaultStory = (props: Omit<ListRootProps<TestItemType>, 'items'>) => {
53
53
 
54
54
  <div role='list' className='w-full h-full overflow-auto'>
55
55
  {items?.map((item) => (
56
- <List.Item<TestItemType> key={item.id} item={item} classNames={mx(grid, ghostHover)}>
56
+ <List.Item<TestItemType> key={item.id} item={item} classNames={mx(grid)}>
57
57
  <List.ItemDragHandle />
58
58
  <List.ItemTitle onClick={() => handleSelect(item)}>{item.name}</List.ItemTitle>
59
59
  <List.ItemDeleteButton onClick={() => handleDelete(item)} />
@@ -91,7 +91,7 @@ const SimpleStory = (props: Omit<ListRootProps<TestItemType>, 'items'>) => {
91
91
  {({ items }) => (
92
92
  <div role='list' className='w-full h-full overflow-auto'>
93
93
  {items?.map((item) => (
94
- <List.Item<TestItemType> key={item.id} item={item} classNames={mx(grid, ghostHover)}>
94
+ <List.Item<TestItemType> key={item.id} item={item} classNames={mx(grid)}>
95
95
  <List.ItemDragHandle />
96
96
  <List.ItemTitle>{item.name}</List.ItemTitle>
97
97
  <List.ItemDeleteButton />
@@ -4,7 +4,7 @@
4
4
 
5
5
  import {
6
6
  ListItem,
7
- ListItemButton,
7
+ ListItemIconButton,
8
8
  ListItemDeleteButton,
9
9
  ListItemDragHandle,
10
10
  ListItemDragPreview,
@@ -33,8 +33,8 @@ export const List = {
33
33
  ItemDragPreview: ListItemDragPreview,
34
34
  ItemWrapper: ListItemWrapper,
35
35
  ItemDragHandle: ListItemDragHandle,
36
+ ItemIconButton: ListItemIconButton,
36
37
  ItemDeleteButton: ListItemDeleteButton,
37
- ItemButton: ListItemButton,
38
38
  ItemTitle: ListItemTitle,
39
39
  };
40
40
 
@@ -11,6 +11,7 @@ import {
11
11
  extractClosestEdge,
12
12
  } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
13
13
  import { createContext } from '@radix-ui/react-context';
14
+ import { Slot } from '@radix-ui/react-slot';
14
15
  import React, {
15
16
  type ComponentProps,
16
17
  type HTMLAttributes,
@@ -80,6 +81,8 @@ export type ListItemProps<T extends ListItemRecord> = ThemedClassName<
80
81
  PropsWithChildren<
81
82
  {
82
83
  item: T;
84
+ asChild?: boolean;
85
+ selected?: boolean;
83
86
  } & HTMLAttributes<HTMLDivElement>
84
87
  >
85
88
  >;
@@ -87,7 +90,15 @@ export type ListItemProps<T extends ListItemRecord> = ThemedClassName<
87
90
  /**
88
91
  * Draggable list item.
89
92
  */
90
- export const ListItem = <T extends ListItemRecord>({ children, classNames, item, ...props }: ListItemProps<T>) => {
93
+ export const ListItem = <T extends ListItemRecord>({
94
+ children,
95
+ classNames,
96
+ item,
97
+ asChild,
98
+ selected,
99
+ ...props
100
+ }: ListItemProps<T>) => {
101
+ const Comp = asChild ? Slot : 'div';
91
102
  const { isItem, readonly, dragPreview, setState: setRootState } = useListContext(LIST_ITEM_NAME);
92
103
  const ref = useRef<HTMLDivElement | null>(null);
93
104
  const dragHandleRef = useRef<HTMLElement | null>(null);
@@ -170,12 +181,18 @@ export const ListItem = <T extends ListItemRecord>({ children, classNames, item,
170
181
 
171
182
  return (
172
183
  <ListItemProvider item={item} dragHandleRef={dragHandleRef}>
173
- <div ref={ref} role='listitem' className={mx('flex relative', classNames, stateStyles[state.type])} {...props}>
184
+ <Comp
185
+ ref={ref}
186
+ role='listitem'
187
+ aria-selected={selected}
188
+ className={mx('relative dx-selected', classNames, stateStyles[state.type])}
189
+ {...props}
190
+ >
174
191
  {children}
175
- {state.type === 'w-dragging-over' && state.closestEdge && (
176
- <NaturalListItem.DropIndicator edge={state.closestEdge} />
177
- )}
178
- </div>
192
+ </Comp>
193
+ {state.type === 'w-dragging-over' && state.closestEdge && (
194
+ <NaturalListItem.DropIndicator edge={state.closestEdge} />
195
+ )}
179
196
  </ListItemProvider>
180
197
  );
181
198
  };
@@ -184,47 +201,48 @@ export const ListItem = <T extends ListItemRecord>({ children, classNames, item,
184
201
  // List item components
185
202
  //
186
203
 
187
- export const ListItemDeleteButton = ({
204
+ export const ListItemIconButton = ({
188
205
  autoHide = true,
206
+ iconOnly = true,
207
+ variant = 'ghost',
189
208
  classNames,
190
209
  disabled,
191
- icon = 'ph--x--regular',
192
- label,
193
210
  ...props
194
- }: Partial<Pick<IconButtonProps, 'icon'>> &
195
- Omit<IconButtonProps, 'icon' | 'label'> & { autoHide?: boolean; label?: string }) => {
196
- const { state } = useListContext('DELETE_BUTTON');
211
+ }: IconButtonProps & { autoHide?: boolean }) => {
212
+ const { state } = useListContext('ITEM_BUTTON');
197
213
  const isDisabled = state.type !== 'idle' || disabled;
198
- const { t } = useTranslation(osTranslations);
199
214
  return (
200
215
  <IconButton
201
- iconOnly
202
- variant='ghost'
203
216
  {...props}
204
- icon={icon}
205
217
  disabled={isDisabled}
206
- label={label ?? t('delete label')}
218
+ iconOnly={iconOnly}
219
+ variant={variant}
207
220
  classNames={[classNames, autoHide && disabled && 'hidden']}
208
221
  />
209
222
  );
210
223
  };
211
224
 
212
- export const ListItemButton = ({
225
+ // TODO(burdon): Generalize to action button.
226
+ export const ListItemDeleteButton = ({
213
227
  autoHide = true,
214
- iconOnly = true,
215
- variant = 'ghost',
216
228
  classNames,
217
229
  disabled,
230
+ icon = 'ph--x--regular',
231
+ label,
218
232
  ...props
219
- }: IconButtonProps & { autoHide?: boolean }) => {
220
- const { state } = useListContext('ITEM_BUTTON');
233
+ }: Partial<Pick<IconButtonProps, 'icon'>> &
234
+ Omit<IconButtonProps, 'icon' | 'label'> & { autoHide?: boolean; label?: string }) => {
235
+ const { state } = useListContext('DELETE_BUTTON');
221
236
  const isDisabled = state.type !== 'idle' || disabled;
237
+ const { t } = useTranslation(osTranslations);
222
238
  return (
223
239
  <IconButton
240
+ iconOnly
241
+ variant='ghost'
224
242
  {...props}
243
+ icon={icon}
225
244
  disabled={isDisabled}
226
- iconOnly={iconOnly}
227
- variant={variant}
245
+ label={label ?? t('delete label')}
228
246
  classNames={[classNames, autoHide && disabled && 'hidden']}
229
247
  />
230
248
  );
@@ -12,6 +12,10 @@ export type TreeItemDataProps = {
12
12
  id: string;
13
13
  label: Label;
14
14
  parentOf?: string[];
15
+ /** When `false`, the item cannot be dragged (overrides tree-level `draggable`). */
16
+ draggable?: boolean;
17
+ /** When `false`, the item does not participate as a drop target. */
18
+ droppable?: boolean;
15
19
  className?: string;
16
20
  headingClassName?: string;
17
21
  icon?: string;
@@ -107,9 +107,19 @@ const RawTreeItem = <T extends { id: string } = any>({
107
107
  } = useTree();
108
108
  const path = useMemo(() => [...pathProp, item.id], [pathProp, item.id]);
109
109
 
110
- const { id, parentOf, label, className, headingClassName, icon, iconHue, disabled, testId } = useAtomValue(
111
- itemPropsAtom(path),
112
- );
110
+ const {
111
+ id,
112
+ parentOf,
113
+ draggable: itemDraggable,
114
+ droppable: itemDroppable,
115
+ label,
116
+ className,
117
+ headingClassName,
118
+ icon,
119
+ iconHue,
120
+ disabled,
121
+ testId,
122
+ } = useAtomValue(itemPropsAtom(path));
113
123
  const childIds = useAtomValue(childIdsAtom(item.id));
114
124
  const open = useAtomValue(itemOpenAtom(path));
115
125
  const current = useAtomValue(itemCurrentAtom(path));
@@ -127,6 +137,9 @@ const RawTreeItem = <T extends { id: string } = any>({
127
137
  }
128
138
  }, []);
129
139
 
140
+ const isItemDraggable = draggableProp && itemDraggable !== false;
141
+ const isItemDroppable = itemDroppable !== false;
142
+
130
143
  useEffect(() => {
131
144
  if (!draggableProp) {
132
145
  return;
@@ -134,10 +147,9 @@ const RawTreeItem = <T extends { id: string } = any>({
134
147
 
135
148
  invariant(buttonRef.current);
136
149
 
137
- // https://atlassian.design/components/pragmatic-drag-and-drop/core-package/adapters/element/about
138
- return combine(
150
+ const makeDraggable = () =>
139
151
  draggable({
140
- element: buttonRef.current,
152
+ element: buttonRef.current!,
141
153
  getInitialData: () => data,
142
154
  onDragStart: () => {
143
155
  setState('dragging');
@@ -152,62 +164,72 @@ const RawTreeItem = <T extends { id: string } = any>({
152
164
  onOpenChange?.({ item, path, open: true });
153
165
  }
154
166
  },
155
- }),
156
- // https://github.com/atlassian/pragmatic-drag-and-drop/blob/main/packages/hitbox/constellation/index/about.mdx
157
- dropTargetForElements({
158
- element: buttonRef.current,
159
- getData: ({ input, element }) => {
160
- return attachInstruction(data, {
161
- input,
162
- element,
163
- indentPerLevel: DEFAULT_INDENTATION,
164
- currentLevel: level,
165
- mode,
166
- block: isBranch ? [] : ['make-child'],
167
- });
168
- },
169
- canDrop: ({ source }) => {
170
- const _canDrop = canDrop ?? (() => true);
171
- return source.element !== buttonRef.current && _canDrop({ source: source.data as TreeData, target: data });
172
- },
173
- getIsSticky: () => true,
174
- onDrag: ({ self, source }) => {
175
- const desired = extractInstruction(self.data);
176
- const block =
177
- desired && blockInstruction?.({ instruction: desired, source: source.data as TreeData, target: data });
178
- const instruction: Instruction | null =
179
- block && desired.type !== 'instruction-blocked' ? { type: 'instruction-blocked', desired } : desired;
180
-
181
- if (source.data.id !== id) {
182
- if (instruction?.type === 'make-child' && isBranch && !open && !cancelExpandRef.current) {
183
- cancelExpandRef.current = setTimeout(() => {
184
- onOpenChange?.({ item, path, open: true });
185
- }, 500);
186
- }
187
-
188
- if (instruction?.type !== 'make-child') {
189
- cancelExpand();
190
- }
191
-
192
- setInstruction(instruction);
193
- } else if (instruction?.type === 'reparent') {
194
- // TODO(wittjosiah): This is not occurring in the current implementation.
195
- setInstruction(instruction);
196
- } else {
197
- setInstruction(null);
167
+ });
168
+
169
+ if (!isItemDroppable) {
170
+ return isItemDraggable ? makeDraggable() : undefined;
171
+ }
172
+
173
+ const dropTarget = dropTargetForElements({
174
+ element: buttonRef.current,
175
+ getData: ({ input, element }) => {
176
+ return attachInstruction(data, {
177
+ input,
178
+ element,
179
+ indentPerLevel: DEFAULT_INDENTATION,
180
+ currentLevel: level,
181
+ mode,
182
+ block: isBranch ? [] : ['make-child'],
183
+ });
184
+ },
185
+ canDrop: ({ source }) => {
186
+ const _canDrop = canDrop ?? (() => true);
187
+ return source.element !== buttonRef.current && _canDrop({ source: source.data as TreeData, target: data });
188
+ },
189
+ getIsSticky: () => true,
190
+ onDrag: ({ self, source }) => {
191
+ const desired = extractInstruction(self.data);
192
+ const block =
193
+ desired && blockInstruction?.({ instruction: desired, source: source.data as TreeData, target: data });
194
+ const instruction: Instruction | null =
195
+ block && desired.type !== 'instruction-blocked' ? { type: 'instruction-blocked', desired } : desired;
196
+
197
+ if (source.data.id !== id) {
198
+ if (instruction?.type === 'make-child' && isBranch && !open && !cancelExpandRef.current) {
199
+ cancelExpandRef.current = setTimeout(() => {
200
+ onOpenChange?.({ item, path, open: true });
201
+ }, 500);
198
202
  }
199
- },
200
- onDragLeave: () => {
201
- cancelExpand();
202
- setInstruction(null);
203
- },
204
- onDrop: () => {
205
- cancelExpand();
203
+
204
+ if (instruction?.type !== 'make-child') {
205
+ cancelExpand();
206
+ }
207
+
208
+ setInstruction(instruction);
209
+ } else if (instruction?.type === 'reparent') {
210
+ // TODO(wittjosiah): This is not occurring in the current implementation.
211
+ setInstruction(instruction);
212
+ } else {
206
213
  setInstruction(null);
207
- },
208
- }),
209
- );
210
- }, [draggableProp, item, id, mode, path, open, blockInstruction, canDrop]);
214
+ }
215
+ },
216
+ onDragLeave: () => {
217
+ cancelExpand();
218
+ setInstruction(null);
219
+ },
220
+ onDrop: () => {
221
+ cancelExpand();
222
+ setInstruction(null);
223
+ },
224
+ });
225
+
226
+ if (!isItemDraggable) {
227
+ return dropTarget;
228
+ }
229
+
230
+ // https://atlassian.design/components/pragmatic-drag-and-drop/core-package/adapters/element/about
231
+ return combine(makeDraggable(), dropTarget);
232
+ }, [draggableProp, isItemDraggable, isItemDroppable, item, id, mode, path, open, blockInstruction, canDrop]);
211
233
 
212
234
  // Cancel expand on unmount.
213
235
  useEffect(() => () => cancelExpand(), [cancelExpand]);