@centreon/ui 26.10.4 → 26.10.6
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 +1 -1
- package/src/components/SortableEntriesList/Row.tsx +117 -0
- package/src/components/SortableEntriesList/SortableEntriesList.cypress.spec.tsx +792 -0
- package/src/components/SortableEntriesList/SortableEntriesList.stories.tsx +325 -0
- package/src/components/SortableEntriesList/SortableEntriesList.tsx +199 -0
- package/src/components/SortableEntriesList/index.ts +3 -0
- package/src/components/SortableEntriesList/models.ts +20 -0
- package/src/components/SortableEntriesList/translatedLabels.ts +12 -0
- package/src/components/SortableEntriesList/useMaxVisibleRows.ts +77 -0
- package/src/components/SortableEntriesList/useSortableEntries.ts +145 -0
- package/src/components/index.ts +1 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { DragEndEvent } from '@dnd-kit/core';
|
|
2
|
+
import { arrayMove } from '@dnd-kit/sortable';
|
|
3
|
+
import {
|
|
4
|
+
type RefObject,
|
|
5
|
+
useEffect,
|
|
6
|
+
useLayoutEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useRef
|
|
9
|
+
} from 'react';
|
|
10
|
+
|
|
11
|
+
// A value paired with the stable key used for React keys, drag & drop and focus
|
|
12
|
+
// management. Consumers only ever see plain values.
|
|
13
|
+
interface Entry<T> {
|
|
14
|
+
id: string;
|
|
15
|
+
value: T;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const generateEntryId = (): string =>
|
|
19
|
+
`${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
20
|
+
|
|
21
|
+
const focusableSelector =
|
|
22
|
+
'input:not([disabled]):not([type="hidden"]), textarea:not([disabled]), select:not([disabled]), button:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
23
|
+
|
|
24
|
+
const getRowSelector = (id: string): string =>
|
|
25
|
+
`[data-entry-id="${CSS.escape(id)}"]`;
|
|
26
|
+
|
|
27
|
+
interface Params<T> {
|
|
28
|
+
addLabel: string;
|
|
29
|
+
createValue: () => T;
|
|
30
|
+
onChange: (values: Array<T>) => void;
|
|
31
|
+
values: Array<T>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface Result<T> {
|
|
35
|
+
addEntry: () => void;
|
|
36
|
+
containerRef: RefObject<HTMLDivElement | null>;
|
|
37
|
+
entries: Array<Entry<T>>;
|
|
38
|
+
removeEntry: (id: string) => void;
|
|
39
|
+
reorderEntries: (event: DragEndEvent) => void;
|
|
40
|
+
updateEntry: (id: string, update: (value: T) => T) => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const useSortableEntries = <T>({
|
|
44
|
+
addLabel,
|
|
45
|
+
createValue,
|
|
46
|
+
onChange,
|
|
47
|
+
values
|
|
48
|
+
}: Params<T>): Result<T> => {
|
|
49
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
50
|
+
// Latest entries, updated as soon as a change is emitted so that several
|
|
51
|
+
// changes fired before the parent re-renders compose instead of overwriting
|
|
52
|
+
// each other.
|
|
53
|
+
const latestEntriesRef = useRef<Array<Entry<T>>>([]);
|
|
54
|
+
const pendingFocusSelectorRef = useRef<string | null>(null);
|
|
55
|
+
|
|
56
|
+
// Ids are kept by position: after a change emitted here, the latest entries
|
|
57
|
+
// are exactly the emitted ones, so every row keeps its id (and its DOM node)
|
|
58
|
+
// even when the parent stores a copy of the values.
|
|
59
|
+
const entries = useMemo(
|
|
60
|
+
() =>
|
|
61
|
+
values.map((value, index) => ({
|
|
62
|
+
id: latestEntriesRef.current[index]?.id ?? generateEntryId(),
|
|
63
|
+
value
|
|
64
|
+
})),
|
|
65
|
+
[values]
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
useLayoutEffect(() => {
|
|
69
|
+
latestEntriesRef.current = entries;
|
|
70
|
+
}, [entries]);
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
const selector = pendingFocusSelectorRef.current;
|
|
74
|
+
|
|
75
|
+
if (!selector) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
pendingFocusSelectorRef.current = null;
|
|
80
|
+
|
|
81
|
+
const element = containerRef.current?.querySelector<HTMLElement>(selector);
|
|
82
|
+
|
|
83
|
+
element?.focus();
|
|
84
|
+
// In a capped list, show the whole row, not only the focused element.
|
|
85
|
+
element?.closest('li')?.scrollIntoView({ block: 'nearest' });
|
|
86
|
+
}, [entries]);
|
|
87
|
+
|
|
88
|
+
const commit = (nextEntries: Array<Entry<T>>): void => {
|
|
89
|
+
latestEntriesRef.current = nextEntries;
|
|
90
|
+
onChange(nextEntries.map(({ value }) => value));
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const updateEntry = (id: string, update: (value: T) => T): void =>
|
|
94
|
+
commit(
|
|
95
|
+
latestEntriesRef.current.map((entry) =>
|
|
96
|
+
entry.id === id ? { ...entry, value: update(entry.value) } : entry
|
|
97
|
+
)
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const addEntry = (): void => {
|
|
101
|
+
const entry = { id: generateEntryId(), value: createValue() };
|
|
102
|
+
|
|
103
|
+
pendingFocusSelectorRef.current = `${getRowSelector(entry.id)} [data-row-fields] :is(${focusableSelector})`;
|
|
104
|
+
commit([...latestEntriesRef.current, entry]);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const removeEntry = (id: string): void => {
|
|
108
|
+
const currentEntries = latestEntriesRef.current;
|
|
109
|
+
const index = currentEntries.findIndex((entry) => entry.id === id);
|
|
110
|
+
// Keep keyboard users in place: focus the row that takes the removed one's
|
|
111
|
+
// position, else the previous row, else the add button.
|
|
112
|
+
const nextFocusedEntry =
|
|
113
|
+
currentEntries[index + 1] ?? currentEntries[index - 1];
|
|
114
|
+
|
|
115
|
+
pendingFocusSelectorRef.current = nextFocusedEntry
|
|
116
|
+
? `${getRowSelector(nextFocusedEntry.id)} [data-row-fields] :is(${focusableSelector})`
|
|
117
|
+
: `button[aria-label="${CSS.escape(addLabel)}"]`;
|
|
118
|
+
commit(currentEntries.filter((entry) => entry.id !== id));
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const reorderEntries = ({ active, over }: DragEndEvent): void => {
|
|
122
|
+
if (!over || active.id === over.id) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const currentEntries = latestEntriesRef.current;
|
|
127
|
+
|
|
128
|
+
commit(
|
|
129
|
+
arrayMove(
|
|
130
|
+
currentEntries,
|
|
131
|
+
currentEntries.findIndex(({ id }) => id === active.id),
|
|
132
|
+
currentEntries.findIndex(({ id }) => id === over.id)
|
|
133
|
+
)
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
addEntry,
|
|
139
|
+
containerRef,
|
|
140
|
+
entries,
|
|
141
|
+
removeEntry,
|
|
142
|
+
reorderEntries,
|
|
143
|
+
updateEntry
|
|
144
|
+
};
|
|
145
|
+
};
|
package/src/components/index.ts
CHANGED