@dxos/react-list 0.1.5
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/LICENSE +8 -0
- package/dist/src/components/List/List.d.ts +8 -0
- package/dist/src/components/List/List.d.ts.map +1 -0
- package/dist/src/components/List/List.js +98 -0
- package/dist/src/components/List/List.js.map +1 -0
- package/dist/src/components/List/List.stories.d.ts +14 -0
- package/dist/src/components/List/List.stories.d.ts.map +1 -0
- package/dist/src/components/List/List.stories.js +116 -0
- package/dist/src/components/List/List.stories.js.map +1 -0
- package/dist/src/components/List/index.d.ts +2 -0
- package/dist/src/components/List/index.d.ts.map +1 -0
- package/dist/src/components/List/index.js +21 -0
- package/dist/src/components/List/index.js.map +1 -0
- package/dist/src/components/ListPrimitive/ListPrimitive.d.ts +52 -0
- package/dist/src/components/ListPrimitive/ListPrimitive.d.ts.map +1 -0
- package/dist/src/components/ListPrimitive/ListPrimitive.js +184 -0
- package/dist/src/components/ListPrimitive/ListPrimitive.js.map +1 -0
- package/dist/src/components/ListPrimitive/ListPrimitive.stories.d.ts +10 -0
- package/dist/src/components/ListPrimitive/ListPrimitive.stories.d.ts.map +1 -0
- package/dist/src/components/ListPrimitive/ListPrimitive.stories.js +42 -0
- package/dist/src/components/ListPrimitive/ListPrimitive.stories.js.map +1 -0
- package/dist/src/components/ListPrimitive/index.d.ts +2 -0
- package/dist/src/components/ListPrimitive/index.d.ts.map +1 -0
- package/dist/src/components/ListPrimitive/index.js +21 -0
- package/dist/src/components/ListPrimitive/index.js.map +1 -0
- package/dist/src/components/index.d.ts +2 -0
- package/dist/src/components/index.d.ts.map +1 -0
- package/dist/src/components/index.js +21 -0
- package/dist/src/components/index.js.map +1 -0
- package/dist/src/hooks/index.d.ts +2 -0
- package/dist/src/hooks/index.d.ts.map +1 -0
- package/dist/src/hooks/index.js +21 -0
- package/dist/src/hooks/index.js.map +1 -0
- package/dist/src/hooks/usePropStatefully.d.ts +6 -0
- package/dist/src/hooks/usePropStatefully.d.ts.map +1 -0
- package/dist/src/hooks/usePropStatefully.js +30 -0
- package/dist/src/hooks/usePropStatefully.js.map +1 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +22 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/model.d.ts +3 -0
- package/dist/src/model.d.ts.map +1 -0
- package/dist/src/model.js +9 -0
- package/dist/src/model.js.map +1 -0
- package/dist/src/testing/index.d.ts +2 -0
- package/dist/src/testing/index.d.ts.map +1 -0
- package/dist/src/testing/index.js +21 -0
- package/dist/src/testing/index.js.map +1 -0
- package/dist/src/testing/templateForComponent.d.ts +4 -0
- package/dist/src/testing/templateForComponent.d.ts.map +1 -0
- package/dist/src/testing/templateForComponent.js +18 -0
- package/dist/src/testing/templateForComponent.js.map +1 -0
- package/package.json +42 -0
- package/src/components/List/List.stories.tsx +123 -0
- package/src/components/List/List.tsx +111 -0
- package/src/components/List/index.ts +5 -0
- package/src/components/ListPrimitive/ListPrimitive.stories.tsx +43 -0
- package/src/components/ListPrimitive/ListPrimitive.tsx +376 -0
- package/src/components/ListPrimitive/index.ts +5 -0
- package/src/components/index.ts +5 -0
- package/src/hooks/index.ts +5 -0
- package/src/hooks/usePropStatefully.ts +33 -0
- package/src/index.ts +6 -0
- package/src/model.ts +6 -0
- package/src/testing/index.ts +5 -0
- package/src/testing/templateForComponent.tsx +16 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import cx from 'classnames';
|
|
6
|
+
import { Minus, Plus } from 'phosphor-react';
|
|
7
|
+
import React, { ComponentProps, useCallback, useState } from 'react';
|
|
8
|
+
|
|
9
|
+
import { defaultGroup, defaultHover } from '@dxos/react-ui';
|
|
10
|
+
import { defaultFocus, Input, useTranslation, getSize, Button, randomString } from '@dxos/react-uikit';
|
|
11
|
+
|
|
12
|
+
import { usePropStatefully } from '../../hooks';
|
|
13
|
+
|
|
14
|
+
type ListId = string;
|
|
15
|
+
type ListItemId = string;
|
|
16
|
+
|
|
17
|
+
export interface ListItemProps {
|
|
18
|
+
id: ListItemId;
|
|
19
|
+
title?: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
annotations?: Record<string, string | boolean>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type ListItems = Record<ListItemId, Omit<ListItemProps, 'id'>>;
|
|
25
|
+
|
|
26
|
+
interface SharedListActionProps {
|
|
27
|
+
listId: ListId;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface SharedListItemActionProps extends SharedListActionProps {
|
|
31
|
+
listItemId: ListItemId;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ListItemChangedAction extends SharedListItemActionProps {
|
|
35
|
+
next: Partial<ListItemProps>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ListChangedAction extends SharedListActionProps {
|
|
39
|
+
next: Partial<ListPrimitiveProps>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ListItemCreatedAction extends SharedListActionProps {
|
|
43
|
+
created: Pick<ListItemProps, 'id'> & Partial<Omit<ListItemProps, 'id'>>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ListItemDeletedAction extends SharedListActionProps {
|
|
47
|
+
deleted: Pick<ListItemProps, 'id'> & Partial<Omit<ListItemProps, 'id'>>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type ListAction = ListItemChangedAction | ListChangedAction | ListItemCreatedAction | ListItemDeletedAction;
|
|
51
|
+
|
|
52
|
+
export const isListItemChangedAction = (o: any): o is ListItemChangedAction => 'listItemId' in o;
|
|
53
|
+
export const isListItemCreatedAction = (o: any): o is ListItemCreatedAction => 'created' in o;
|
|
54
|
+
export const isListItemDeletedAction = (o: any): o is ListItemDeletedAction => 'deleted' in o;
|
|
55
|
+
|
|
56
|
+
export interface ListPrimitiveProps {
|
|
57
|
+
id: ListId;
|
|
58
|
+
title?: string;
|
|
59
|
+
description?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ListPrimitiveComponentProps extends ListPrimitiveProps, Omit<ComponentProps<'div'>, 'id'> {
|
|
63
|
+
items: ListItems;
|
|
64
|
+
onAction?: (action: ListAction) => void;
|
|
65
|
+
createListItemId?: () => Promise<ListItemId>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface ListItemPrimitiveComponentProps extends ListItemProps, Omit<ComponentProps<'li'>, 'id' | 'title'> {
|
|
69
|
+
updateItem: (item: ListItemProps) => void;
|
|
70
|
+
updateOrder: (id: ListItemId, delta: number) => void;
|
|
71
|
+
deleteItem: (id: ListItemId) => void;
|
|
72
|
+
isFirst?: boolean;
|
|
73
|
+
isLast?: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const defaultCreateListItemId = async () => randomString(8);
|
|
77
|
+
|
|
78
|
+
const ListItemPrimitive = ({
|
|
79
|
+
id,
|
|
80
|
+
title: propsTitle,
|
|
81
|
+
description: propsDescription,
|
|
82
|
+
annotations: propsAnnotations,
|
|
83
|
+
updateItem,
|
|
84
|
+
deleteItem,
|
|
85
|
+
updateOrder,
|
|
86
|
+
isFirst,
|
|
87
|
+
isLast
|
|
88
|
+
}: ListItemPrimitiveComponentProps) => {
|
|
89
|
+
const checkId = `${id}__checkbox`;
|
|
90
|
+
const labelId = `${id}__title`;
|
|
91
|
+
const descriptionId = `${id}__description`;
|
|
92
|
+
|
|
93
|
+
const { t } = useTranslation('appkit');
|
|
94
|
+
|
|
95
|
+
const rejectTextUpdatesWhenFocused = useCallback(
|
|
96
|
+
(a: string, b: string) => {
|
|
97
|
+
return document.activeElement?.getAttribute('data-itemid') === id ? true : a === b;
|
|
98
|
+
},
|
|
99
|
+
[id]
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const [title, setTitle, titleSession] = usePropStatefully<string>(propsTitle ?? '', rejectTextUpdatesWhenFocused);
|
|
103
|
+
|
|
104
|
+
const [description, _setDescription, _descriptionSession] = usePropStatefully<string>(
|
|
105
|
+
propsDescription ?? '',
|
|
106
|
+
rejectTextUpdatesWhenFocused
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const [annotations, setAnnotations] = useState(propsAnnotations ?? {});
|
|
110
|
+
const isDone = annotations?.state === 'done';
|
|
111
|
+
|
|
112
|
+
const onChangeTitle = useCallback((nextValue: string) => {
|
|
113
|
+
setTitle(nextValue);
|
|
114
|
+
updateItem({ id, title: nextValue });
|
|
115
|
+
}, []);
|
|
116
|
+
|
|
117
|
+
const _onChangeDescription = useCallback((nextValue: string) => {
|
|
118
|
+
_setDescription(nextValue);
|
|
119
|
+
updateItem({ id, description: nextValue });
|
|
120
|
+
}, []);
|
|
121
|
+
|
|
122
|
+
const _onClickMoveUp = useCallback(() => {
|
|
123
|
+
updateOrder(id, -1);
|
|
124
|
+
}, [updateOrder, id]);
|
|
125
|
+
const _onClickMoveDown = useCallback(() => {
|
|
126
|
+
updateOrder(id, 1);
|
|
127
|
+
}, [updateOrder, id]);
|
|
128
|
+
|
|
129
|
+
const onChangeCheckbox = useCallback(() => {
|
|
130
|
+
const nextAnnotations = { ...annotations, state: isDone ? 'init' : 'done' };
|
|
131
|
+
setAnnotations(nextAnnotations);
|
|
132
|
+
updateItem({ id, annotations: nextAnnotations });
|
|
133
|
+
}, [annotations, isDone]);
|
|
134
|
+
|
|
135
|
+
const onClickDelete = useCallback(() => {
|
|
136
|
+
deleteItem(id);
|
|
137
|
+
}, [deleteItem, id]);
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<li
|
|
141
|
+
key={id}
|
|
142
|
+
className='flex items-center gap-2 pli-4'
|
|
143
|
+
aria-labelledby={labelId}
|
|
144
|
+
{...(description && { 'aria-describedby': descriptionId })}
|
|
145
|
+
>
|
|
146
|
+
<input
|
|
147
|
+
key={titleSession}
|
|
148
|
+
{...{
|
|
149
|
+
type: 'checkbox',
|
|
150
|
+
id: checkId,
|
|
151
|
+
...(description && {
|
|
152
|
+
'aria-describedby': descriptionId
|
|
153
|
+
}),
|
|
154
|
+
className: cx(
|
|
155
|
+
getSize(4),
|
|
156
|
+
'text-primary-600 bg-neutral-50 rounded border-neutral-300 dark:bg-neutral-800 dark:border-neutral-600 cursor-pointer',
|
|
157
|
+
defaultFocus,
|
|
158
|
+
defaultHover({})
|
|
159
|
+
),
|
|
160
|
+
onChange: onChangeCheckbox,
|
|
161
|
+
checked: isDone
|
|
162
|
+
}}
|
|
163
|
+
/>
|
|
164
|
+
<label htmlFor={checkId} id={labelId} className='sr-only'>
|
|
165
|
+
{title}
|
|
166
|
+
</label>
|
|
167
|
+
<span id={descriptionId} className='sr-only'>
|
|
168
|
+
{description}
|
|
169
|
+
</span>
|
|
170
|
+
<div role='none' className='grow'>
|
|
171
|
+
<Input
|
|
172
|
+
label={t('list item title label')}
|
|
173
|
+
placeholder={t('list item title placeholder')}
|
|
174
|
+
labelVisuallyHidden
|
|
175
|
+
initialValue={title}
|
|
176
|
+
onChange={onChangeTitle}
|
|
177
|
+
data-itemid={id}
|
|
178
|
+
/>
|
|
179
|
+
{/* TODO(thure): Re-enable this when descriptions become relevant */}
|
|
180
|
+
{/* <Input */}
|
|
181
|
+
{/* key={descriptionSession} */}
|
|
182
|
+
{/* label={t('list item description label')} */}
|
|
183
|
+
{/* placeholder={t('list item description placeholder')} */}
|
|
184
|
+
{/* labelVisuallyHidden */}
|
|
185
|
+
{/* initialValue={description} */}
|
|
186
|
+
{/* onChange={onChangeDescription} */}
|
|
187
|
+
{/* data-itemid={id} */}
|
|
188
|
+
{/* /> */}
|
|
189
|
+
</div>
|
|
190
|
+
{/* TODO(thure): Restore these, or implement drag & drop, when how best to change order in Echo is clarified. */}
|
|
191
|
+
{/* <ButtonGroup className='flex flex-col items-stretch'> */}
|
|
192
|
+
{/* <Button compact rounding='rounded-bs-md border-be-0' disabled={isFirst} onClick={onClickMoveUp}> */}
|
|
193
|
+
{/* <CaretUp /> */}
|
|
194
|
+
{/* <span className='sr-only'>{t('move list item up label')}</span> */}
|
|
195
|
+
{/* </Button> */}
|
|
196
|
+
{/* <Button compact rounding='rounded-be-md' disabled={isLast} onClick={onClickMoveDown}> */}
|
|
197
|
+
{/* <CaretDown /> */}
|
|
198
|
+
{/* <span className='sr-only'>{t('move list item down label')}</span> */}
|
|
199
|
+
{/* </Button> */}
|
|
200
|
+
{/* </ButtonGroup> */}
|
|
201
|
+
<Button compact onClick={onClickDelete}>
|
|
202
|
+
<Minus />
|
|
203
|
+
<span className='sr-only'>{t('delete list item label')}</span>
|
|
204
|
+
</Button>
|
|
205
|
+
</li>
|
|
206
|
+
);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const itemsEquivalent = (a: ListItems, b: ListItems) => {
|
|
210
|
+
const aKeys = Object.keys(a);
|
|
211
|
+
const bKeys = Object.keys(b);
|
|
212
|
+
for (let i = 0; i < aKeys.length; i++) {
|
|
213
|
+
const key = aKeys[i];
|
|
214
|
+
if (key !== bKeys[i] || a[key].title !== b[key].title || a[key].annotations?.state !== b[key].annotations?.state) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return true;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
export const ListPrimitive = ({
|
|
222
|
+
id: listId,
|
|
223
|
+
title: propsTitle,
|
|
224
|
+
description: propsDescription,
|
|
225
|
+
items: propsItems,
|
|
226
|
+
onAction,
|
|
227
|
+
createListItemId = defaultCreateListItemId,
|
|
228
|
+
...divProps
|
|
229
|
+
}: ListPrimitiveComponentProps) => {
|
|
230
|
+
const { t } = useTranslation('appkit');
|
|
231
|
+
|
|
232
|
+
const [creating, setCreating] = useState(false);
|
|
233
|
+
|
|
234
|
+
const rejectTextUpdatesWhenFocused = useCallback(
|
|
235
|
+
(a: string, b: string) => {
|
|
236
|
+
return document.activeElement?.getAttribute('data-itemid') === listId ? true : a === b;
|
|
237
|
+
},
|
|
238
|
+
[listId]
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const titleId = `${listId}__title`;
|
|
242
|
+
const [title, setTitle, titleSession] = usePropStatefully(propsTitle ?? '', rejectTextUpdatesWhenFocused);
|
|
243
|
+
const onChangeTitle = useCallback(
|
|
244
|
+
(nextValue: string) => {
|
|
245
|
+
setTitle(nextValue);
|
|
246
|
+
onAction?.({ listId, next: { title: nextValue } });
|
|
247
|
+
},
|
|
248
|
+
[onAction, listId]
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const descriptionId = `${listId}__description`;
|
|
252
|
+
const [description, setDescription, _descriptionSession] = usePropStatefully(
|
|
253
|
+
propsDescription ?? '',
|
|
254
|
+
rejectTextUpdatesWhenFocused
|
|
255
|
+
);
|
|
256
|
+
const _onChangeDescription = useCallback(
|
|
257
|
+
(nextValue: string) => {
|
|
258
|
+
setDescription(nextValue);
|
|
259
|
+
onAction?.({ listId, next: { description: nextValue } });
|
|
260
|
+
},
|
|
261
|
+
[onAction, listId]
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const [items, setItems, itemsSession] = usePropStatefully(propsItems ?? {}, itemsEquivalent);
|
|
265
|
+
const order = Object.keys(items);
|
|
266
|
+
|
|
267
|
+
const updateItem = useCallback(
|
|
268
|
+
({ id, ...next }: Pick<ListItemProps, 'id'> & Partial<Omit<ListItemProps, 'id'>>) => {
|
|
269
|
+
setItems(Object.assign(items, { [id]: { ...items[id], ...next } }));
|
|
270
|
+
onAction?.({ listId, listItemId: id, next });
|
|
271
|
+
},
|
|
272
|
+
[items, onAction]
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
const updateOrder = useCallback(
|
|
276
|
+
(id: ListItemId, delta: number) => {
|
|
277
|
+
const order = Object.keys(items);
|
|
278
|
+
const fromIndex = order.indexOf(id);
|
|
279
|
+
const toIndex = fromIndex + delta;
|
|
280
|
+
const nextOrder = Array.from(order);
|
|
281
|
+
nextOrder.splice(fromIndex, 1);
|
|
282
|
+
nextOrder.splice(toIndex, 0, id);
|
|
283
|
+
const nextItems = nextOrder.reduce((acc: ListItems, listItemId) => {
|
|
284
|
+
acc[listItemId] = items[listItemId];
|
|
285
|
+
return acc;
|
|
286
|
+
}, {});
|
|
287
|
+
setItems(nextItems);
|
|
288
|
+
onAction?.({ listId, next: {} });
|
|
289
|
+
},
|
|
290
|
+
[items, onAction]
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const deleteItem = useCallback(
|
|
294
|
+
(id: ListItemId) => {
|
|
295
|
+
if (items[id]) {
|
|
296
|
+
const { [id]: deletedItem, ...nextItems } = items;
|
|
297
|
+
setItems(nextItems);
|
|
298
|
+
onAction?.({ listId, next: {}, deleted: { id, ...deletedItem } });
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
[items, onAction]
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
const createItem = useCallback(() => {
|
|
305
|
+
setCreating(true);
|
|
306
|
+
return createListItemId()
|
|
307
|
+
.then((id) => {
|
|
308
|
+
const next = {
|
|
309
|
+
items: { ...items, [id]: {} }
|
|
310
|
+
};
|
|
311
|
+
const created = { id };
|
|
312
|
+
setItems(next.items);
|
|
313
|
+
onAction?.({ listId, next: {}, created });
|
|
314
|
+
})
|
|
315
|
+
.finally(() => setCreating(false));
|
|
316
|
+
}, [items, createListItemId, onAction]);
|
|
317
|
+
|
|
318
|
+
return (
|
|
319
|
+
<div
|
|
320
|
+
role='group'
|
|
321
|
+
{...divProps}
|
|
322
|
+
className={cx(defaultGroup({ elevation: 3, spacing: '' }), 'plb-2', divProps.className)}
|
|
323
|
+
aria-labelledby={titleId}
|
|
324
|
+
{...(description && { 'aria-describedby': descriptionId })}
|
|
325
|
+
>
|
|
326
|
+
<span id={titleId} className='sr-only'>
|
|
327
|
+
{title}
|
|
328
|
+
</span>
|
|
329
|
+
{description && (
|
|
330
|
+
<span id={descriptionId} className='sr-only'>
|
|
331
|
+
{description}
|
|
332
|
+
</span>
|
|
333
|
+
)}
|
|
334
|
+
<Input
|
|
335
|
+
key={titleSession}
|
|
336
|
+
label={t('list title label')}
|
|
337
|
+
placeholder={t('list title placeholder')}
|
|
338
|
+
labelVisuallyHidden
|
|
339
|
+
initialValue={title}
|
|
340
|
+
onChange={onChangeTitle}
|
|
341
|
+
className='mli-4'
|
|
342
|
+
data-itemid={listId}
|
|
343
|
+
/>
|
|
344
|
+
{/* TODO(thure): Re-enable this when relevant */}
|
|
345
|
+
{/* <Input */}
|
|
346
|
+
{/* key={descriptionSession} */}
|
|
347
|
+
{/* label={t('list description label')} */}
|
|
348
|
+
{/* placeholder={t('list description placeholder')} */}
|
|
349
|
+
{/* labelVisuallyHidden */}
|
|
350
|
+
{/* initialValue={description} */}
|
|
351
|
+
{/* onChange={onChangeDescription} */}
|
|
352
|
+
{/* className='mli-4' */}
|
|
353
|
+
{/* data-itemid={listId} */}
|
|
354
|
+
{/* /> */}
|
|
355
|
+
<ol className='contents'>
|
|
356
|
+
{order.map((listItemId, index) => {
|
|
357
|
+
return (
|
|
358
|
+
<ListItemPrimitive
|
|
359
|
+
key={`${itemsSession}__${listItemId}`}
|
|
360
|
+
isFirst={index === 0}
|
|
361
|
+
isLast={index === order.length - 1}
|
|
362
|
+
{...{ id: listItemId, updateItem, updateOrder, deleteItem }}
|
|
363
|
+
{...items[listItemId]}
|
|
364
|
+
/>
|
|
365
|
+
);
|
|
366
|
+
})}
|
|
367
|
+
</ol>
|
|
368
|
+
<div role='none' className='mli-4 mlb-4'>
|
|
369
|
+
<Button className='is-full' onClick={createItem} disabled={creating}>
|
|
370
|
+
<Plus />
|
|
371
|
+
<span className='sr-only'>{t('add list item label')}</span>
|
|
372
|
+
</Button>
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
);
|
|
376
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { useEffect, useRef, useState, Dispatch, SetStateAction } from 'react';
|
|
6
|
+
|
|
7
|
+
import { randomString } from '@dxos/react-uikit';
|
|
8
|
+
|
|
9
|
+
const isEqual = <T>(a: T, b: T) => a === b;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @deprecated
|
|
13
|
+
*/
|
|
14
|
+
export const usePropStatefully = <T>(
|
|
15
|
+
propsValue: T,
|
|
16
|
+
equivalentPredicate?: (a: T, b: T) => boolean
|
|
17
|
+
): [T, Dispatch<SetStateAction<T>>, string] => {
|
|
18
|
+
const [value, setValue] = useState<T>(propsValue);
|
|
19
|
+
const [valueSession, setValueSession] = useState(randomString());
|
|
20
|
+
const mounted = useRef(false);
|
|
21
|
+
const predicate = equivalentPredicate ?? isEqual<T>;
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (mounted.current && !predicate(propsValue, value)) {
|
|
25
|
+
setValue(propsValue);
|
|
26
|
+
setValueSession(randomString());
|
|
27
|
+
} else {
|
|
28
|
+
mounted.current = true;
|
|
29
|
+
}
|
|
30
|
+
}, [propsValue]);
|
|
31
|
+
|
|
32
|
+
return [value, setValue, valueSession];
|
|
33
|
+
};
|
package/src/index.ts
ADDED
package/src/model.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { ComponentStory, Story } from '@storybook/react';
|
|
6
|
+
import React, { FC } from 'react';
|
|
7
|
+
|
|
8
|
+
export const templateForComponent =
|
|
9
|
+
<P extends {}>(Component: FC<P>) =>
|
|
10
|
+
(props: P): Story<P> => {
|
|
11
|
+
const template: ComponentStory<typeof Component> = (args) => <Component {...args} />;
|
|
12
|
+
|
|
13
|
+
const story = template.bind({});
|
|
14
|
+
story.args = props;
|
|
15
|
+
return story;
|
|
16
|
+
};
|