@inkra/react 0.3.0 → 0.5.0
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 +113 -3
- 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.js +14 -4
- package/dist/audio-player/index.d.ts +2 -0
- package/dist/audio-player/index.js +131 -34
- package/dist/breadcrumb/index.js +4 -1
- package/dist/calendar/index.d.ts +4 -1
- package/dist/calendar/index.js +37 -12
- package/dist/calendar/locales.d.ts +7 -0
- package/dist/calendar/locales.js +2 -0
- package/dist/carousel/index.js +20 -10
- package/dist/choice-grid/index.d.ts +6 -1
- package/dist/choice-grid/index.js +63 -33
- package/dist/color-picker/GradientEditor.d.ts +40 -0
- package/dist/color-picker/GradientEditor.js +64 -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 +144 -40
- package/dist/color-swatch/index.d.ts +10 -0
- package/dist/color-swatch/index.js +9 -0
- package/dist/command/index.js +27 -4
- package/dist/content/index.d.ts +5 -0
- package/dist/content/index.js +15 -3
- package/dist/context-menu/index.js +1 -1
- package/dist/data-table/index.d.ts +7 -1
- package/dist/data-table/index.js +67 -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 +20 -21
- package/dist/day-agenda/index.d.ts +6 -1
- package/dist/day-agenda/index.js +49 -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 +15 -13
- package/dist/drawer/index.js +11 -5
- package/dist/editable/index.d.ts +24 -2
- package/dist/editable/index.js +148 -40
- 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/Field.js +11 -10
- package/dist/form/Input.js +4 -1
- package/dist/form/Textarea.d.ts +3 -1
- package/dist/form/Textarea.js +9 -5
- package/dist/heatmap/index.js +4 -1
- package/dist/hotkey/index.d.ts +1 -0
- package/dist/hotkey/index.js +9 -4
- package/dist/hover-card/index.js +1 -1
- package/dist/i18n/catalog.d.ts +5 -0
- package/dist/i18n/catalog.js +289 -0
- package/dist/i18n/index.d.ts +23 -0
- package/dist/i18n/index.js +50 -0
- package/dist/image-cropper/index.js +21 -17
- package/dist/image-viewer/VideoResource.d.ts +21 -0
- package/dist/image-viewer/VideoResource.js +196 -0
- package/dist/image-viewer/index.d.ts +14 -0
- package/dist/image-viewer/index.js +141 -86
- package/dist/image-viewer/lucideGeometry.d.ts +3 -0
- package/dist/image-viewer/lucideGeometry.js +19 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/input-number/index.js +13 -5
- 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 +3 -0
- package/dist/internal/textRenderer.js +13 -5
- package/dist/internal/usePencilSurface.js +17 -1
- package/dist/internal/wash.js +4 -3
- package/dist/journey-map/index.d.ts +1 -1
- package/dist/journey-map/index.js +6 -3
- package/dist/journey-map/terrain.js +10 -5
- package/dist/keypad/index.js +4 -1
- package/dist/layout/index.js +1 -1
- package/dist/log/index.js +42 -33
- 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 +177 -0
- package/dist/node-graph/index.d.ts +3 -1
- package/dist/node-graph/index.js +63 -28
- package/dist/number-ticker/index.js +4 -1
- package/dist/number-ticker/precision.js +9 -2
- package/dist/overflow-list/index.js +9 -6
- package/dist/overlay/Overlay.js +8 -3
- package/dist/overlay/Popover.js +3 -3
- package/dist/overlay/themeBridge.js +14 -2
- package/dist/pagination/index.d.ts +10 -1
- package/dist/pagination/index.js +13 -9
- package/dist/pan-zoom/index.d.ts +2 -0
- package/dist/pan-zoom/index.js +12 -8
- package/dist/pencil.d.ts +1 -0
- package/dist/pencil.js +4 -4
- package/dist/radar/index.d.ts +2 -0
- package/dist/radar/index.js +44 -21
- package/dist/range-slider/index.js +4 -3
- package/dist/rating/index.d.ts +4 -0
- package/dist/rating/index.js +131 -24
- package/dist/resizable/index.d.ts +1 -1
- package/dist/resizable/index.js +5 -2
- 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 +64 -7
- package/dist/select/Select.d.ts +1 -0
- package/dist/select/Select.js +6 -2
- package/dist/select/StepperSelect.d.ts +29 -0
- package/dist/select/StepperSelect.js +122 -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 +17 -11
- package/dist/sparkline/index.js +21 -6
- package/dist/sparkline/model.js +11 -4
- package/dist/steps/index.js +4 -1
- 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 +554 -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 +2091 -421
- package/dist/tabs/index.d.ts +9 -0
- package/dist/tabs/index.js +69 -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 +81 -42
- package/dist/theme/index.d.ts +11 -0
- package/dist/theme/index.js +23 -0
- package/dist/time-picker/index.js +45 -27
- package/dist/timeline/index.js +4 -1
- package/dist/toast/index.d.ts +19 -11
- package/dist/toast/index.js +51 -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.js +3 -3
- package/dist/transfer/index.d.ts +5 -0
- package/dist/transfer/index.js +120 -53
- 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 +11 -6
- package/dist/tree-select/index.js +70 -31
- 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 +50 -33
- package/dist/virtual-list/index.js +11 -6
- package/package.json +423 -3
package/dist/transfer/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useInkraLocale } from '../i18n/index.js';
|
|
2
3
|
import { forwardRef, useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
3
4
|
import { Button } from '../Button.js';
|
|
4
5
|
import { Input } from '../form/Input.js';
|
|
@@ -7,33 +8,34 @@ import { PencilLine } from '../PencilLine.js';
|
|
|
7
8
|
import { PencilSurface } from '../PencilSurface.js';
|
|
8
9
|
import { PencilText } from '../PencilText.js';
|
|
9
10
|
import { Checkbox } from '../selection/Checkbox.js';
|
|
10
|
-
import { moveTransferKeys, normalizeTargetKeys, splitTransferItems, uniqueTransferItems, } from './model.js';
|
|
11
|
+
import { moveTransferKeys, normalizeTargetKeys, resolveTransferFocusIntent, splitTransferItems, transferKeysEqual, uniqueTransferItems, } from './model.js';
|
|
11
12
|
const searchPaths = [
|
|
12
13
|
{ 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
14
|
];
|
|
14
15
|
const rightPaths = [{ d: 'M 5 12 L 19 12 M 13 6 L 19 12 L 13 18' }];
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
16
|
+
const defaultLabels = (t) => ({
|
|
17
|
+
sourceTitle: t('transfer.available'),
|
|
18
|
+
targetTitle: t('common.selected'),
|
|
19
|
+
sourceSearchLabel: t('transfer.sourceSearch'),
|
|
20
|
+
targetSearchLabel: t('transfer.targetSearch'),
|
|
21
|
+
sourceClearSearch: t('transfer.sourceClear'),
|
|
22
|
+
targetClearSearch: t('transfer.targetClear'),
|
|
23
|
+
sourceSearchPlaceholder: t('transfer.sourcePlaceholder'),
|
|
24
|
+
targetSearchPlaceholder: t('transfer.targetPlaceholder'),
|
|
25
|
+
selectAllSource: t('transfer.allSource'),
|
|
26
|
+
selectAllTarget: t('transfer.allTarget'),
|
|
27
|
+
sourceEmpty: t('transfer.sourceEmpty'),
|
|
28
|
+
targetEmpty: t('transfer.targetEmpty'),
|
|
29
|
+
sourceNoResults: t('transfer.sourceNoResults'),
|
|
30
|
+
targetNoResults: t('transfer.targetNoResults'),
|
|
31
|
+
moveToTarget: t('transfer.toTarget'),
|
|
32
|
+
moveToSource: t('transfer.toSource'),
|
|
33
|
+
moveSelectedToTarget: t('transfer.selectedToTarget'),
|
|
34
|
+
moveSelectedToSource: t('transfer.selectedToSource'),
|
|
35
|
+
batchActions: t('transfer.batch'),
|
|
36
|
+
previousPage: t('common.previousPage'),
|
|
37
|
+
nextPage: t('common.nextPage'),
|
|
38
|
+
});
|
|
37
39
|
function searchableText(item) {
|
|
38
40
|
const parts = [
|
|
39
41
|
item.searchText,
|
|
@@ -43,57 +45,113 @@ function searchableText(item) {
|
|
|
43
45
|
];
|
|
44
46
|
return parts.filter(Boolean).join(' ').toLocaleLowerCase();
|
|
45
47
|
}
|
|
46
|
-
function
|
|
47
|
-
|
|
48
|
+
function filterPanelItems(items, query, side, filterItem) {
|
|
49
|
+
if (query.length === 0)
|
|
50
|
+
return items;
|
|
51
|
+
if (filterItem)
|
|
52
|
+
return items.filter(item => filterItem(item, query, side));
|
|
53
|
+
const needle = query.trim().toLocaleLowerCase();
|
|
54
|
+
return items.filter(item => searchableText(item).includes(needle));
|
|
48
55
|
}
|
|
49
|
-
function defaultCount({ visible, total, selected }) {
|
|
50
|
-
return selected > 0 ?
|
|
56
|
+
function defaultCount({ visible, total, selected }, t) {
|
|
57
|
+
return selected > 0 ? t('transfer.count', { selected, visible, total }) : `${visible}/${total}`;
|
|
51
58
|
}
|
|
52
59
|
function itemCopy(item) {
|
|
53
60
|
return _jsxs("span", { className: "inkra-transfer__item-copy", children: [_jsx("span", { className: "inkra-transfer__item-label", children: typeof item.label === 'string' || typeof item.label === 'number'
|
|
54
61
|
? _jsx(PencilText, { children: String(item.label) })
|
|
55
62
|
: item.label }), item.description == null ? null : _jsx("span", { className: "inkra-transfer__item-description", children: item.description })] });
|
|
56
63
|
}
|
|
57
|
-
export const Transfer = forwardRef(function Transfer({ className, defaultTargetKeys = [], disabled = false, filterItem, formatCount
|
|
64
|
+
export const Transfer = forwardRef(function Transfer({ className, defaultTargetKeys = [], disabled = false, filterItem, formatCount: formatCountProp, items, labels: labelOverrides, onTargetKeysChange, pageSize, pageLabel = (page, count) => `${page} / ${count}`, renderItem, searchable = true, targetKeys, ...props }, ref) {
|
|
65
|
+
const { t } = useInkraLocale();
|
|
66
|
+
const formatCount = formatCountProp ?? ((details) => defaultCount(details, t));
|
|
58
67
|
const generatedId = useId();
|
|
59
68
|
const controlled = targetKeys !== undefined;
|
|
60
69
|
const uniqueItems = useMemo(() => uniqueTransferItems(items), [items]);
|
|
61
70
|
const [uncontrolledTargetKeys, setUncontrolledTargetKeys] = useState(() => normalizeTargetKeys(items, defaultTargetKeys));
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
const
|
|
71
|
+
const requestedTargetKeys = controlled ? targetKeys : uncontrolledTargetKeys;
|
|
72
|
+
const currentTargetKeys = useMemo(() => normalizeTargetKeys(uniqueItems, requestedTargetKeys), [uniqueItems, requestedTargetKeys]);
|
|
73
|
+
const currentTargetSet = useMemo(() => new Set(currentTargetKeys), [currentTargetKeys]);
|
|
74
|
+
const itemsById = useMemo(() => new Map(uniqueItems.map(item => [item.id, item])), [uniqueItems]);
|
|
75
|
+
const panels = useMemo(() => splitTransferItems(uniqueItems, currentTargetKeys), [uniqueItems, currentTargetKeys]);
|
|
76
|
+
const labels = { ...defaultLabels(t), ...labelOverrides };
|
|
65
77
|
const [queries, setQueries] = useState({ source: '', target: '' });
|
|
78
|
+
const [pages, setPages] = useState({ source: 0, target: 0 });
|
|
79
|
+
const pageLength = pageSize !== undefined && Number.isFinite(pageSize) ? Math.max(1, Math.floor(pageSize)) : undefined;
|
|
66
80
|
const [selected, setSelected] = useState(() => ({ source: new Set(), target: new Set() }));
|
|
67
81
|
const [pendingFocus, setPendingFocus] = useState(null);
|
|
68
82
|
const itemRefs = useRef(new Map());
|
|
69
83
|
const panelRefs = useRef({ source: null, target: null });
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
84
|
+
const visibleSource = useMemo(() => filterPanelItems(panels.source, queries.source, 'source', filterItem), [panels.source, queries.source, filterItem]);
|
|
85
|
+
const visibleTarget = useMemo(() => filterPanelItems(panels.target, queries.target, 'target', filterItem), [panels.target, queries.target, filterItem]);
|
|
86
|
+
const pageIndex = (side, count) => pageLength === undefined ? 0 : Math.min(pages[side], Math.max(0, Math.ceil(count / pageLength) - 1));
|
|
87
|
+
const sourcePage = pageIndex('source', visibleSource.length);
|
|
88
|
+
const targetPage = pageIndex('target', visibleTarget.length);
|
|
89
|
+
const shownSource = useMemo(() => pageLength === undefined ? visibleSource : visibleSource.slice(sourcePage * pageLength, (sourcePage + 1) * pageLength), [visibleSource, sourcePage, pageLength]);
|
|
90
|
+
const shownTarget = useMemo(() => pageLength === undefined ? visibleTarget : visibleTarget.slice(targetPage * pageLength, (targetPage + 1) * pageLength), [visibleTarget, targetPage, pageLength]);
|
|
91
|
+
const visibleItems = { source: shownSource, target: shownTarget };
|
|
75
92
|
useEffect(() => {
|
|
76
|
-
const valid = new Set(uniqueItems.map(item => item.id));
|
|
77
93
|
setSelected(current => {
|
|
78
|
-
const source = new Set([...current.source].filter(key =>
|
|
79
|
-
const target = new Set([...current.target].filter(key =>
|
|
94
|
+
const source = new Set([...current.source].filter(key => itemsById.has(key) && !currentTargetSet.has(key)));
|
|
95
|
+
const target = new Set([...current.target].filter(key => itemsById.has(key) && currentTargetSet.has(key)));
|
|
80
96
|
if (source.size === current.source.size && target.size === current.target.size)
|
|
81
97
|
return current;
|
|
82
98
|
return { source, target };
|
|
83
99
|
});
|
|
84
|
-
}, [
|
|
100
|
+
}, [itemsById, currentTargetSet]);
|
|
85
101
|
useEffect(() => {
|
|
86
102
|
if (!pendingFocus)
|
|
87
103
|
return;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
104
|
+
if (disabled || !itemsById.has(pendingFocus.id)) {
|
|
105
|
+
setPendingFocus(null);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const valueResolution = transferKeysEqual(currentTargetKeys, pendingFocus.expectedTargetKeys)
|
|
109
|
+
? 'accepted'
|
|
110
|
+
: transferKeysEqual(currentTargetKeys, pendingFocus.previousTargetKeys) ? 'unchanged' : 'mismatch';
|
|
111
|
+
const ownerDocument = pendingFocus.origin?.ownerDocument ?? document;
|
|
112
|
+
const activeElement = ownerDocument.activeElement;
|
|
113
|
+
const originStillOwnsFocus = activeElement === pendingFocus.origin;
|
|
114
|
+
const focusFellBackToDocument = activeElement === ownerDocument.body || activeElement === ownerDocument.documentElement;
|
|
115
|
+
const originUnavailable = pendingFocus.origin != null
|
|
116
|
+
&& (!pendingFocus.origin.isConnected || pendingFocus.origin.matches(':disabled'))
|
|
117
|
+
&& focusFellBackToDocument;
|
|
118
|
+
if (valueResolution !== 'accepted') {
|
|
119
|
+
if (resolveTransferFocusIntent(valueResolution, originStillOwnsFocus, originUnavailable) !== 'wait') {
|
|
120
|
+
setPendingFocus(null);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const cancel = (event) => {
|
|
124
|
+
const target = event.target;
|
|
125
|
+
const documentFallback = target === ownerDocument.body || target === ownerDocument.documentElement;
|
|
126
|
+
const unavailable = pendingFocus.origin != null
|
|
127
|
+
&& (!pendingFocus.origin.isConnected || pendingFocus.origin.matches(':disabled'));
|
|
128
|
+
if (target !== pendingFocus.origin && !(documentFallback && unavailable))
|
|
129
|
+
setPendingFocus(null);
|
|
130
|
+
};
|
|
131
|
+
ownerDocument.addEventListener('focusin', cancel);
|
|
132
|
+
return () => ownerDocument.removeEventListener('focusin', cancel);
|
|
133
|
+
}
|
|
134
|
+
if (resolveTransferFocusIntent('accepted', originStillOwnsFocus, originUnavailable) !== 'focus') {
|
|
135
|
+
setPendingFocus(null);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (pageLength !== undefined) {
|
|
139
|
+
const matching = pendingFocus.side === 'source' ? visibleSource : visibleTarget;
|
|
140
|
+
const index = matching.findIndex(item => item.id === pendingFocus.id);
|
|
141
|
+
const nextPage = index < 0 ? undefined : Math.floor(index / pageLength);
|
|
142
|
+
if (nextPage !== undefined && nextPage !== pages[pendingFocus.side]) {
|
|
143
|
+
setPages(current => ({ ...current, [pendingFocus.side]: nextPage }));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const control = itemRefs.current.get(`${pendingFocus.side}:${pendingFocus.id}`);
|
|
92
148
|
(control ?? panelRefs.current[pendingFocus.side])?.focus({ preventScroll: true });
|
|
93
149
|
setPendingFocus(null);
|
|
94
|
-
}, [pendingFocus, currentTargetKeys
|
|
150
|
+
}, [disabled, pendingFocus, currentTargetKeys, itemsById, pageLength, pages, visibleSource, visibleTarget]);
|
|
95
151
|
const setItemSelected = (side, id, checked) => {
|
|
96
152
|
setSelected(current => {
|
|
153
|
+
if (current[side].has(id) === checked)
|
|
154
|
+
return current;
|
|
97
155
|
const next = new Set(current[side]);
|
|
98
156
|
if (checked)
|
|
99
157
|
next.add(id);
|
|
@@ -111,11 +169,11 @@ export const Transfer = forwardRef(function Transfer({ className, defaultTargetK
|
|
|
111
169
|
return { ...current, [side]: next };
|
|
112
170
|
});
|
|
113
171
|
};
|
|
114
|
-
const move = (keys, direction, action) => {
|
|
172
|
+
const move = (keys, direction, action, origin) => {
|
|
115
173
|
const sourceSide = direction === 'to-target' ? 'source' : 'target';
|
|
116
174
|
const destinationSide = direction === 'to-target' ? 'target' : 'source';
|
|
117
175
|
const allowed = new Set(panels[sourceSide].filter(item => !item.disabled && !disabled).map(item => item.id));
|
|
118
|
-
const movedKeys = keys.filter(
|
|
176
|
+
const movedKeys = [...new Set(keys)].filter(key => allowed.has(key));
|
|
119
177
|
if (movedKeys.length === 0)
|
|
120
178
|
return;
|
|
121
179
|
const next = moveTransferKeys(uniqueItems, currentTargetKeys, movedKeys, direction);
|
|
@@ -126,12 +184,18 @@ export const Transfer = forwardRef(function Transfer({ className, defaultTargetK
|
|
|
126
184
|
movedKeys.forEach(key => source.delete(key));
|
|
127
185
|
return { ...current, [sourceSide]: source };
|
|
128
186
|
});
|
|
129
|
-
setPendingFocus({
|
|
187
|
+
setPendingFocus({
|
|
188
|
+
id: movedKeys[0],
|
|
189
|
+
side: destinationSide,
|
|
190
|
+
origin,
|
|
191
|
+
previousTargetKeys: [...currentTargetKeys],
|
|
192
|
+
expectedTargetKeys: next,
|
|
193
|
+
});
|
|
130
194
|
onTargetKeysChange?.(next, {
|
|
131
195
|
action,
|
|
132
196
|
direction,
|
|
133
197
|
movedKeys,
|
|
134
|
-
movedItems: movedKeys.map(key =>
|
|
198
|
+
movedItems: movedKeys.map(key => itemsById.get(key)).filter((item) => item !== undefined),
|
|
135
199
|
});
|
|
136
200
|
};
|
|
137
201
|
const renderPanel = (side) => {
|
|
@@ -154,7 +218,10 @@ export const Transfer = forwardRef(function Transfer({ className, defaultTargetK
|
|
|
154
218
|
const selectAllLabel = source ? labels.selectAllSource : labels.selectAllTarget;
|
|
155
219
|
const direction = source ? 'to-target' : 'to-source';
|
|
156
220
|
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 =>
|
|
221
|
+
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 => {
|
|
222
|
+
setQueries(current => ({ ...current, [side]: value }));
|
|
223
|
+
setPages(current => ({ ...current, [side]: 0 }));
|
|
224
|
+
}, 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
225
|
const itemDisabled = disabled || item.disabled === true;
|
|
159
226
|
const itemSelected = selected[side].has(item.id);
|
|
160
227
|
const copy = renderItem?.(item, { side, selected: itemSelected, disabled: itemDisabled, query: queries[side] }) ?? itemCopy(item);
|
|
@@ -164,11 +231,11 @@ export const Transfer = forwardRef(function Transfer({ className, defaultTargetK
|
|
|
164
231
|
itemRefs.current.set(key, element);
|
|
165
232
|
else
|
|
166
233
|
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
|
-
}) })] });
|
|
234
|
+
}, 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);
|
|
235
|
+
}) }), 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
236
|
};
|
|
170
237
|
const sourceBatchKeys = selectableVisible('source').filter(item => selected.source.has(item.id)).map(item => item.id);
|
|
171
238
|
const targetBatchKeys = selectableVisible('target').filter(item => selected.target.has(item.id)).map(item => item.id);
|
|
172
|
-
return
|
|
239
|
+
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
240
|
});
|
|
174
241
|
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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useInkraLocale } from '../i18n/index.js';
|
|
2
3
|
import { Tree as AriaTree, TreeItem as AriaTreeItem, TreeItemContent as AriaTreeItemContent, } from 'react-aria-components/Tree';
|
|
3
4
|
import { Button as AriaButton } from 'react-aria-components/Button';
|
|
4
5
|
import { forwardRef } from 'react';
|
|
@@ -6,26 +7,30 @@ import { PencilIcon } from '../PencilIcon.js';
|
|
|
6
7
|
import { PencilSurface } from '../PencilSurface.js';
|
|
7
8
|
import { PencilText } from '../PencilText.js';
|
|
8
9
|
const chevronIcon = [{ d: 'M 7 10 L 12 15 L 17 10' }];
|
|
9
|
-
function TreeRootInner(
|
|
10
|
-
|
|
10
|
+
function TreeRootInner(inkraProps, ref) {
|
|
11
|
+
const { t } = useInkraLocale();
|
|
12
|
+
const { className, style, indent = 24, size = 'md', emptyState = t('common.noItems'), selectionMode = 'single', selectionBehavior = 'toggle', ...props } = inkraProps;
|
|
13
|
+
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
14
|
}
|
|
12
15
|
const TreeRoot = forwardRef(TreeRootInner);
|
|
13
16
|
const TreeItem = forwardRef(function TreeItem({ className, ...props }, ref) {
|
|
14
17
|
return _jsx(AriaTreeItem, { ...props, ref: ref, className: ['inkra-tree__item', className].filter(Boolean).join(' ') });
|
|
15
18
|
});
|
|
16
|
-
const TreeItemContent = forwardRef(function TreeItemContent(
|
|
19
|
+
const TreeItemContent = forwardRef(function TreeItemContent(inkraProps, ref) {
|
|
20
|
+
const { t } = useInkraLocale();
|
|
21
|
+
const { children, className, icon, disclosureIcon, expandLabel = t('tree.expand'), collapseLabel = t('tree.collapse'), } = inkraProps;
|
|
17
22
|
return _jsx(AriaTreeItemContent, { ref: ref, children: ({ hasChildItems, isExpanded, isSelected, isFocusVisible, isDisabled, level, id, }) => {
|
|
18
|
-
const state = { isExpanded, hasChildItems, isSelected, isDisabled };
|
|
23
|
+
const state = { isExpanded, hasChildItems, isSelected, isDisabled, level };
|
|
19
24
|
const resolvedIcon = typeof icon === 'function' ? icon(state) : icon;
|
|
20
25
|
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 })] });
|
|
26
|
+
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
27
|
} });
|
|
23
28
|
});
|
|
24
29
|
const TreeItemTitle = forwardRef(function TreeItemTitle({ className, children, ...props }, ref) {
|
|
25
30
|
return _jsx("span", { ...props, ref: ref, className: ['inkra-tree__title', className].filter(Boolean).join(' '), children: typeof children === 'string' ? _jsx(PencilText, { children: children }) : children });
|
|
26
31
|
});
|
|
27
32
|
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 });
|
|
33
|
+
return _jsx("span", { ...props, ref: ref, className: ['inkra-tree__description', className].filter(Boolean).join(' '), children: typeof children === 'string' ? _jsx(PencilText, { children: children }) : children });
|
|
29
34
|
});
|
|
30
35
|
export const Tree = {
|
|
31
36
|
Root: TreeRoot,
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useInkraLocale } from '../i18n/index.js';
|
|
2
3
|
import { forwardRef, useEffect, useId, useMemo, useRef, useState, } from 'react';
|
|
3
|
-
import { Button } from '../Button.js';
|
|
4
|
+
import { Button, ButtonGroup } from '../Button.js';
|
|
4
5
|
import { Input } from '../form/Input.js';
|
|
5
6
|
import { mergeAriaIds, useFieldContext } from '../form/context.js';
|
|
6
7
|
import { PencilIcon } from '../PencilIcon.js';
|
|
7
8
|
import { PencilText } from '../PencilText.js';
|
|
8
9
|
import { Popover } from '../overlay/index.js';
|
|
9
10
|
import { Tree } from '../tree/index.js';
|
|
10
|
-
import {
|
|
11
|
+
import { filterTreeSelectNodesFromIndex, indexTreeSelectNodes, updateTreeSelectSelectionFromIndex, } from './model.js';
|
|
11
12
|
const chevronPaths = [{ d: 'M6 9l6 6 6-6' }];
|
|
12
13
|
const clearPaths = [{ d: 'M6 6l12 12M18 6L6 18' }];
|
|
13
14
|
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,8 +21,44 @@ function selectionFromProps(props, fallback = []) {
|
|
|
20
21
|
function keys(selection) {
|
|
21
22
|
return selection === 'all' ? [] : [...selection].map(String);
|
|
22
23
|
}
|
|
24
|
+
// The portal owns mounting (including its exit animation). Keep collection
|
|
25
|
+
// construction here rather than doing work for content the portal never mounts.
|
|
26
|
+
const TreeSelectTree = forwardRef(function TreeSelectTree({ nodes, renderLabel, selectedSet, query, collapseLabel, expandLabel, treeLabel, emptyContent, effectiveExpanded, onExpandedChange, onSelectionChange, multiple, }, ref) {
|
|
27
|
+
// Default labels are selection-independent; only custom label renderers need
|
|
28
|
+
// a new collection when the selected keys change.
|
|
29
|
+
const labelSelection = renderLabel ? selectedSet : undefined;
|
|
30
|
+
const treeItems = useMemo(() => {
|
|
31
|
+
// Construct the same nested tree without growing the JavaScript call stack.
|
|
32
|
+
// Enter jobs keep renderLabel callbacks in the original depth-first order.
|
|
33
|
+
const result = [];
|
|
34
|
+
const stack = [];
|
|
35
|
+
for (let index = nodes.length - 1; index >= 0; index--)
|
|
36
|
+
stack.push({ node: nodes[index], output: result });
|
|
37
|
+
while (stack.length) {
|
|
38
|
+
const { node, output, finish } = stack.pop();
|
|
39
|
+
if (finish) {
|
|
40
|
+
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));
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const label = renderLabel?.(node, {
|
|
44
|
+
branch: Boolean(node.children?.length),
|
|
45
|
+
disabled: Boolean(node.disabled),
|
|
46
|
+
selected: labelSelection?.has(node.id) ?? false,
|
|
47
|
+
query,
|
|
48
|
+
}) ?? node.label;
|
|
49
|
+
const children = [];
|
|
50
|
+
stack.push({ node, output, finish: { label, children } });
|
|
51
|
+
for (let index = (node.children?.length ?? 0) - 1; index >= 0; index--) {
|
|
52
|
+
stack.push({ node: node.children[index], output: children });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}, [nodes, renderLabel, labelSelection, query, collapseLabel, expandLabel]);
|
|
57
|
+
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 });
|
|
58
|
+
});
|
|
23
59
|
export const TreeSelect = forwardRef(function TreeSelect(props, ref) {
|
|
24
|
-
const {
|
|
60
|
+
const { t } = useInkraLocale();
|
|
61
|
+
const { items, branchSelection = 'leaf', selectionPropagation = 'none', renderLabel, renderValue, placeholder = t('treeSelect.placeholder'), searchPlaceholder = t('treeSelect.search'), emptyContent = t('treeSelect.empty'), clearable = true, clearLabel = t('select.clear'), treeLabel = t('treeSelect.options'), expandLabel = t('tree.expand'), collapseLabel = t('tree.collapse'), 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
62
|
const field = useFieldContext();
|
|
26
63
|
const generatedId = useId();
|
|
27
64
|
const controlId = field?.controlId ?? id ?? `inkra-tree-select-${generatedId}`;
|
|
@@ -31,25 +68,25 @@ export const TreeSelect = forwardRef(function TreeSelect(props, ref) {
|
|
|
31
68
|
const resolvedInvalid = Boolean(invalid || field?.invalid);
|
|
32
69
|
const describedBy = mergeAriaIds(ariaDescribedBy, field?.descriptionId, field?.errorId);
|
|
33
70
|
const normalized = useMemo(() => indexTreeSelectNodes(items), [items]);
|
|
34
|
-
const defaultSelection = useRef(selectionFromProps(props));
|
|
35
71
|
const controlledSelection = props.value !== undefined;
|
|
36
|
-
const [uncontrolledSelection, setUncontrolledSelection] = useState(
|
|
37
|
-
const
|
|
72
|
+
const [uncontrolledSelection, setUncontrolledSelection] = useState(() => selectionFromProps(props));
|
|
73
|
+
const defaultSelection = useRef(uncontrolledSelection);
|
|
74
|
+
const selectedIds = useMemo(() => controlledSelection ? selectionFromProps(props) : uncontrolledSelection, [controlledSelection, props.value, props.multiple, uncontrolledSelection]);
|
|
38
75
|
const selectedSet = useMemo(() => new Set(selectedIds), [selectedIds]);
|
|
39
76
|
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
|
|
40
77
|
const isOpen = controlledOpen ?? uncontrolledOpen;
|
|
41
78
|
const [uncontrolledExpanded, setUncontrolledExpanded] = useState(() => [...new Set(defaultExpandedIds)]);
|
|
42
79
|
const baseExpanded = expandedIds ?? uncontrolledExpanded;
|
|
43
80
|
const [query, setQuery] = useState('');
|
|
44
|
-
const filtered = useMemo(() =>
|
|
81
|
+
const filtered = useMemo(() => filterTreeSelectNodesFromIndex(normalized, query), [normalized, query]);
|
|
45
82
|
const effectiveExpanded = useMemo(() => new Set([...baseExpanded, ...filtered.expandedIds]), [baseExpanded, filtered.expandedIds]);
|
|
46
83
|
const triggerRef = useRef(null);
|
|
47
84
|
const searchRef = useRef(null);
|
|
48
85
|
const treeRef = useRef(null);
|
|
49
86
|
const formAnchorRef = useRef(null);
|
|
50
|
-
const selectedNodes = selectedIds
|
|
87
|
+
const selectedNodes = useMemo(() => selectedIds
|
|
51
88
|
.map(selectedId => normalized.byId.get(selectedId))
|
|
52
|
-
.filter((node) => node !== undefined);
|
|
89
|
+
.filter((node) => node !== undefined), [selectedIds, normalized]);
|
|
53
90
|
const setOpen = (next) => {
|
|
54
91
|
if (resolvedDisabled || resolvedReadOnly)
|
|
55
92
|
return;
|
|
@@ -68,20 +105,28 @@ export const TreeSelect = forwardRef(function TreeSelect(props, ref) {
|
|
|
68
105
|
props.onValueChange?.(next[0] ?? null, details);
|
|
69
106
|
};
|
|
70
107
|
useEffect(() => {
|
|
71
|
-
if (controlledSelection)
|
|
72
|
-
return;
|
|
73
108
|
const currentForm = formAnchorRef.current?.form;
|
|
74
109
|
if (!currentForm)
|
|
75
110
|
return;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
111
|
+
let resetTimer;
|
|
112
|
+
const reset = (event) => {
|
|
113
|
+
clearTimeout(resetTimer);
|
|
114
|
+
resetTimer = setTimeout(() => {
|
|
115
|
+
if (event.defaultPrevented)
|
|
116
|
+
return;
|
|
117
|
+
if (!controlledSelection)
|
|
118
|
+
setUncontrolledSelection(defaultSelection.current);
|
|
119
|
+
setQuery('');
|
|
120
|
+
if (controlledOpen === undefined)
|
|
121
|
+
setUncontrolledOpen(false);
|
|
122
|
+
}, 0);
|
|
81
123
|
};
|
|
82
124
|
currentForm.addEventListener('reset', reset);
|
|
83
|
-
return () =>
|
|
84
|
-
|
|
125
|
+
return () => {
|
|
126
|
+
clearTimeout(resetTimer);
|
|
127
|
+
currentForm.removeEventListener('reset', reset);
|
|
128
|
+
};
|
|
129
|
+
}, [controlledOpen, controlledSelection, form]);
|
|
85
130
|
const handleSelectionChange = (nextSelection) => {
|
|
86
131
|
if (resolvedDisabled || resolvedReadOnly)
|
|
87
132
|
return;
|
|
@@ -92,7 +137,7 @@ export const TreeSelect = forwardRef(function TreeSelect(props, ref) {
|
|
|
92
137
|
if (!toggledId)
|
|
93
138
|
return;
|
|
94
139
|
const selected = added === toggledId;
|
|
95
|
-
const next =
|
|
140
|
+
const next = updateTreeSelectSelectionFromIndex(normalized, selectedIds, toggledId, selected, {
|
|
96
141
|
multiple: Boolean(props.multiple),
|
|
97
142
|
branchSelection,
|
|
98
143
|
selectionPropagation,
|
|
@@ -120,23 +165,17 @@ export const TreeSelect = forwardRef(function TreeSelect(props, ref) {
|
|
|
120
165
|
event.preventDefault();
|
|
121
166
|
treeRef.current?.focus();
|
|
122
167
|
};
|
|
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
168
|
const fallbackValue = selectedIds.map(selectedId => normalized.byId.get(selectedId)?.label ?? selectedId).join(', ');
|
|
134
169
|
const valueContent = renderValue?.(selectedNodes, selectedIds)
|
|
135
170
|
?? (fallbackValue ? _jsx(PencilText, { ellipsis: true, children: fallbackValue }) : _jsx(PencilText, { ellipsis: true, children: placeholder }));
|
|
136
171
|
const indicatorSize = size === 'sm' ? 24 : size === 'lg' ? 32 : 28;
|
|
137
172
|
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("
|
|
173
|
+
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: () => {
|
|
174
|
+
emitSelection([], { reason: 'clear' });
|
|
175
|
+
setOpen(false);
|
|
176
|
+
triggerRef.current?.focus({ preventScroll: true });
|
|
177
|
+
}, 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
178
|
event.preventDefault();
|
|
140
179
|
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(
|
|
180
|
+
}, 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
181
|
});
|