@inkra/react 0.2.0 → 0.4.3
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/README.md +101 -7
- package/dist/Button.js +3 -2
- package/dist/PencilIcon.d.ts +10 -0
- package/dist/PencilIcon.js +83 -39
- package/dist/PencilLine.js +9 -1
- package/dist/PencilText.js +15 -9
- package/dist/accordion/Accordion.js +65 -5
- package/dist/alert-dialog/index.d.ts +1 -1
- package/dist/alert-dialog/index.js +18 -6
- package/dist/audio-player/index.d.ts +2 -0
- package/dist/audio-player/index.js +126 -31
- package/dist/calendar/index.d.ts +4 -1
- package/dist/calendar/index.js +31 -12
- package/dist/carousel/index.js +13 -6
- package/dist/choice-grid/index.d.ts +6 -1
- package/dist/choice-grid/index.js +61 -33
- package/dist/color-picker/GradientEditor.d.ts +39 -0
- package/dist/color-picker/GradientEditor.js +63 -0
- package/dist/color-picker/gradient.d.ts +25 -0
- package/dist/color-picker/gradient.js +60 -0
- package/dist/color-picker/index.d.ts +27 -7
- package/dist/color-picker/index.js +134 -32
- package/dist/color-swatch/index.d.ts +10 -0
- package/dist/color-swatch/index.js +9 -0
- package/dist/command/index.d.ts +24 -2
- package/dist/command/index.js +29 -6
- package/dist/content/index.d.ts +5 -0
- package/dist/content/index.js +12 -3
- package/dist/context-menu/index.d.ts +2 -2
- package/dist/context-menu/index.js +4 -3
- package/dist/data-table/index.d.ts +7 -1
- package/dist/data-table/index.js +64 -16
- package/dist/data-table/model.d.ts +2 -0
- package/dist/data-table/model.js +7 -0
- package/dist/date-picker/index.d.ts +2 -0
- package/dist/date-picker/index.js +10 -9
- package/dist/day-agenda/index.d.ts +6 -1
- package/dist/day-agenda/index.js +46 -21
- package/dist/day-agenda/layout.js +65 -9
- package/dist/display/ProgressRing.js +7 -3
- package/dist/display/index.d.ts +4 -1
- package/dist/display/index.js +3 -2
- package/dist/dockable-panel/index.js +5 -5
- package/dist/drawer/index.d.ts +1 -1
- package/dist/drawer/index.js +9 -6
- package/dist/dropdown-menu/index.d.ts +1 -1
- package/dist/dropdown-menu/index.js +3 -2
- package/dist/editable/index.d.ts +24 -2
- package/dist/editable/index.js +142 -37
- package/dist/empty/illustrations.d.ts +8 -0
- package/dist/empty/illustrations.js +78 -0
- package/dist/empty/index.d.ts +6 -1
- package/dist/empty/index.js +11 -3
- package/dist/flip/index.js +28 -8
- package/dist/form/EditablePencilText.js +34 -0
- package/dist/form/Field.js +11 -10
- package/dist/form/Input.js +1 -1
- package/dist/form/Textarea.d.ts +3 -1
- package/dist/form/Textarea.js +6 -4
- package/dist/hotkey/index.d.ts +1 -0
- package/dist/hotkey/index.js +6 -4
- package/dist/hover-card/index.js +1 -1
- package/dist/image-cropper/index.js +4 -2
- package/dist/image-viewer/VideoResource.d.ts +21 -0
- package/dist/image-viewer/VideoResource.js +194 -0
- package/dist/image-viewer/index.d.ts +14 -0
- package/dist/image-viewer/index.js +118 -65
- package/dist/image-viewer/lucideGeometry.d.ts +3 -0
- package/dist/image-viewer/lucideGeometry.js +19 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/input-number/index.d.ts +6 -0
- package/dist/input-number/index.js +131 -15
- package/dist/input-number/model.d.ts +13 -0
- package/dist/input-number/model.js +50 -0
- package/dist/internal/cache.d.ts +4 -0
- package/dist/internal/cache.js +18 -1
- package/dist/internal/iconGeometry.d.ts +2 -0
- package/dist/internal/iconGeometry.js +32 -0
- package/dist/internal/renderTokens.d.ts +4 -0
- package/dist/internal/renderTokens.js +4 -0
- package/dist/internal/stroke.d.ts +7 -0
- package/dist/internal/stroke.js +63 -7
- package/dist/internal/surfaceBudget.d.ts +20 -0
- package/dist/internal/surfaceBudget.js +69 -0
- package/dist/internal/surfaceRenderer.js +87 -28
- package/dist/internal/surfaceVisibility.d.ts +3 -0
- package/dist/internal/surfaceVisibility.js +60 -0
- package/dist/internal/textRenderer.d.ts +2 -0
- package/dist/internal/textRenderer.js +3 -3
- package/dist/internal/usePencilSurface.js +17 -1
- package/dist/internal/wash.js +4 -3
- package/dist/journey-map/terrain.js +10 -5
- package/dist/layout/index.js +1 -1
- package/dist/log/index.js +29 -22
- package/dist/markdown/CodeBlock.d.ts +19 -0
- package/dist/markdown/CodeBlock.js +86 -0
- package/dist/markdown/Deletion.d.ts +2 -0
- package/dist/markdown/Deletion.js +69 -0
- package/dist/markdown/icons.d.ts +14 -0
- package/dist/markdown/icons.js +16 -0
- package/dist/markdown/index.d.ts +17 -0
- package/dist/markdown/index.js +175 -0
- package/dist/node-graph/index.d.ts +2 -0
- package/dist/node-graph/index.js +49 -25
- package/dist/number-ticker/index.d.ts +11 -9
- package/dist/number-ticker/index.js +14 -11
- package/dist/number-ticker/model.d.ts +38 -5
- package/dist/number-ticker/model.js +139 -10
- package/dist/number-ticker/precision.d.ts +7 -0
- package/dist/number-ticker/precision.js +141 -0
- package/dist/overflow-list/index.js +5 -2
- package/dist/overlay/Overlay.d.ts +1 -1
- package/dist/overlay/Overlay.js +5 -3
- package/dist/overlay/Popover.d.ts +1 -1
- package/dist/overlay/Popover.js +6 -5
- package/dist/overlay/themeBridge.d.ts +12 -1
- package/dist/overlay/themeBridge.js +41 -3
- package/dist/pagination/index.d.ts +10 -1
- package/dist/pagination/index.js +6 -4
- package/dist/pan-zoom/index.d.ts +2 -0
- package/dist/pan-zoom/index.js +7 -6
- package/dist/pencil.d.ts +1 -0
- package/dist/pencil.js +4 -4
- package/dist/radar/index.d.ts +4 -0
- package/dist/radar/index.js +41 -21
- package/dist/rating/index.d.ts +4 -0
- package/dist/rating/index.js +128 -24
- package/dist/resizable/index.js +1 -1
- package/dist/scroll-area/index.d.ts +1 -1
- package/dist/scroll-area/index.js +5 -2
- package/dist/select/MultipleSelect.js +5 -4
- package/dist/select/SearchableSelect.js +61 -6
- package/dist/select/Select.d.ts +1 -0
- package/dist/select/Select.js +3 -1
- package/dist/select/StepperSelect.d.ts +29 -0
- package/dist/select/StepperSelect.js +119 -0
- package/dist/select/index.d.ts +2 -0
- package/dist/select/index.js +1 -0
- package/dist/select/stepperModel.d.ts +7 -0
- package/dist/select/stepperModel.js +29 -0
- package/dist/selection/Checkbox.js +4 -3
- package/dist/selection/RadioGroup.js +29 -13
- package/dist/selection/Switch.js +4 -3
- package/dist/selection/useNativeChecked.d.ts +2 -1
- package/dist/selection/useNativeChecked.js +26 -8
- package/dist/slider/index.js +50 -6
- package/dist/sortable/index.js +15 -6
- package/dist/sparkline/index.js +17 -2
- package/dist/sparkline/model.js +11 -4
- package/dist/styles/accordion.css +228 -0
- package/dist/styles/alert-dialog.css +139 -0
- package/dist/styles/audio-player.css +22 -0
- package/dist/styles/base.css +638 -0
- package/dist/styles/breadcrumb.css +7 -0
- package/dist/styles/calendar.css +62 -0
- package/dist/styles/carousel.css +173 -0
- package/dist/styles/choice-grid.css +226 -0
- package/dist/styles/color-picker.css +281 -0
- package/dist/styles/color-swatch.css +47 -0
- package/dist/styles/command.css +52 -0
- package/dist/styles/content.css +79 -0
- package/dist/styles/context-menu.css +35 -0
- package/dist/styles/data-table.css +31 -0
- package/dist/styles/date-picker.css +107 -0
- package/dist/styles/day-agenda.css +30 -0
- package/dist/styles/display.css +304 -0
- package/dist/styles/dockable-panel.css +110 -0
- package/dist/styles/drawer.css +133 -0
- package/dist/styles/dropdown-menu.css +68 -0
- package/dist/styles/editable.css +145 -0
- package/dist/styles/empty.css +108 -0
- package/dist/styles/flip.css +101 -0
- package/dist/styles/form.css +634 -0
- package/dist/styles/heatmap.css +256 -0
- package/dist/styles/hotkey.css +189 -0
- package/dist/styles/hover-card.css +27 -0
- package/dist/styles/image-cropper.css +205 -0
- package/dist/styles/image-viewer.css +422 -0
- package/dist/styles/input-number.css +21 -0
- package/dist/styles/journey-map.css +19 -0
- package/dist/styles/keypad.css +92 -0
- package/dist/styles/layout.css +72 -0
- package/dist/styles/leaflet.css +661 -0
- package/dist/styles/log.css +203 -0
- package/dist/styles/markdown.css +379 -0
- package/dist/styles/node-graph.css +142 -0
- package/dist/styles/number-ticker.css +83 -0
- package/dist/styles/overflow-list.css +51 -0
- package/dist/styles/overlay.css +284 -0
- package/dist/styles/pagination.css +21 -0
- package/dist/styles/pan-zoom.css +136 -0
- package/dist/styles/radar.css +1 -0
- package/dist/styles/rating.css +121 -0
- package/dist/styles/resizable.css +108 -0
- package/dist/styles/scroll-area.css +122 -0
- package/dist/styles/select.css +557 -0
- package/dist/styles/selection.css +264 -0
- package/dist/styles/slider.css +145 -0
- package/dist/styles/slot.css +109 -0
- package/dist/styles/sortable.css +159 -0
- package/dist/styles/sparkline.css +24 -0
- package/dist/styles/steps.css +27 -0
- package/dist/styles/tabs.css +79 -0
- package/dist/styles/tag-input.css +213 -0
- package/dist/styles/time-picker.css +66 -0
- package/dist/styles/timeline.css +24 -0
- package/dist/styles/toast.css +160 -0
- package/dist/styles/toggle-group.css +107 -0
- package/dist/styles/tooltip.css +78 -0
- package/dist/styles/transfer.css +148 -0
- package/dist/styles/tree-select.css +131 -0
- package/dist/styles/tree.css +59 -0
- package/dist/styles/typewriter.css +62 -0
- package/dist/styles/upload.css +60 -0
- package/dist/styles/virtual-list.css +112 -0
- package/dist/styles.css +2241 -443
- package/dist/tabs/index.d.ts +9 -0
- package/dist/tabs/index.js +66 -16
- package/dist/tabs/useOverflow.d.ts +20 -0
- package/dist/tabs/useOverflow.js +145 -0
- package/dist/tag-input/index.d.ts +7 -2
- package/dist/tag-input/index.js +70 -26
- package/dist/time-picker/index.js +38 -20
- package/dist/toast/index.d.ts +19 -11
- package/dist/toast/index.js +48 -8
- package/dist/toast/manager.d.ts +42 -0
- package/dist/toast/manager.js +85 -0
- package/dist/toast/useToastNavigation.d.ts +3 -0
- package/dist/toast/useToastNavigation.js +75 -0
- package/dist/tokens.css +4 -0
- package/dist/tooltip/Tooltip.d.ts +1 -1
- package/dist/tooltip/Tooltip.js +6 -5
- package/dist/transfer/index.d.ts +5 -0
- package/dist/transfer/index.js +93 -29
- package/dist/transfer/model.d.ts +4 -0
- package/dist/transfer/model.js +37 -11
- package/dist/tree/index.d.ts +5 -1
- package/dist/tree/index.js +5 -5
- package/dist/tree-select/index.js +67 -30
- package/dist/tree-select/model.d.ts +11 -5
- package/dist/tree-select/model.js +111 -83
- package/dist/typewriter/index.js +10 -10
- package/dist/upload/index.js +30 -16
- package/dist/virtual-list/index.js +10 -7
- package/package.json +416 -5
package/dist/transfer/index.js
CHANGED
|
@@ -7,12 +7,11 @@ import { PencilLine } from '../PencilLine.js';
|
|
|
7
7
|
import { PencilSurface } from '../PencilSurface.js';
|
|
8
8
|
import { PencilText } from '../PencilText.js';
|
|
9
9
|
import { Checkbox } from '../selection/Checkbox.js';
|
|
10
|
-
import { moveTransferKeys, normalizeTargetKeys, splitTransferItems, uniqueTransferItems, } from './model.js';
|
|
10
|
+
import { moveTransferKeys, normalizeTargetKeys, resolveTransferFocusIntent, splitTransferItems, transferKeysEqual, uniqueTransferItems, } from './model.js';
|
|
11
11
|
const searchPaths = [
|
|
12
12
|
{ d: 'M 11 4 A 7 7 0 1 0 11 18 A 7 7 0 1 0 11 4 M 16 16 L 21 21' },
|
|
13
13
|
];
|
|
14
14
|
const rightPaths = [{ d: 'M 5 12 L 19 12 M 13 6 L 19 12 L 13 18' }];
|
|
15
|
-
const leftPaths = [{ d: 'M 19 12 L 5 12 M 11 6 L 5 12 L 11 18' }];
|
|
16
15
|
const defaultLabels = {
|
|
17
16
|
sourceTitle: 'Available',
|
|
18
17
|
targetTitle: 'Selected',
|
|
@@ -33,6 +32,8 @@ const defaultLabels = {
|
|
|
33
32
|
moveSelectedToTarget: 'Move selected visible items to selected',
|
|
34
33
|
moveSelectedToSource: 'Move selected visible items to available',
|
|
35
34
|
batchActions: 'Transfer selected visible items',
|
|
35
|
+
previousPage: 'Previous page',
|
|
36
|
+
nextPage: 'Next page',
|
|
36
37
|
};
|
|
37
38
|
function searchableText(item) {
|
|
38
39
|
const parts = [
|
|
@@ -43,8 +44,13 @@ function searchableText(item) {
|
|
|
43
44
|
];
|
|
44
45
|
return parts.filter(Boolean).join(' ').toLocaleLowerCase();
|
|
45
46
|
}
|
|
46
|
-
function
|
|
47
|
-
|
|
47
|
+
function filterPanelItems(items, query, side, filterItem) {
|
|
48
|
+
if (query.length === 0)
|
|
49
|
+
return items;
|
|
50
|
+
if (filterItem)
|
|
51
|
+
return items.filter(item => filterItem(item, query, side));
|
|
52
|
+
const needle = query.trim().toLocaleLowerCase();
|
|
53
|
+
return items.filter(item => searchableText(item).includes(needle));
|
|
48
54
|
}
|
|
49
55
|
function defaultCount({ visible, total, selected }) {
|
|
50
56
|
return selected > 0 ? `${selected} selected · ${visible}/${total}` : `${visible}/${total}`;
|
|
@@ -54,46 +60,95 @@ function itemCopy(item) {
|
|
|
54
60
|
? _jsx(PencilText, { children: String(item.label) })
|
|
55
61
|
: item.label }), item.description == null ? null : _jsx("span", { className: "inkra-transfer__item-description", children: item.description })] });
|
|
56
62
|
}
|
|
57
|
-
export const Transfer = forwardRef(function Transfer({ className, defaultTargetKeys = [], disabled = false, filterItem, formatCount = defaultCount, items, labels: labelOverrides, onTargetKeysChange, renderItem, searchable = true, targetKeys, ...props }, ref) {
|
|
63
|
+
export const Transfer = forwardRef(function Transfer({ className, defaultTargetKeys = [], disabled = false, filterItem, formatCount = defaultCount, items, labels: labelOverrides, onTargetKeysChange, pageSize, pageLabel = (page, count) => `${page} / ${count}`, renderItem, searchable = true, targetKeys, ...props }, ref) {
|
|
58
64
|
const generatedId = useId();
|
|
59
65
|
const controlled = targetKeys !== undefined;
|
|
60
66
|
const uniqueItems = useMemo(() => uniqueTransferItems(items), [items]);
|
|
61
67
|
const [uncontrolledTargetKeys, setUncontrolledTargetKeys] = useState(() => normalizeTargetKeys(items, defaultTargetKeys));
|
|
62
|
-
const
|
|
63
|
-
const
|
|
68
|
+
const requestedTargetKeys = controlled ? targetKeys : uncontrolledTargetKeys;
|
|
69
|
+
const currentTargetKeys = useMemo(() => normalizeTargetKeys(uniqueItems, requestedTargetKeys), [uniqueItems, requestedTargetKeys]);
|
|
70
|
+
const currentTargetSet = useMemo(() => new Set(currentTargetKeys), [currentTargetKeys]);
|
|
71
|
+
const itemsById = useMemo(() => new Map(uniqueItems.map(item => [item.id, item])), [uniqueItems]);
|
|
72
|
+
const panels = useMemo(() => splitTransferItems(uniqueItems, currentTargetKeys), [uniqueItems, currentTargetKeys]);
|
|
64
73
|
const labels = { ...defaultLabels, ...labelOverrides };
|
|
65
74
|
const [queries, setQueries] = useState({ source: '', target: '' });
|
|
75
|
+
const [pages, setPages] = useState({ source: 0, target: 0 });
|
|
76
|
+
const pageLength = pageSize !== undefined && Number.isFinite(pageSize) ? Math.max(1, Math.floor(pageSize)) : undefined;
|
|
66
77
|
const [selected, setSelected] = useState(() => ({ source: new Set(), target: new Set() }));
|
|
67
78
|
const [pendingFocus, setPendingFocus] = useState(null);
|
|
68
79
|
const itemRefs = useRef(new Map());
|
|
69
80
|
const panelRefs = useRef({ source: null, target: null });
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
81
|
+
const visibleSource = useMemo(() => filterPanelItems(panels.source, queries.source, 'source', filterItem), [panels.source, queries.source, filterItem]);
|
|
82
|
+
const visibleTarget = useMemo(() => filterPanelItems(panels.target, queries.target, 'target', filterItem), [panels.target, queries.target, filterItem]);
|
|
83
|
+
const pageIndex = (side, count) => pageLength === undefined ? 0 : Math.min(pages[side], Math.max(0, Math.ceil(count / pageLength) - 1));
|
|
84
|
+
const sourcePage = pageIndex('source', visibleSource.length);
|
|
85
|
+
const targetPage = pageIndex('target', visibleTarget.length);
|
|
86
|
+
const shownSource = useMemo(() => pageLength === undefined ? visibleSource : visibleSource.slice(sourcePage * pageLength, (sourcePage + 1) * pageLength), [visibleSource, sourcePage, pageLength]);
|
|
87
|
+
const shownTarget = useMemo(() => pageLength === undefined ? visibleTarget : visibleTarget.slice(targetPage * pageLength, (targetPage + 1) * pageLength), [visibleTarget, targetPage, pageLength]);
|
|
88
|
+
const visibleItems = { source: shownSource, target: shownTarget };
|
|
75
89
|
useEffect(() => {
|
|
76
|
-
const valid = new Set(uniqueItems.map(item => item.id));
|
|
77
90
|
setSelected(current => {
|
|
78
|
-
const source = new Set([...current.source].filter(key =>
|
|
79
|
-
const target = new Set([...current.target].filter(key =>
|
|
91
|
+
const source = new Set([...current.source].filter(key => itemsById.has(key) && !currentTargetSet.has(key)));
|
|
92
|
+
const target = new Set([...current.target].filter(key => itemsById.has(key) && currentTargetSet.has(key)));
|
|
80
93
|
if (source.size === current.source.size && target.size === current.target.size)
|
|
81
94
|
return current;
|
|
82
95
|
return { source, target };
|
|
83
96
|
});
|
|
84
|
-
}, [
|
|
97
|
+
}, [itemsById, currentTargetSet]);
|
|
85
98
|
useEffect(() => {
|
|
86
99
|
if (!pendingFocus)
|
|
87
100
|
return;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
101
|
+
if (disabled || !itemsById.has(pendingFocus.id)) {
|
|
102
|
+
setPendingFocus(null);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const valueResolution = transferKeysEqual(currentTargetKeys, pendingFocus.expectedTargetKeys)
|
|
106
|
+
? 'accepted'
|
|
107
|
+
: transferKeysEqual(currentTargetKeys, pendingFocus.previousTargetKeys) ? 'unchanged' : 'mismatch';
|
|
108
|
+
const ownerDocument = pendingFocus.origin?.ownerDocument ?? document;
|
|
109
|
+
const activeElement = ownerDocument.activeElement;
|
|
110
|
+
const originStillOwnsFocus = activeElement === pendingFocus.origin;
|
|
111
|
+
const focusFellBackToDocument = activeElement === ownerDocument.body || activeElement === ownerDocument.documentElement;
|
|
112
|
+
const originUnavailable = pendingFocus.origin != null
|
|
113
|
+
&& (!pendingFocus.origin.isConnected || pendingFocus.origin.matches(':disabled'))
|
|
114
|
+
&& focusFellBackToDocument;
|
|
115
|
+
if (valueResolution !== 'accepted') {
|
|
116
|
+
if (resolveTransferFocusIntent(valueResolution, originStillOwnsFocus, originUnavailable) !== 'wait') {
|
|
117
|
+
setPendingFocus(null);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const cancel = (event) => {
|
|
121
|
+
const target = event.target;
|
|
122
|
+
const documentFallback = target === ownerDocument.body || target === ownerDocument.documentElement;
|
|
123
|
+
const unavailable = pendingFocus.origin != null
|
|
124
|
+
&& (!pendingFocus.origin.isConnected || pendingFocus.origin.matches(':disabled'));
|
|
125
|
+
if (target !== pendingFocus.origin && !(documentFallback && unavailable))
|
|
126
|
+
setPendingFocus(null);
|
|
127
|
+
};
|
|
128
|
+
ownerDocument.addEventListener('focusin', cancel);
|
|
129
|
+
return () => ownerDocument.removeEventListener('focusin', cancel);
|
|
130
|
+
}
|
|
131
|
+
if (resolveTransferFocusIntent('accepted', originStillOwnsFocus, originUnavailable) !== 'focus') {
|
|
132
|
+
setPendingFocus(null);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (pageLength !== undefined) {
|
|
136
|
+
const matching = pendingFocus.side === 'source' ? visibleSource : visibleTarget;
|
|
137
|
+
const index = matching.findIndex(item => item.id === pendingFocus.id);
|
|
138
|
+
const nextPage = index < 0 ? undefined : Math.floor(index / pageLength);
|
|
139
|
+
if (nextPage !== undefined && nextPage !== pages[pendingFocus.side]) {
|
|
140
|
+
setPages(current => ({ ...current, [pendingFocus.side]: nextPage }));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const control = itemRefs.current.get(`${pendingFocus.side}:${pendingFocus.id}`);
|
|
92
145
|
(control ?? panelRefs.current[pendingFocus.side])?.focus({ preventScroll: true });
|
|
93
146
|
setPendingFocus(null);
|
|
94
|
-
}, [pendingFocus, currentTargetKeys
|
|
147
|
+
}, [disabled, pendingFocus, currentTargetKeys, itemsById, pageLength, pages, visibleSource, visibleTarget]);
|
|
95
148
|
const setItemSelected = (side, id, checked) => {
|
|
96
149
|
setSelected(current => {
|
|
150
|
+
if (current[side].has(id) === checked)
|
|
151
|
+
return current;
|
|
97
152
|
const next = new Set(current[side]);
|
|
98
153
|
if (checked)
|
|
99
154
|
next.add(id);
|
|
@@ -111,11 +166,11 @@ export const Transfer = forwardRef(function Transfer({ className, defaultTargetK
|
|
|
111
166
|
return { ...current, [side]: next };
|
|
112
167
|
});
|
|
113
168
|
};
|
|
114
|
-
const move = (keys, direction, action) => {
|
|
169
|
+
const move = (keys, direction, action, origin) => {
|
|
115
170
|
const sourceSide = direction === 'to-target' ? 'source' : 'target';
|
|
116
171
|
const destinationSide = direction === 'to-target' ? 'target' : 'source';
|
|
117
172
|
const allowed = new Set(panels[sourceSide].filter(item => !item.disabled && !disabled).map(item => item.id));
|
|
118
|
-
const movedKeys = keys.filter(
|
|
173
|
+
const movedKeys = [...new Set(keys)].filter(key => allowed.has(key));
|
|
119
174
|
if (movedKeys.length === 0)
|
|
120
175
|
return;
|
|
121
176
|
const next = moveTransferKeys(uniqueItems, currentTargetKeys, movedKeys, direction);
|
|
@@ -126,12 +181,18 @@ export const Transfer = forwardRef(function Transfer({ className, defaultTargetK
|
|
|
126
181
|
movedKeys.forEach(key => source.delete(key));
|
|
127
182
|
return { ...current, [sourceSide]: source };
|
|
128
183
|
});
|
|
129
|
-
setPendingFocus({
|
|
184
|
+
setPendingFocus({
|
|
185
|
+
id: movedKeys[0],
|
|
186
|
+
side: destinationSide,
|
|
187
|
+
origin,
|
|
188
|
+
previousTargetKeys: [...currentTargetKeys],
|
|
189
|
+
expectedTargetKeys: next,
|
|
190
|
+
});
|
|
130
191
|
onTargetKeysChange?.(next, {
|
|
131
192
|
action,
|
|
132
193
|
direction,
|
|
133
194
|
movedKeys,
|
|
134
|
-
movedItems: movedKeys.map(key =>
|
|
195
|
+
movedItems: movedKeys.map(key => itemsById.get(key)).filter((item) => item !== undefined),
|
|
135
196
|
});
|
|
136
197
|
};
|
|
137
198
|
const renderPanel = (side) => {
|
|
@@ -154,7 +215,10 @@ export const Transfer = forwardRef(function Transfer({ className, defaultTargetK
|
|
|
154
215
|
const selectAllLabel = source ? labels.selectAllSource : labels.selectAllTarget;
|
|
155
216
|
const direction = source ? 'to-target' : 'to-source';
|
|
156
217
|
const moveLabel = source ? labels.moveToTarget : labels.moveToSource;
|
|
157
|
-
return _jsxs(PencilSurface, { "aria-labelledby": titleId, as: "section", className: "inkra-transfer__panel", "data-searchable": searchable || undefined, "data-side": side, ref: element => { panelRefs.current[side] = element; }, seed: `inkra-transfer-${generatedId}-${side}`, tabIndex: -1, variant: "paper", children: [_jsxs("header", { className: "inkra-transfer__header", children: [_jsx("span", { className: "inkra-transfer__title", id: titleId, children: typeof panelTitle === 'string' ? _jsx(PencilText, { children: panelTitle }) : panelTitle }), _jsx("span", { "aria-live": "polite", className: "inkra-transfer__count", children: count })] }), _jsx(PencilLine, { "aria-hidden": "true", className: "inkra-transfer__divider", seed: `inkra-transfer-${generatedId}-${side}-header` }), searchable && _jsx(Input, { "aria-label": searchLabel, clearable: true, clearButtonLabel: clearSearchLabel, disabled: disabled, onValueChange: value =>
|
|
218
|
+
return _jsxs(PencilSurface, { "aria-labelledby": titleId, as: "section", className: "inkra-transfer__panel", "data-searchable": searchable || undefined, "data-side": side, ref: element => { panelRefs.current[side] = element; }, seed: `inkra-transfer-${generatedId}-${side}`, tabIndex: -1, variant: "paper", children: [_jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-transfer__panel-focus", variant: "outline", seed: `inkra-transfer-${generatedId}-${side}-focus` }), _jsxs("header", { className: "inkra-transfer__header", children: [_jsx("span", { className: "inkra-transfer__title", id: titleId, children: typeof panelTitle === 'string' ? _jsx(PencilText, { children: panelTitle }) : panelTitle }), _jsx("span", { "aria-live": "polite", className: "inkra-transfer__count", children: count })] }), _jsx(PencilLine, { "aria-hidden": "true", className: "inkra-transfer__divider", seed: `inkra-transfer-${generatedId}-${side}-header` }), searchable && _jsx(Input, { "aria-label": searchLabel, clearable: true, clearButtonLabel: clearSearchLabel, disabled: disabled, onValueChange: value => {
|
|
219
|
+
setQueries(current => ({ ...current, [side]: value }));
|
|
220
|
+
setPages(current => ({ ...current, [side]: 0 }));
|
|
221
|
+
}, placeholder: searchPlaceholder, startIcon: _jsx(PencilIcon, { decorative: true, paths: searchPaths, size: 18, sourceSize: 24 }), type: "search", value: queries[side], variant: "outline" }), _jsx("div", { className: "inkra-transfer__selection-bar", children: _jsx(Checkbox, { checked: allVisibleSelected, disabled: disabled || selectable.length === 0, indeterminate: someVisibleSelected, label: selectAllLabel, onCheckedChange: checked => toggleAll(side, checked), seed: `inkra-transfer-${generatedId}-${side}-all` }) }), _jsx("ul", { className: "inkra-transfer__list", "aria-labelledby": titleId, children: shown.length === 0 ? _jsx("li", { className: "inkra-transfer__empty", children: empty }) : shown.map(item => {
|
|
158
222
|
const itemDisabled = disabled || item.disabled === true;
|
|
159
223
|
const itemSelected = selected[side].has(item.id);
|
|
160
224
|
const copy = renderItem?.(item, { side, selected: itemSelected, disabled: itemDisabled, query: queries[side] }) ?? itemCopy(item);
|
|
@@ -164,11 +228,11 @@ export const Transfer = forwardRef(function Transfer({ className, defaultTargetK
|
|
|
164
228
|
itemRefs.current.set(key, element);
|
|
165
229
|
else
|
|
166
230
|
itemRefs.current.delete(key);
|
|
167
|
-
}, seed: `inkra-transfer-${generatedId}-${side}-${item.id}` }), _jsx(Button, { "aria-label": `${moveLabel}: ${typeof item.label === 'string' || typeof item.label === 'number' ? String(item.label) : item.searchText ?? item.id}`, disabled: itemDisabled, onClick:
|
|
168
|
-
}) })] });
|
|
231
|
+
}, seed: `inkra-transfer-${generatedId}-${side}-${item.id}` }), _jsx(Button, { "aria-label": `${moveLabel}: ${typeof item.label === 'string' || typeof item.label === 'number' ? String(item.label) : item.searchText ?? item.id}`, disabled: itemDisabled, onClick: event => move([item.id], direction, 'single', event.currentTarget), seed: `inkra-transfer-${generatedId}-${side}-${item.id}-move`, size: "icon-sm", title: moveLabel, variant: "ghost", children: _jsx(PencilIcon, { className: "inkra-transfer__direction-icon", "data-direction": direction, decorative: true, paths: rightPaths, size: 18, sourceSize: 24 }) })] }, item.id);
|
|
232
|
+
}) }), pageLength !== undefined && _jsxs("nav", { "aria-label": typeof panelTitle === 'string' ? panelTitle : undefined, children: [_jsx(Button, { size: "sm", variant: "ghost", "aria-label": labels.previousPage, disabled: disabled || pageIndex(side, (source ? visibleSource : visibleTarget).length) === 0, onClick: () => setPages(current => ({ ...current, [side]: pageIndex(side, (source ? visibleSource : visibleTarget).length) - 1 })), children: labels.previousPage }), _jsx("span", { "aria-live": "polite", children: pageLabel((source ? visibleSource : visibleTarget).length === 0 ? 0 : pageIndex(side, (source ? visibleSource : visibleTarget).length) + 1, Math.ceil((source ? visibleSource : visibleTarget).length / pageLength)) }), _jsx(Button, { size: "sm", variant: "ghost", "aria-label": labels.nextPage, disabled: disabled || (pageIndex(side, (source ? visibleSource : visibleTarget).length) + 1) * pageLength >= (source ? visibleSource : visibleTarget).length, onClick: () => setPages(current => ({ ...current, [side]: pageIndex(side, (source ? visibleSource : visibleTarget).length) + 1 })), children: labels.nextPage })] })] });
|
|
169
233
|
};
|
|
170
234
|
const sourceBatchKeys = selectableVisible('source').filter(item => selected.source.has(item.id)).map(item => item.id);
|
|
171
235
|
const targetBatchKeys = selectableVisible('target').filter(item => selected.target.has(item.id)).map(item => item.id);
|
|
172
|
-
return
|
|
236
|
+
return _jsx("div", { ...props, "aria-disabled": disabled || undefined, className: ['inkra-transfer', className].filter(Boolean).join(' '), "data-disabled": disabled || undefined, ref: ref, children: _jsxs("div", { className: "inkra-transfer__layout", children: [renderPanel('source'), _jsxs("div", { className: "inkra-transfer__actions", role: "group", "aria-label": labels.batchActions, children: [_jsx(Button, { "aria-label": labels.moveSelectedToTarget, disabled: disabled || sourceBatchKeys.length === 0, onClick: event => move(sourceBatchKeys, 'to-target', 'batch', event.currentTarget), seed: `inkra-transfer-${generatedId}-batch-target`, size: "icon", title: labels.moveSelectedToTarget, variant: "outline", children: _jsx(PencilIcon, { className: "inkra-transfer__direction-icon", "data-direction": "to-target", decorative: true, paths: rightPaths, size: 20, sourceSize: 24 }) }), _jsx(Button, { "aria-label": labels.moveSelectedToSource, disabled: disabled || targetBatchKeys.length === 0, onClick: event => move(targetBatchKeys, 'to-source', 'batch', event.currentTarget), seed: `inkra-transfer-${generatedId}-batch-source`, size: "icon", title: labels.moveSelectedToSource, variant: "outline", children: _jsx(PencilIcon, { className: "inkra-transfer__direction-icon", "data-direction": "to-source", decorative: true, paths: rightPaths, size: 20, sourceSize: 24 }) })] }), renderPanel('target')] }) });
|
|
173
237
|
});
|
|
174
238
|
export { moveTransferKeys, normalizeTargetKeys, splitTransferItems, uniqueTransferItems } from './model.js';
|
package/dist/transfer/model.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
export type TransferDirection = 'to-target' | 'to-source';
|
|
2
2
|
export type TransferSide = 'source' | 'target';
|
|
3
|
+
export type TransferFocusIntent = 'wait' | 'cancel' | 'focus';
|
|
4
|
+
export type TransferValueResolution = 'unchanged' | 'accepted' | 'mismatch';
|
|
3
5
|
export interface TransferIdentity {
|
|
4
6
|
id: string;
|
|
5
7
|
}
|
|
8
|
+
export declare function resolveTransferFocusIntent(valueResolution: TransferValueResolution, originOwnsFocus: boolean, originUnavailable: boolean): TransferFocusIntent;
|
|
9
|
+
export declare function transferKeysEqual(left: readonly string[], right: readonly string[]): boolean;
|
|
6
10
|
export declare function uniqueTransferItems<Item extends TransferIdentity>(items: readonly Item[]): Item[];
|
|
7
11
|
export declare function normalizeTargetKeys<Item extends TransferIdentity>(items: readonly Item[], targetKeys: readonly string[]): string[];
|
|
8
12
|
export declare function splitTransferItems<Item extends TransferIdentity>(items: readonly Item[], targetKeys: readonly string[]): {
|
package/dist/transfer/model.js
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
export function resolveTransferFocusIntent(valueResolution, originOwnsFocus, originUnavailable) {
|
|
2
|
+
if (valueResolution === 'mismatch')
|
|
3
|
+
return 'cancel';
|
|
4
|
+
if (valueResolution === 'unchanged')
|
|
5
|
+
return originOwnsFocus || originUnavailable ? 'wait' : 'cancel';
|
|
6
|
+
return originOwnsFocus || originUnavailable ? 'focus' : 'cancel';
|
|
7
|
+
}
|
|
8
|
+
export function transferKeysEqual(left, right) {
|
|
9
|
+
return left.length === right.length && left.every((key, index) => key === right[index]);
|
|
10
|
+
}
|
|
1
11
|
export function uniqueTransferItems(items) {
|
|
2
12
|
const seen = new Set();
|
|
3
13
|
return items.filter(item => {
|
|
@@ -8,20 +18,36 @@ export function uniqueTransferItems(items) {
|
|
|
8
18
|
});
|
|
9
19
|
}
|
|
10
20
|
export function normalizeTargetKeys(items, targetKeys) {
|
|
11
|
-
const uniqueItems = uniqueTransferItems(items);
|
|
12
21
|
const requested = new Set(targetKeys);
|
|
13
|
-
const knownIds = new Set(
|
|
14
|
-
const orderedKnown =
|
|
15
|
-
|
|
16
|
-
|
|
22
|
+
const knownIds = new Set();
|
|
23
|
+
const orderedKnown = [];
|
|
24
|
+
for (const item of items) {
|
|
25
|
+
if (knownIds.has(item.id))
|
|
26
|
+
continue;
|
|
27
|
+
knownIds.add(item.id);
|
|
28
|
+
if (requested.has(item.id))
|
|
29
|
+
orderedKnown.push(item.id);
|
|
30
|
+
}
|
|
31
|
+
// Set iteration preserves the first occurrence of unknown controlled keys,
|
|
32
|
+
// without scanning the entire key array for each unknown key.
|
|
33
|
+
for (const key of requested) {
|
|
34
|
+
if (!knownIds.has(key))
|
|
35
|
+
orderedKnown.push(key);
|
|
36
|
+
}
|
|
37
|
+
return orderedKnown;
|
|
17
38
|
}
|
|
18
39
|
export function splitTransferItems(items, targetKeys) {
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
40
|
+
const targetKeysSet = new Set(targetKeys);
|
|
41
|
+
const seen = new Set();
|
|
42
|
+
const source = [];
|
|
43
|
+
const target = [];
|
|
44
|
+
for (const item of items) {
|
|
45
|
+
if (seen.has(item.id))
|
|
46
|
+
continue;
|
|
47
|
+
seen.add(item.id);
|
|
48
|
+
(targetKeysSet.has(item.id) ? target : source).push(item);
|
|
49
|
+
}
|
|
50
|
+
return { source, target };
|
|
25
51
|
}
|
|
26
52
|
export function moveTransferKeys(items, targetKeys, movedKeys, direction) {
|
|
27
53
|
const next = new Set(targetKeys);
|
package/dist/tree/index.d.ts
CHANGED
|
@@ -6,18 +6,22 @@ export type TreeRootProps<T extends object = object> = Omit<AriaTreeProps<T>, 'c
|
|
|
6
6
|
className?: string;
|
|
7
7
|
style?: CSSProperties;
|
|
8
8
|
emptyState?: ReactNode;
|
|
9
|
+
/** Indentation per level in CSS pixels. Nonfinite values use 24. */
|
|
10
|
+
indent?: number;
|
|
11
|
+
size?: 'sm' | 'md' | 'lg';
|
|
9
12
|
};
|
|
10
13
|
export type TreeItemProps<T = object> = Omit<AriaTreeItemProps<T>, 'className'> & {
|
|
11
14
|
className?: string;
|
|
12
15
|
};
|
|
13
16
|
export interface TreeItemContentState {
|
|
17
|
+
level: number;
|
|
14
18
|
isExpanded: boolean;
|
|
15
19
|
hasChildItems: boolean;
|
|
16
20
|
isSelected: boolean;
|
|
17
21
|
isDisabled: boolean;
|
|
18
22
|
}
|
|
19
23
|
export interface TreeItemContentProps {
|
|
20
|
-
children: ReactNode;
|
|
24
|
+
children: ReactNode | ((state: TreeItemContentState) => ReactNode);
|
|
21
25
|
className?: string;
|
|
22
26
|
icon?: ReactNode | ((state: TreeItemContentState) => ReactNode);
|
|
23
27
|
disclosureIcon?: ReactNode | ((state: TreeItemContentState) => ReactNode);
|
package/dist/tree/index.js
CHANGED
|
@@ -6,8 +6,8 @@ import { PencilIcon } from '../PencilIcon.js';
|
|
|
6
6
|
import { PencilSurface } from '../PencilSurface.js';
|
|
7
7
|
import { PencilText } from '../PencilText.js';
|
|
8
8
|
const chevronIcon = [{ d: 'M 7 10 L 12 15 L 17 10' }];
|
|
9
|
-
function TreeRootInner({ className, emptyState = 'No items', selectionMode = 'single', selectionBehavior = 'toggle', ...props }, ref) {
|
|
10
|
-
return _jsx(AriaTree, { ...props, ref: ref, className: ['inkra-tree', className].filter(Boolean).join(' '), selectionMode: selectionMode, selectionBehavior: selectionBehavior, renderEmptyState: () => _jsx("div", { className: "inkra-tree__empty", children: emptyState }) });
|
|
9
|
+
function TreeRootInner({ className, style, indent = 24, size = 'md', emptyState = 'No items', selectionMode = 'single', selectionBehavior = 'toggle', ...props }, ref) {
|
|
10
|
+
return _jsx(AriaTree, { ...props, ref: ref, className: ['inkra-tree', className].filter(Boolean).join(' '), "data-size": size, style: { '--inkra-tree-indent': `${Number.isFinite(indent) ? Math.max(0, indent) : 24}px`, ...style }, selectionMode: selectionMode, selectionBehavior: selectionBehavior, renderEmptyState: () => _jsx("div", { className: "inkra-tree__empty", children: emptyState }) });
|
|
11
11
|
}
|
|
12
12
|
const TreeRoot = forwardRef(TreeRootInner);
|
|
13
13
|
const TreeItem = forwardRef(function TreeItem({ className, ...props }, ref) {
|
|
@@ -15,17 +15,17 @@ const TreeItem = forwardRef(function TreeItem({ className, ...props }, ref) {
|
|
|
15
15
|
});
|
|
16
16
|
const TreeItemContent = forwardRef(function TreeItemContent({ children, className, icon, disclosureIcon, expandLabel = 'Expand item', collapseLabel = 'Collapse item', }, ref) {
|
|
17
17
|
return _jsx(AriaTreeItemContent, { ref: ref, children: ({ hasChildItems, isExpanded, isSelected, isFocusVisible, isDisabled, level, id, }) => {
|
|
18
|
-
const state = { isExpanded, hasChildItems, isSelected, isDisabled };
|
|
18
|
+
const state = { isExpanded, hasChildItems, isSelected, isDisabled, level };
|
|
19
19
|
const resolvedIcon = typeof icon === 'function' ? icon(state) : icon;
|
|
20
20
|
const resolvedDisclosure = typeof disclosureIcon === 'function' ? disclosureIcon(state) : disclosureIcon;
|
|
21
|
-
return _jsxs("div", { className: ['inkra-tree__item-content', className].filter(Boolean).join(' '), "data-expanded": isExpanded || undefined, "data-selected": isSelected || undefined, "data-disabled": isDisabled || undefined, "data-has-icon": resolvedIcon != null || undefined, style: { '--inkra-tree-level': level }, children: [isSelected && _jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-tree__selection", variant: "selection", seed: `tree-${String(id)}-selection` }), isFocusVisible && _jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-tree__focus", variant: "outline", seed: `tree-${String(id)}-focus` }), hasChildItems ? _jsx(AriaButton, { slot: "chevron", className: "inkra-tree__disclosure", "aria-label": isExpanded ? collapseLabel : expandLabel, children: ({ isFocusVisible: disclosureFocused }) => _jsxs(_Fragment, { children: [disclosureFocused && _jsx(PencilSurface, { as: "span", "aria-hidden": "true", className: "inkra-tree__disclosure-focus", variant: "outline" }), _jsx("span", { className: "inkra-tree__disclosure-icon", "aria-hidden": "true", children: disclosureIcon === undefined ? _jsx(PencilIcon, { className: "inkra-tree__chevron", paths: chevronIcon, size: 18, sourceSize: 24 }) : resolvedDisclosure })] }) }) : _jsx("span", { className: "inkra-tree__disclosure-spacer", "aria-hidden": "true" }), resolvedIcon != null && _jsx("span", { className: "inkra-tree__icon", "aria-hidden": "true", children: resolvedIcon }), _jsx("span", { className: "inkra-tree__copy", children: children })] });
|
|
21
|
+
return _jsxs("div", { className: ['inkra-tree__item-content', className].filter(Boolean).join(' '), "data-expanded": isExpanded || undefined, "data-selected": isSelected || undefined, "data-disabled": isDisabled || undefined, "data-has-icon": resolvedIcon != null || undefined, style: { '--inkra-tree-level': level }, children: [isSelected && _jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-tree__selection", variant: "selection", seed: `tree-${String(id)}-selection` }), isFocusVisible && _jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-tree__focus", variant: "outline", seed: `tree-${String(id)}-focus` }), hasChildItems ? _jsx(AriaButton, { slot: "chevron", className: "inkra-tree__disclosure", "aria-label": isExpanded ? collapseLabel : expandLabel, children: ({ isFocusVisible: disclosureFocused }) => _jsxs(_Fragment, { children: [disclosureFocused && _jsx(PencilSurface, { as: "span", "aria-hidden": "true", className: "inkra-tree__disclosure-focus", variant: "outline" }), _jsx("span", { className: "inkra-tree__disclosure-icon", "aria-hidden": "true", children: disclosureIcon === undefined ? _jsx(PencilIcon, { className: "inkra-tree__chevron", paths: chevronIcon, size: 18, sourceSize: 24 }) : resolvedDisclosure })] }) }) : _jsx("span", { className: "inkra-tree__disclosure-spacer", "aria-hidden": "true" }), resolvedIcon != null && _jsx("span", { className: "inkra-tree__icon", "aria-hidden": "true", children: resolvedIcon }), _jsx("span", { className: "inkra-tree__copy", children: typeof children === 'function' ? children(state) : children })] });
|
|
22
22
|
} });
|
|
23
23
|
});
|
|
24
24
|
const TreeItemTitle = forwardRef(function TreeItemTitle({ className, children, ...props }, ref) {
|
|
25
25
|
return _jsx("span", { ...props, ref: ref, className: ['inkra-tree__title', className].filter(Boolean).join(' '), children: typeof children === 'string' ? _jsx(PencilText, { children: children }) : children });
|
|
26
26
|
});
|
|
27
27
|
const TreeItemDescription = forwardRef(function TreeItemDescription({ className, children, ...props }, ref) {
|
|
28
|
-
return _jsx("span", { ...props, ref: ref, className: ['inkra-tree__description', className].filter(Boolean).join(' '), children: children });
|
|
28
|
+
return _jsx("span", { ...props, ref: ref, className: ['inkra-tree__description', className].filter(Boolean).join(' '), children: typeof children === 'string' ? _jsx(PencilText, { children: children }) : children });
|
|
29
29
|
});
|
|
30
30
|
export const Tree = {
|
|
31
31
|
Root: TreeRoot,
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef, useEffect, useId, useMemo, useRef, useState, } from 'react';
|
|
3
|
-
import { Button } from '../Button.js';
|
|
3
|
+
import { Button, ButtonGroup } from '../Button.js';
|
|
4
4
|
import { Input } from '../form/Input.js';
|
|
5
5
|
import { mergeAriaIds, useFieldContext } from '../form/context.js';
|
|
6
6
|
import { PencilIcon } from '../PencilIcon.js';
|
|
7
7
|
import { PencilText } from '../PencilText.js';
|
|
8
8
|
import { Popover } from '../overlay/index.js';
|
|
9
9
|
import { Tree } from '../tree/index.js';
|
|
10
|
-
import {
|
|
10
|
+
import { filterTreeSelectNodesFromIndex, indexTreeSelectNodes, updateTreeSelectSelectionFromIndex, } from './model.js';
|
|
11
11
|
const chevronPaths = [{ d: 'M6 9l6 6 6-6' }];
|
|
12
12
|
const clearPaths = [{ d: 'M6 6l12 12M18 6L6 18' }];
|
|
13
13
|
const searchPaths = [{ d: 'M21 21l-4.5-4.5' }, { d: 'M18 10a8 8 0 1 1-16 0a8 8 0 1 1 16 0' }];
|
|
@@ -20,6 +20,41 @@ function selectionFromProps(props, fallback = []) {
|
|
|
20
20
|
function keys(selection) {
|
|
21
21
|
return selection === 'all' ? [] : [...selection].map(String);
|
|
22
22
|
}
|
|
23
|
+
// The portal owns mounting (including its exit animation). Keep collection
|
|
24
|
+
// construction here rather than doing work for content the portal never mounts.
|
|
25
|
+
const TreeSelectTree = forwardRef(function TreeSelectTree({ nodes, renderLabel, selectedSet, query, collapseLabel, expandLabel, treeLabel, emptyContent, effectiveExpanded, onExpandedChange, onSelectionChange, multiple, }, ref) {
|
|
26
|
+
// Default labels are selection-independent; only custom label renderers need
|
|
27
|
+
// a new collection when the selected keys change.
|
|
28
|
+
const labelSelection = renderLabel ? selectedSet : undefined;
|
|
29
|
+
const treeItems = useMemo(() => {
|
|
30
|
+
// Construct the same nested tree without growing the JavaScript call stack.
|
|
31
|
+
// Enter jobs keep renderLabel callbacks in the original depth-first order.
|
|
32
|
+
const result = [];
|
|
33
|
+
const stack = [];
|
|
34
|
+
for (let index = nodes.length - 1; index >= 0; index--)
|
|
35
|
+
stack.push({ node: nodes[index], output: result });
|
|
36
|
+
while (stack.length) {
|
|
37
|
+
const { node, output, finish } = stack.pop();
|
|
38
|
+
if (finish) {
|
|
39
|
+
output.push(_jsxs(Tree.Item, { className: "inkra-tree-select__item", id: node.id, isDisabled: node.disabled, textValue: node.label, children: [_jsxs(Tree.ItemContent, { collapseLabel: collapseLabel, expandLabel: expandLabel, children: [_jsx(Tree.ItemTitle, { children: finish.label }), node.description ? _jsx(Tree.ItemDescription, { children: node.description }) : null] }), node.children?.length ? finish.children : null] }, node.id));
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const label = renderLabel?.(node, {
|
|
43
|
+
branch: Boolean(node.children?.length),
|
|
44
|
+
disabled: Boolean(node.disabled),
|
|
45
|
+
selected: labelSelection?.has(node.id) ?? false,
|
|
46
|
+
query,
|
|
47
|
+
}) ?? node.label;
|
|
48
|
+
const children = [];
|
|
49
|
+
stack.push({ node, output, finish: { label, children } });
|
|
50
|
+
for (let index = (node.children?.length ?? 0) - 1; index >= 0; index--) {
|
|
51
|
+
stack.push({ node: node.children[index], output: children });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}, [nodes, renderLabel, labelSelection, query, collapseLabel, expandLabel]);
|
|
56
|
+
return _jsx(Tree.Root, { "aria-label": treeLabel, className: "inkra-tree-select__tree", emptyState: emptyContent, expandedKeys: effectiveExpanded, onExpandedChange: onExpandedChange, onSelectionChange: onSelectionChange, ref: ref, selectedKeys: selectedSet, selectionMode: multiple ? 'multiple' : 'single', children: treeItems });
|
|
57
|
+
});
|
|
23
58
|
export const TreeSelect = forwardRef(function TreeSelect(props, ref) {
|
|
24
59
|
const { items, branchSelection = 'leaf', selectionPropagation = 'none', renderLabel, renderValue, placeholder = 'Select an item', searchPlaceholder = 'Search tree', emptyContent = 'No matching items', clearable = true, clearLabel = 'Clear selection', treeLabel = 'Tree options', expandLabel = 'Expand item', collapseLabel = 'Collapse item', size = 'md', name, form, disabled, readOnly, required, invalid, open: controlledOpen, defaultOpen = false, onOpenChange, expandedIds, defaultExpandedIds = [], onExpandedIdsChange, side = 'bottom', align = 'start', sideOffset = 8, collisionPadding = 8, id, className, multiple: _multiple, value: _value, defaultValue: _defaultValue, onValueChange: _onValueChange, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, ...rest } = props;
|
|
25
60
|
const field = useFieldContext();
|
|
@@ -31,25 +66,25 @@ export const TreeSelect = forwardRef(function TreeSelect(props, ref) {
|
|
|
31
66
|
const resolvedInvalid = Boolean(invalid || field?.invalid);
|
|
32
67
|
const describedBy = mergeAriaIds(ariaDescribedBy, field?.descriptionId, field?.errorId);
|
|
33
68
|
const normalized = useMemo(() => indexTreeSelectNodes(items), [items]);
|
|
34
|
-
const defaultSelection = useRef(selectionFromProps(props));
|
|
35
69
|
const controlledSelection = props.value !== undefined;
|
|
36
|
-
const [uncontrolledSelection, setUncontrolledSelection] = useState(
|
|
37
|
-
const
|
|
70
|
+
const [uncontrolledSelection, setUncontrolledSelection] = useState(() => selectionFromProps(props));
|
|
71
|
+
const defaultSelection = useRef(uncontrolledSelection);
|
|
72
|
+
const selectedIds = useMemo(() => controlledSelection ? selectionFromProps(props) : uncontrolledSelection, [controlledSelection, props.value, props.multiple, uncontrolledSelection]);
|
|
38
73
|
const selectedSet = useMemo(() => new Set(selectedIds), [selectedIds]);
|
|
39
74
|
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
|
|
40
75
|
const isOpen = controlledOpen ?? uncontrolledOpen;
|
|
41
76
|
const [uncontrolledExpanded, setUncontrolledExpanded] = useState(() => [...new Set(defaultExpandedIds)]);
|
|
42
77
|
const baseExpanded = expandedIds ?? uncontrolledExpanded;
|
|
43
78
|
const [query, setQuery] = useState('');
|
|
44
|
-
const filtered = useMemo(() =>
|
|
79
|
+
const filtered = useMemo(() => filterTreeSelectNodesFromIndex(normalized, query), [normalized, query]);
|
|
45
80
|
const effectiveExpanded = useMemo(() => new Set([...baseExpanded, ...filtered.expandedIds]), [baseExpanded, filtered.expandedIds]);
|
|
46
81
|
const triggerRef = useRef(null);
|
|
47
82
|
const searchRef = useRef(null);
|
|
48
83
|
const treeRef = useRef(null);
|
|
49
84
|
const formAnchorRef = useRef(null);
|
|
50
|
-
const selectedNodes = selectedIds
|
|
85
|
+
const selectedNodes = useMemo(() => selectedIds
|
|
51
86
|
.map(selectedId => normalized.byId.get(selectedId))
|
|
52
|
-
.filter((node) => node !== undefined);
|
|
87
|
+
.filter((node) => node !== undefined), [selectedIds, normalized]);
|
|
53
88
|
const setOpen = (next) => {
|
|
54
89
|
if (resolvedDisabled || resolvedReadOnly)
|
|
55
90
|
return;
|
|
@@ -68,20 +103,28 @@ export const TreeSelect = forwardRef(function TreeSelect(props, ref) {
|
|
|
68
103
|
props.onValueChange?.(next[0] ?? null, details);
|
|
69
104
|
};
|
|
70
105
|
useEffect(() => {
|
|
71
|
-
if (controlledSelection)
|
|
72
|
-
return;
|
|
73
106
|
const currentForm = formAnchorRef.current?.form;
|
|
74
107
|
if (!currentForm)
|
|
75
108
|
return;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
109
|
+
let resetTimer;
|
|
110
|
+
const reset = (event) => {
|
|
111
|
+
clearTimeout(resetTimer);
|
|
112
|
+
resetTimer = setTimeout(() => {
|
|
113
|
+
if (event.defaultPrevented)
|
|
114
|
+
return;
|
|
115
|
+
if (!controlledSelection)
|
|
116
|
+
setUncontrolledSelection(defaultSelection.current);
|
|
117
|
+
setQuery('');
|
|
118
|
+
if (controlledOpen === undefined)
|
|
119
|
+
setUncontrolledOpen(false);
|
|
120
|
+
}, 0);
|
|
81
121
|
};
|
|
82
122
|
currentForm.addEventListener('reset', reset);
|
|
83
|
-
return () =>
|
|
84
|
-
|
|
123
|
+
return () => {
|
|
124
|
+
clearTimeout(resetTimer);
|
|
125
|
+
currentForm.removeEventListener('reset', reset);
|
|
126
|
+
};
|
|
127
|
+
}, [controlledOpen, controlledSelection, form]);
|
|
85
128
|
const handleSelectionChange = (nextSelection) => {
|
|
86
129
|
if (resolvedDisabled || resolvedReadOnly)
|
|
87
130
|
return;
|
|
@@ -92,7 +135,7 @@ export const TreeSelect = forwardRef(function TreeSelect(props, ref) {
|
|
|
92
135
|
if (!toggledId)
|
|
93
136
|
return;
|
|
94
137
|
const selected = added === toggledId;
|
|
95
|
-
const next =
|
|
138
|
+
const next = updateTreeSelectSelectionFromIndex(normalized, selectedIds, toggledId, selected, {
|
|
96
139
|
multiple: Boolean(props.multiple),
|
|
97
140
|
branchSelection,
|
|
98
141
|
selectionPropagation,
|
|
@@ -120,23 +163,17 @@ export const TreeSelect = forwardRef(function TreeSelect(props, ref) {
|
|
|
120
163
|
event.preventDefault();
|
|
121
164
|
treeRef.current?.focus();
|
|
122
165
|
};
|
|
123
|
-
const renderNodes = (nodes) => nodes.map(node => {
|
|
124
|
-
const branch = Boolean(node.children?.length);
|
|
125
|
-
const label = renderLabel?.(node, {
|
|
126
|
-
branch,
|
|
127
|
-
disabled: Boolean(node.disabled),
|
|
128
|
-
selected: selectedSet.has(node.id),
|
|
129
|
-
query,
|
|
130
|
-
}) ?? node.label;
|
|
131
|
-
return _jsxs(Tree.Item, { className: "inkra-tree-select__item", id: node.id, isDisabled: node.disabled, textValue: node.label, children: [_jsxs(Tree.ItemContent, { collapseLabel: collapseLabel, expandLabel: expandLabel, children: [_jsx(Tree.ItemTitle, { children: label }), node.description ? _jsx(Tree.ItemDescription, { children: node.description }) : null] }), node.children?.length ? renderNodes(node.children) : null] }, node.id);
|
|
132
|
-
});
|
|
133
166
|
const fallbackValue = selectedIds.map(selectedId => normalized.byId.get(selectedId)?.label ?? selectedId).join(', ');
|
|
134
167
|
const valueContent = renderValue?.(selectedNodes, selectedIds)
|
|
135
168
|
?? (fallbackValue ? _jsx(PencilText, { ellipsis: true, children: fallbackValue }) : _jsx(PencilText, { ellipsis: true, children: placeholder }));
|
|
136
169
|
const indicatorSize = size === 'sm' ? 24 : size === 'lg' ? 32 : 28;
|
|
137
170
|
const exposedOpen = resolvedDisabled || resolvedReadOnly ? false : isOpen;
|
|
138
|
-
return _jsxs("div", { ...rest, className: ['inkra-tree-select', className].filter(Boolean).join(' '), "data-disabled": resolvedDisabled || undefined, "data-invalid": resolvedInvalid || undefined, "data-readonly": resolvedReadOnly || undefined, "data-size": size, ref: ref, children: [_jsxs(Popover.Root, { open: exposedOpen, onOpenChange: setOpen, children: [_jsxs("
|
|
171
|
+
return _jsxs("div", { ...rest, className: ['inkra-tree-select', className].filter(Boolean).join(' '), "data-disabled": resolvedDisabled || undefined, "data-invalid": resolvedInvalid || undefined, "data-readonly": resolvedReadOnly || undefined, "data-size": size, ref: ref, children: [_jsxs(Popover.Root, { open: exposedOpen, onOpenChange: setOpen, children: [_jsxs(ButtonGroup, { "aria-label": ariaLabel ?? (ariaLabelledBy ? undefined : treeLabel), "aria-labelledby": ariaLabelledBy, className: "inkra-tree-select__control", "data-size": size, disabled: resolvedDisabled, fullWidth: true, size: size, variant: "outline", children: [_jsx(Popover.Trigger, { asChild: true, children: _jsx(Button, { "aria-describedby": describedBy, "aria-invalid": resolvedInvalid || undefined, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-readonly": resolvedReadOnly || undefined, "aria-required": resolvedRequired || undefined, className: "inkra-tree-select__trigger", disabled: resolvedDisabled, endIcon: _jsx(PencilIcon, { paths: chevronPaths, size: indicatorSize, sourceSize: 24 }), fullWidth: true, id: controlId, ref: triggerRef, size: size, children: _jsx("span", { className: "inkra-tree-select__value", "data-placeholder": !selectedIds.length || undefined, children: valueContent }) }) }), clearable ? _jsx(Button, { "aria-hidden": !selectedIds.length || undefined, "aria-label": clearLabel, className: "inkra-tree-select__clear", disabled: resolvedDisabled || resolvedReadOnly || selectedIds.length === 0, onPointerDown: event => event.preventDefault(), onClick: () => {
|
|
172
|
+
emitSelection([], { reason: 'clear' });
|
|
173
|
+
setOpen(false);
|
|
174
|
+
triggerRef.current?.focus({ preventScroll: true });
|
|
175
|
+
}, size: size === 'sm' ? 'icon-sm' : size === 'lg' ? 'icon-lg' : 'icon', tabIndex: selectedIds.length ? undefined : -1, variant: "ghost", children: _jsx(PencilIcon, { paths: clearPaths, size: 24, sourceSize: 24 }) }) : null] }), _jsx(Popover.Portal, { children: _jsxs(Popover.Content, { align: align, "aria-label": treeLabel, className: "inkra-tree-select__popover", collisionPadding: collisionPadding, onOpenAutoFocus: (event) => {
|
|
139
176
|
event.preventDefault();
|
|
140
177
|
searchRef.current?.focus();
|
|
141
|
-
}, side: side, sideOffset: sideOffset, children: [_jsx("div", { className: "inkra-tree-select__search", children: _jsx(Input, { "aria-label": searchPlaceholder, clearable: true, clearButtonLabel: clearLabel, onKeyDown: handleSearchKeyDown, onValueChange: setQuery, placeholder: searchPlaceholder, ref: searchRef, size: size, startIcon: _jsx(PencilIcon, { paths: searchPaths, size: 22, sourceSize: 24 }), type: "search", value: query, variant: "underline" }) }), _jsx(
|
|
178
|
+
}, side: side, sideOffset: sideOffset, children: [_jsx("div", { className: "inkra-tree-select__search", children: _jsx(Input, { "aria-label": searchPlaceholder, clearable: true, clearButtonLabel: clearLabel, onKeyDown: handleSearchKeyDown, onValueChange: setQuery, placeholder: searchPlaceholder, ref: searchRef, size: size, startIcon: _jsx(PencilIcon, { paths: searchPaths, size: 22, sourceSize: 24 }), type: "search", value: query, variant: "underline" }) }), _jsx(TreeSelectTree, { collapseLabel: collapseLabel, effectiveExpanded: effectiveExpanded, emptyContent: emptyContent, expandLabel: expandLabel, multiple: Boolean(props.multiple), nodes: filtered.nodes, onExpandedChange: handleExpandedChange, onSelectionChange: handleSelectionChange, query: query, ref: treeRef, renderLabel: renderLabel, selectedSet: selectedSet, treeLabel: treeLabel })] }) })] }), _jsx("input", { "aria-hidden": "true", className: "inkra-tree-select__form-anchor", form: form, ref: formAnchorRef, tabIndex: -1, type: "hidden" }), name && !resolvedDisabled ? selectedIds.map(selectedId => _jsx("input", { form: form, name: name, type: "hidden", value: selectedId }, selectedId)) : null] });
|
|
142
179
|
});
|
|
@@ -21,11 +21,17 @@ export interface FilteredTreeSelectNodes {
|
|
|
21
21
|
visibleIds: ReadonlySet<string>;
|
|
22
22
|
expandedIds: ReadonlySet<string>;
|
|
23
23
|
}
|
|
24
|
-
|
|
25
|
-
export declare function indexTreeSelectNodes(nodes: readonly TreeSelectNode[]): TreeSelectIndex;
|
|
26
|
-
export declare function filterTreeSelectNodes(nodes: readonly TreeSelectNode[], query: string): FilteredTreeSelectNodes;
|
|
27
|
-
export declare function updateTreeSelectSelection(nodes: readonly TreeSelectNode[], current: readonly string[], toggledId: string, selected: boolean, options: {
|
|
24
|
+
interface TreeSelectSelectionOptions {
|
|
28
25
|
multiple: boolean;
|
|
29
26
|
branchSelection: TreeSelectBranchSelection;
|
|
30
27
|
selectionPropagation: TreeSelectSelectionPropagation;
|
|
31
|
-
}
|
|
28
|
+
}
|
|
29
|
+
export declare function normalizeTreeSelectNodes(nodes: readonly TreeSelectNode[]): readonly TreeSelectNode[];
|
|
30
|
+
export declare function indexTreeSelectNodes(nodes: readonly TreeSelectNode[]): TreeSelectIndex;
|
|
31
|
+
export declare function filterTreeSelectNodes(nodes: readonly TreeSelectNode[], query: string): FilteredTreeSelectNodes;
|
|
32
|
+
/** Reuse the component's index instead of cloning every node on each search. */
|
|
33
|
+
export declare function filterTreeSelectNodesFromIndex(index: TreeSelectIndex, query: string): FilteredTreeSelectNodes;
|
|
34
|
+
export declare function updateTreeSelectSelection(nodes: readonly TreeSelectNode[], current: readonly string[], toggledId: string, selected: boolean, options: TreeSelectSelectionOptions): string[];
|
|
35
|
+
/** Reuse the component's index for selection and aggregate each subtree once. */
|
|
36
|
+
export declare function updateTreeSelectSelectionFromIndex(index: TreeSelectIndex, current: readonly string[], toggledId: string, selected: boolean, options: TreeSelectSelectionOptions): string[];
|
|
37
|
+
export {};
|