@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/tabs/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as Primitive from '@radix-ui/react-tabs';
|
|
2
2
|
import { type ReactNode } from 'react';
|
|
3
|
+
import { type TabsOverflow } from './useOverflow.js';
|
|
4
|
+
export type { TabsOverflow } from './useOverflow.js';
|
|
3
5
|
export interface TabsProps extends Primitive.TabsProps {
|
|
4
6
|
size?: 'sm' | 'md' | 'lg';
|
|
5
7
|
animated?: boolean;
|
|
@@ -8,10 +10,17 @@ export interface TabsListProps extends Primitive.TabsListProps {
|
|
|
8
10
|
align?: 'start' | 'center' | 'end';
|
|
9
11
|
fullWidth?: boolean;
|
|
10
12
|
divided?: boolean;
|
|
13
|
+
/** Horizontal overflow interaction. Vertical lists retain native scrolling. */
|
|
14
|
+
overflow?: TabsOverflow;
|
|
15
|
+
moreLabel?: string;
|
|
16
|
+
scrollPreviousLabel?: string;
|
|
17
|
+
scrollNextLabel?: string;
|
|
11
18
|
}
|
|
12
19
|
export interface TabsTriggerProps extends Omit<Primitive.TabsTriggerProps, 'asChild'> {
|
|
13
20
|
startIcon?: ReactNode;
|
|
14
21
|
endIcon?: ReactNode;
|
|
22
|
+
/** Plain text used by the overflow menu for a custom trigger label. */
|
|
23
|
+
overflowLabel?: string;
|
|
15
24
|
}
|
|
16
25
|
export type TabsContentProps = Primitive.TabsContentProps;
|
|
17
26
|
export declare const Tabs: {
|
package/dist/tabs/index.js
CHANGED
|
@@ -1,39 +1,89 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import * as Primitive from '@radix-ui/react-tabs';
|
|
3
|
-
import { createContext, forwardRef, useContext, useRef, useState } from 'react';
|
|
3
|
+
import { createContext, forwardRef, useCallback, useContext, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { __iconData as chevronLeft } from 'lucide-react/dist/esm/icons/chevron-left.mjs';
|
|
5
|
+
import { __iconData as chevronRight } from 'lucide-react/dist/esm/icons/chevron-right.mjs';
|
|
6
|
+
import { Button } from '../Button.js';
|
|
7
|
+
import { DropdownMenu } from '../dropdown-menu/index.js';
|
|
8
|
+
import { PencilIcon } from '../PencilIcon.js';
|
|
4
9
|
import { PencilSurface } from '../PencilSurface.js';
|
|
5
10
|
import { PencilText } from '../PencilText.js';
|
|
6
11
|
import { PencilLine } from '../PencilLine.js';
|
|
7
12
|
import { useSelectionDrawing } from '../internal/useSelectionDrawing.js';
|
|
13
|
+
import { revealTab, useTabsOverflow } from './useOverflow.js';
|
|
14
|
+
const leftPaths = chevronLeft.node.flatMap(([, attributes]) => attributes.d ? [{ d: attributes.d }] : []);
|
|
15
|
+
const rightPaths = chevronRight.node.flatMap(([, attributes]) => attributes.d ? [{ d: attributes.d }] : []);
|
|
16
|
+
function OverflowIcon({ left = false, down = false }) {
|
|
17
|
+
const paths = left ? leftPaths : rightPaths;
|
|
18
|
+
return _jsxs("span", { className: "inkra-tabs__overflow-icon", "data-down": down || undefined, "aria-hidden": "true", children: [_jsx(PencilIcon, { size: 32, sourceSize: 24, paths: paths }), _jsx("svg", { viewBox: "0 0 40 40", focusable: "false", children: _jsx("g", { transform: "translate(8 8)", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: paths.map((path, index) => _jsx("path", { d: path.d }, index)) }) })] });
|
|
19
|
+
}
|
|
8
20
|
const ValueContext = createContext(undefined);
|
|
9
21
|
const AnimatedContext = createContext(true);
|
|
10
22
|
const OrientationContext = createContext('horizontal');
|
|
11
23
|
const DividedContext = createContext(false);
|
|
24
|
+
const ChangeContext = createContext(() => { });
|
|
25
|
+
const HiddenContext = createContext(new Set());
|
|
12
26
|
const Root = forwardRef(function TabsRoot({ value, defaultValue, onValueChange, className, size = 'md', animated = true, orientation = 'horizontal', ...props }, ref) {
|
|
13
27
|
const [internal, setInternal] = useState(defaultValue);
|
|
14
28
|
const current = value === undefined ? internal : value;
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
const pendingRequest = useRef(null);
|
|
30
|
+
const change = (next) => {
|
|
31
|
+
// Radix pointer activation can request once from focus and once from pointer down.
|
|
32
|
+
// Deduplicate only that synchronous interaction so a rejected controlled value can retry.
|
|
33
|
+
if (pendingRequest.current === next)
|
|
34
|
+
return;
|
|
35
|
+
pendingRequest.current = next;
|
|
36
|
+
queueMicrotask(() => {
|
|
37
|
+
if (pendingRequest.current === next)
|
|
38
|
+
pendingRequest.current = null;
|
|
39
|
+
});
|
|
40
|
+
if (value === undefined)
|
|
41
|
+
setInternal(next);
|
|
42
|
+
onValueChange?.(next);
|
|
43
|
+
};
|
|
44
|
+
return _jsx(ValueContext.Provider, { value: current, children: _jsx(ChangeContext.Provider, { value: change, children: _jsx(AnimatedContext.Provider, { value: animated, children: _jsx(OrientationContext.Provider, { value: orientation, children: _jsx(Primitive.Root, { ...props, orientation: orientation, ref: ref, "data-size": size, className: ['inkra-tabs', className].filter(Boolean).join(' '), value: current ?? '', onValueChange: change }) }) }) }) });
|
|
26
45
|
});
|
|
27
|
-
const List = forwardRef(function TabsList({ className, align = 'start', fullWidth = false, divided = false, ...props }, ref) {
|
|
28
|
-
|
|
46
|
+
const List = forwardRef(function TabsList({ className, align = 'start', fullWidth = false, divided = false, overflow = 'scroll', moreLabel = 'More tabs', scrollPreviousLabel = 'Scroll to previous tabs', scrollNextLabel = 'Scroll to next tabs', onFocusCapture, ...props }, ref) {
|
|
47
|
+
const current = useContext(ValueContext);
|
|
48
|
+
const change = useContext(ChangeContext);
|
|
49
|
+
const mode = useContext(OrientationContext) === 'horizontal' ? overflow : 'scroll';
|
|
50
|
+
const state = useTabsOverflow(mode, current);
|
|
51
|
+
const { listRef } = state;
|
|
52
|
+
const hidden = useMemo(() => new Set(mode === 'menu' ? state.items.map(item => item.value) : []), [mode, state.items]);
|
|
53
|
+
const selectionPending = useRef(false);
|
|
54
|
+
const setRef = useCallback((node) => {
|
|
55
|
+
listRef.current = node;
|
|
56
|
+
if (typeof ref === 'function')
|
|
57
|
+
ref(node);
|
|
58
|
+
else if (ref)
|
|
59
|
+
ref.current = node;
|
|
60
|
+
}, [listRef, ref]);
|
|
61
|
+
const list = _jsx(Primitive.List, { ...props, ref: setRef, "data-overflow": mode, "data-align": align, "data-full-width": fullWidth || undefined, "data-divided": divided || undefined, className: ['inkra-tabs__list', className].filter(Boolean).join(' '), onFocusCapture: event => {
|
|
62
|
+
onFocusCapture?.(event);
|
|
63
|
+
if (!event.defaultPrevented && mode !== 'menu')
|
|
64
|
+
revealTab(listRef.current, event.target.closest('[data-tabs-value]'));
|
|
65
|
+
} });
|
|
66
|
+
const directionIcon = (previous) => _jsx(OverflowIcon, { left: previous !== state.rtl });
|
|
67
|
+
return _jsx(DividedContext.Provider, { value: divided, children: _jsx(HiddenContext.Provider, { value: hidden, children: mode === 'scroll' ? list : _jsxs("div", { className: "inkra-tabs__overflow", "data-overflow": mode, "data-has-overflow": state.overflowing || undefined, ref: state.containerRef, children: [mode === 'buttons' && state.overflowing && _jsx(Button, { size: "icon", variant: "ghost", className: "inkra-tabs__scroll-button", "aria-label": scrollPreviousLabel, title: scrollPreviousLabel, disabled: !state.previous, onClick: () => state.scroll(-1), children: directionIcon(true) }), list, mode === 'buttons' && state.overflowing && _jsx(Button, { size: "icon", variant: "ghost", className: "inkra-tabs__scroll-button", "aria-label": scrollNextLabel, title: scrollNextLabel, disabled: !state.next, onClick: () => state.scroll(1), children: directionIcon(false) }), mode === 'menu' && state.overflowing && _jsxs(DropdownMenu.Root, { dir: state.rtl ? 'rtl' : 'ltr', children: [_jsx(DropdownMenu.Trigger, { asChild: true, children: _jsx(Button, { ref: state.moreRef, size: "icon", variant: "ghost", className: "inkra-tabs__more", "aria-label": moreLabel, title: moreLabel, children: _jsx(OverflowIcon, { down: true }) }) }), _jsx(DropdownMenu.Portal, { children: _jsx(DropdownMenu.Content, { align: "end", "aria-label": moreLabel, onCloseAutoFocus: event => {
|
|
68
|
+
if (!selectionPending.current)
|
|
69
|
+
return;
|
|
70
|
+
selectionPending.current = false;
|
|
71
|
+
const selected = Array.from(listRef.current?.querySelectorAll('[data-tabs-value]') ?? [])
|
|
72
|
+
.find(node => node.dataset.tabsValue === current && !node.hasAttribute('data-overflow-hidden') && !node.disabled);
|
|
73
|
+
if (selected) {
|
|
74
|
+
event.preventDefault();
|
|
75
|
+
selected.focus({ preventScroll: true });
|
|
76
|
+
}
|
|
77
|
+
}, children: _jsx(DropdownMenu.RadioGroup, { value: current ?? '', onValueChange: next => { selectionPending.current = true; change(next); }, children: state.items.map(item => _jsx(DropdownMenu.RadioItem, { value: item.value, disabled: item.disabled, children: item.label }, item.value)) }) }) })] })] }) }) });
|
|
29
78
|
});
|
|
30
|
-
const Trigger = forwardRef(function TabsTrigger({ value, className, children, startIcon, endIcon, ...props }, ref) {
|
|
79
|
+
const Trigger = forwardRef(function TabsTrigger({ value, className, children, startIcon, endIcon, overflowLabel, ...props }, ref) {
|
|
31
80
|
const current = useContext(ValueContext);
|
|
32
81
|
const divided = useContext(DividedContext);
|
|
33
82
|
const orientation = useContext(OrientationContext);
|
|
83
|
+
const hidden = useContext(HiddenContext).has(value);
|
|
34
84
|
const surface = useRef(null);
|
|
35
85
|
useSelectionDrawing(surface, current === value, useContext(AnimatedContext));
|
|
36
|
-
return _jsxs(Primitive.Trigger, { ...props, value: value, ref: ref, className: ['inkra-tabs__trigger', className].filter(Boolean).join(' '), children: [divided && _jsx(PencilLine, { className: "inkra-tabs__divider", orientation: orientation === 'horizontal' ? 'vertical' : 'horizontal', seed: `tab-divider-${value}` }), _jsx(PencilSurface, { as: "span", "aria-hidden": "true", className: "inkra-tabs__selection", variant: "selection", seed: `tab-${value}`, ref: surface }), _jsx(PencilSurface, { as: "span", "aria-hidden": "true", className: "inkra-tabs__focus-ring", variant: "outline", seed: `tab-${value}-focus` }), startIcon && _jsx("span", { className: "inkra-tabs__icon", "aria-hidden": "true", children: startIcon }), _jsx("span", { className: "inkra-tabs__label", children: typeof children === 'string' ? _jsx(PencilText, { children: children }) : children }), endIcon && _jsx("span", { className: "inkra-tabs__icon", "aria-hidden": "true", children: endIcon })] });
|
|
86
|
+
return _jsxs(Primitive.Trigger, { ...props, value: value, ref: ref, "data-tabs-value": value, "data-overflow-label": overflowLabel, "data-overflow-hidden": hidden || undefined, "aria-hidden": hidden || props['aria-hidden'], inert: hidden || undefined, className: ['inkra-tabs__trigger', className].filter(Boolean).join(' '), children: [divided && _jsx(PencilLine, { className: "inkra-tabs__divider", orientation: orientation === 'horizontal' ? 'vertical' : 'horizontal', seed: `tab-divider-${value}` }), _jsx(PencilSurface, { as: "span", "aria-hidden": "true", className: "inkra-tabs__selection", variant: "selection", seed: `tab-${value}`, ref: surface }), _jsx(PencilSurface, { as: "span", "aria-hidden": "true", className: "inkra-tabs__focus-ring", variant: "outline", seed: `tab-${value}-focus` }), startIcon && _jsx("span", { className: "inkra-tabs__icon", "aria-hidden": "true", children: startIcon }), _jsx("span", { className: "inkra-tabs__label", children: typeof children === 'string' ? _jsx(PencilText, { children: children }) : children }), endIcon && _jsx("span", { className: "inkra-tabs__icon", "aria-hidden": "true", children: endIcon })] });
|
|
37
87
|
});
|
|
38
88
|
const Content = forwardRef(function TabsContent({ className, value, ...props }, ref) {
|
|
39
89
|
const current = useContext(ValueContext);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type TabsOverflow = 'scroll' | 'menu' | 'buttons';
|
|
2
|
+
interface OverflowItem {
|
|
3
|
+
value: string;
|
|
4
|
+
label: string;
|
|
5
|
+
disabled: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function revealTab(list: HTMLDivElement | null, tab: HTMLElement | null): void;
|
|
8
|
+
export declare function useTabsOverflow(mode: TabsOverflow, current: string | undefined): {
|
|
9
|
+
listRef: import("react").RefObject<HTMLDivElement | null>;
|
|
10
|
+
containerRef: import("react").RefObject<HTMLDivElement | null>;
|
|
11
|
+
moreRef: import("react").RefObject<HTMLButtonElement | null>;
|
|
12
|
+
scroll: (direction: -1 | 1) => void;
|
|
13
|
+
measure: () => void;
|
|
14
|
+
overflowing: boolean;
|
|
15
|
+
previous: boolean;
|
|
16
|
+
next: boolean;
|
|
17
|
+
rtl: boolean;
|
|
18
|
+
items: OverflowItem[];
|
|
19
|
+
};
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { useBrowserLayoutEffect } from '../internal/useBrowserLayoutEffect.js';
|
|
3
|
+
import { calculateOverflowListLayout } from '../overflow-list/layout.js';
|
|
4
|
+
const initialState = { overflowing: false, previous: false, next: false, rtl: false, items: [] };
|
|
5
|
+
function triggers(list) {
|
|
6
|
+
return Array.from(list.querySelectorAll('[data-tabs-value]'))
|
|
7
|
+
.filter(node => node.closest('[role="tablist"]') === list);
|
|
8
|
+
}
|
|
9
|
+
export function revealTab(list, tab) {
|
|
10
|
+
if (!list || !tab || tab.hasAttribute('data-overflow-hidden'))
|
|
11
|
+
return;
|
|
12
|
+
const view = list.getBoundingClientRect();
|
|
13
|
+
const item = tab.getBoundingClientRect();
|
|
14
|
+
const delta = item.left < view.left ? item.left - view.left : item.right > view.right ? item.right - view.right : 0;
|
|
15
|
+
if (delta)
|
|
16
|
+
list.scrollBy({ left: delta, behavior: 'instant' });
|
|
17
|
+
}
|
|
18
|
+
export function useTabsOverflow(mode, current) {
|
|
19
|
+
const listRef = useRef(null);
|
|
20
|
+
const containerRef = useRef(null);
|
|
21
|
+
const moreRef = useRef(null);
|
|
22
|
+
const focusAfterLayout = useRef(null);
|
|
23
|
+
const [state, setState] = useState(initialState);
|
|
24
|
+
const measure = useCallback(() => {
|
|
25
|
+
const list = listRef.current;
|
|
26
|
+
const container = containerRef.current;
|
|
27
|
+
if (mode === 'scroll' || !list || !container || container.clientWidth <= 0)
|
|
28
|
+
return;
|
|
29
|
+
const nodes = triggers(list);
|
|
30
|
+
const rtl = getComputedStyle(list).direction === 'rtl';
|
|
31
|
+
let next;
|
|
32
|
+
if (mode === 'menu') {
|
|
33
|
+
const gap = parseFloat(getComputedStyle(list).columnGap) || 0;
|
|
34
|
+
const controlWidth = moreRef.current?.getBoundingClientRect().width || 44;
|
|
35
|
+
// Use the layout box, not scrollWidth: pencil canvases include decorative paint bleed.
|
|
36
|
+
const items = nodes.map(node => {
|
|
37
|
+
const style = getComputedStyle(node);
|
|
38
|
+
const label = node.querySelector('.inkra-tabs__label');
|
|
39
|
+
const labelWidth = label ? parseFloat(getComputedStyle(label).width) || label.offsetWidth : 0;
|
|
40
|
+
const icons = Array.from(node.querySelectorAll(':scope > .inkra-tabs__icon'));
|
|
41
|
+
const width = Math.max(parseFloat(style.minWidth) || 44, labelWidth + icons.reduce((sum, icon) => sum + icon.offsetWidth, 0)
|
|
42
|
+
+ icons.length * (parseFloat(style.columnGap) || 0)
|
|
43
|
+
+ (parseFloat(style.paddingLeft) || 0) + (parseFloat(style.paddingRight) || 0));
|
|
44
|
+
return { key: node.dataset.tabsValue, width, priority: node.dataset.tabsValue === current ? 1 : 0 };
|
|
45
|
+
});
|
|
46
|
+
const layout = calculateOverflowListLayout({ items, containerWidth: container.clientWidth, gap, strategy: 'priority', triggerWidth: controlWidth });
|
|
47
|
+
const hidden = new Set(layout.overflowKeys);
|
|
48
|
+
next = {
|
|
49
|
+
overflowing: hidden.size > 0, previous: false, next: false, rtl,
|
|
50
|
+
items: nodes.filter(node => hidden.has(node.dataset.tabsValue)).map(node => ({
|
|
51
|
+
value: node.dataset.tabsValue,
|
|
52
|
+
label: node.dataset.overflowLabel ?? node.getAttribute('aria-label') ?? node.querySelector('.inkra-tabs__label')?.textContent ?? '',
|
|
53
|
+
disabled: node.disabled,
|
|
54
|
+
})),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
const max = Math.max(0, list.scrollWidth - list.clientWidth);
|
|
59
|
+
const position = Math.min(max, Math.max(0, rtl ? -list.scrollLeft : list.scrollLeft));
|
|
60
|
+
next = { overflowing: list.scrollWidth > container.clientWidth + 1, previous: position > 1, next: position < max - 1, rtl, items: [] };
|
|
61
|
+
}
|
|
62
|
+
const active = list.ownerDocument.activeElement;
|
|
63
|
+
if (mode === 'menu' && active && nodes.some(node => node === active && next.items.some(item => item.value === node.dataset.tabsValue))) {
|
|
64
|
+
focusAfterLayout.current = 'more';
|
|
65
|
+
}
|
|
66
|
+
else if (!next.overflowing && moreRef.current && (active === moreRef.current || moreRef.current.getAttribute('aria-expanded') === 'true')) {
|
|
67
|
+
focusAfterLayout.current = 'selected';
|
|
68
|
+
}
|
|
69
|
+
setState(previous => JSON.stringify(previous) === JSON.stringify(next) ? previous : next);
|
|
70
|
+
}, [mode, current]);
|
|
71
|
+
useBrowserLayoutEffect(measure);
|
|
72
|
+
useBrowserLayoutEffect(() => {
|
|
73
|
+
if (!focusAfterLayout.current || !listRef.current)
|
|
74
|
+
return;
|
|
75
|
+
const nodes = triggers(listRef.current);
|
|
76
|
+
const target = focusAfterLayout.current === 'more' ? moreRef.current
|
|
77
|
+
: nodes.find(node => node.dataset.tabsValue === current && !node.disabled) ?? nodes.find(node => !node.disabled);
|
|
78
|
+
if (target) {
|
|
79
|
+
target.focus({ preventScroll: true });
|
|
80
|
+
focusAfterLayout.current = null;
|
|
81
|
+
}
|
|
82
|
+
}, [state, current]);
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
const list = listRef.current;
|
|
85
|
+
if (!list || mode === 'scroll')
|
|
86
|
+
return;
|
|
87
|
+
let frame = 0;
|
|
88
|
+
const schedule = () => {
|
|
89
|
+
if (!frame)
|
|
90
|
+
frame = requestAnimationFrame(() => { frame = 0; measure(); });
|
|
91
|
+
};
|
|
92
|
+
const resize = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(schedule);
|
|
93
|
+
const observe = () => {
|
|
94
|
+
resize?.disconnect();
|
|
95
|
+
resize?.observe(list);
|
|
96
|
+
if (containerRef.current)
|
|
97
|
+
resize?.observe(containerRef.current);
|
|
98
|
+
for (const node of triggers(list)) {
|
|
99
|
+
resize?.observe(node);
|
|
100
|
+
const label = node.querySelector('.inkra-tabs__label');
|
|
101
|
+
if (label)
|
|
102
|
+
resize?.observe(label);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
observe();
|
|
106
|
+
const mutation = new MutationObserver(records => {
|
|
107
|
+
if (records.some(record => record.type === 'childList'))
|
|
108
|
+
observe();
|
|
109
|
+
schedule();
|
|
110
|
+
});
|
|
111
|
+
mutation.observe(list, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ['disabled', 'data-tabs-value', 'data-overflow-label'] });
|
|
112
|
+
const direction = new MutationObserver(schedule);
|
|
113
|
+
for (let node = list; node; node = node.parentElement) {
|
|
114
|
+
direction.observe(node, { attributes: true, attributeFilter: ['dir', 'style', 'class'] });
|
|
115
|
+
}
|
|
116
|
+
list.addEventListener('scroll', schedule, { passive: true });
|
|
117
|
+
if (!resize)
|
|
118
|
+
window.addEventListener('resize', schedule);
|
|
119
|
+
document.fonts?.addEventListener('loadingdone', schedule);
|
|
120
|
+
return () => {
|
|
121
|
+
cancelAnimationFrame(frame);
|
|
122
|
+
resize?.disconnect();
|
|
123
|
+
mutation.disconnect();
|
|
124
|
+
direction.disconnect();
|
|
125
|
+
list.removeEventListener('scroll', schedule);
|
|
126
|
+
window.removeEventListener('resize', schedule);
|
|
127
|
+
document.fonts?.removeEventListener('loadingdone', schedule);
|
|
128
|
+
};
|
|
129
|
+
}, [measure, mode]);
|
|
130
|
+
useBrowserLayoutEffect(() => {
|
|
131
|
+
const list = listRef.current;
|
|
132
|
+
if (list && mode !== 'menu')
|
|
133
|
+
revealTab(list, triggers(list).find(node => node.dataset.tabsValue === current) ?? null);
|
|
134
|
+
}, [current, mode]);
|
|
135
|
+
const scroll = (direction) => {
|
|
136
|
+
const list = listRef.current;
|
|
137
|
+
if (!list)
|
|
138
|
+
return;
|
|
139
|
+
list.scrollBy({
|
|
140
|
+
left: direction * (state.rtl ? -1 : 1) * Math.max(44, list.clientWidth * 0.8),
|
|
141
|
+
behavior: matchMedia('(prefers-reduced-motion: reduce)').matches ? 'instant' : 'smooth',
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
return { ...state, listRef, containerRef, moreRef, scroll, measure };
|
|
145
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { HTMLAttributes, InputHTMLAttributes } from 'react';
|
|
2
2
|
import type { FieldControlSize, FieldControlStateProps } from '../form/types.js';
|
|
3
|
-
import { createDefaultTag, defaultTagInputDelimiters, defaultTagValueKey,
|
|
3
|
+
import { createDefaultTag, defaultTagInputDelimiters, defaultTagValueKey, normalizeTagInputValue, splitTagInputValue } from './model.js';
|
|
4
4
|
import type { TagInputIdentity } from './model.js';
|
|
5
5
|
export interface TagInputTag extends TagInputIdentity {
|
|
6
6
|
label?: string;
|
|
@@ -51,6 +51,10 @@ export interface TagInputProps extends Omit<HTMLAttributes<HTMLDivElement>, 'chi
|
|
|
51
51
|
getTagValueKey?: (value: string) => string;
|
|
52
52
|
delimiters?: readonly string[];
|
|
53
53
|
maxTags?: number;
|
|
54
|
+
/** Opt-in initial display bound. Every value remains submitted and can be expanded. */
|
|
55
|
+
maxVisibleTags?: number;
|
|
56
|
+
showAllTagsLabel?: (count: number) => string;
|
|
57
|
+
showFewerTagsLabel?: string;
|
|
54
58
|
disabled?: boolean;
|
|
55
59
|
readOnly?: boolean;
|
|
56
60
|
required?: boolean;
|
|
@@ -65,5 +69,6 @@ export interface TagInputProps extends Omit<HTMLAttributes<HTMLDivElement>, 'chi
|
|
|
65
69
|
classNames?: TagInputClassNames;
|
|
66
70
|
}
|
|
67
71
|
export declare const TagInput: import("react").ForwardRefExoticComponent<TagInputProps & import("react").RefAttributes<HTMLInputElement>>;
|
|
68
|
-
export { createDefaultTag, defaultTagInputDelimiters, defaultTagValueKey,
|
|
72
|
+
export { createDefaultTag, defaultTagInputDelimiters, defaultTagValueKey, normalizeTagInputValue, splitTagInputValue, };
|
|
73
|
+
export { hasTagValue } from './model.js';
|
|
69
74
|
export type { FieldControlSize, TagInputIdentity };
|
package/dist/tag-input/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { forwardRef, useEffect, useId, useImperativeHandle, useMemo, useRef, useState, } from 'react';
|
|
2
|
+
import { forwardRef, memo, useCallback, useEffect, useId, useImperativeHandle, useMemo, useRef, useState, } from 'react';
|
|
3
3
|
import { PencilIcon } from '../PencilIcon.js';
|
|
4
|
+
import { Button } from '../Button.js';
|
|
4
5
|
import { PencilLine } from '../PencilLine.js';
|
|
5
6
|
import { PencilSurface } from '../PencilSurface.js';
|
|
6
7
|
import { PencilText } from '../PencilText.js';
|
|
7
8
|
import { useResolvedControlProps } from '../form/controlProps.js';
|
|
8
|
-
import { createDefaultTag, defaultTagInputDelimiters, defaultTagValueKey,
|
|
9
|
+
import { createDefaultTag, defaultTagInputDelimiters, defaultTagValueKey, normalizeTagInputValue, splitTagInputValue, } from './model.js';
|
|
9
10
|
const removePaths = [{ d: 'M18 6 6 18' }, { d: 'm6 6 12 12' }];
|
|
10
11
|
const clearPaths = [{ d: 'M4 7h16' }, { d: 'M9 7V4h6v3' }, { d: 'm7 7 1 13h8l1-13' }];
|
|
11
12
|
const classes = (...values) => values.filter(Boolean).join(' ');
|
|
@@ -38,7 +39,28 @@ function validationText(result, fallback) {
|
|
|
38
39
|
return fallback;
|
|
39
40
|
return undefined;
|
|
40
41
|
}
|
|
41
|
-
|
|
42
|
+
// Rows contain only their own display/ref inputs. Events bubble to the list,
|
|
43
|
+
// whose current handlers own selection, callbacks, guards and tag positions.
|
|
44
|
+
const TagInputTagRow = memo(function TagInputTagRow({ tag, index, disabled, readOnly, className, seed, removeLabel, removeRefs, }) {
|
|
45
|
+
const removeText = readOnly ? undefined : removeLabel(tag);
|
|
46
|
+
return _jsxs(PencilSurface, { as: "li", className: classes('inkra-tag-input__tag', className), "data-tag-id": tag.id, radius: 6, seed: `${seed}-tag-${tag.id}`, variant: "quiet", children: [_jsx("span", { className: "inkra-tag-input__tag-label", children: tag.label ?? tag.value }), !readOnly ? _jsx("button", { "aria-label": removeText, className: "inkra-tag-input__remove", "data-inkra-tag-index": index, disabled: disabled, ref: node => {
|
|
47
|
+
if (node)
|
|
48
|
+
removeRefs.current.set(tag.id, node);
|
|
49
|
+
else
|
|
50
|
+
removeRefs.current.delete(tag.id);
|
|
51
|
+
}, title: removeText, type: "button", children: _jsx(PencilIcon, { decorative: true, paths: removePaths, seed: `${seed}-remove-${tag.id}`, size: 18, sourceSize: 24 }) }) : null] });
|
|
52
|
+
});
|
|
53
|
+
function tagRemoveTarget(event, length) {
|
|
54
|
+
const target = event.target;
|
|
55
|
+
const button = typeof target.closest === 'function'
|
|
56
|
+
? target.closest('button.inkra-tag-input__remove[data-inkra-tag-index]')
|
|
57
|
+
: null;
|
|
58
|
+
if (!button || !event.currentTarget.contains(button))
|
|
59
|
+
return undefined;
|
|
60
|
+
const index = Number(button.getAttribute('data-inkra-tag-index'));
|
|
61
|
+
return Number.isInteger(index) && index >= 0 && index < length ? { button, index } : undefined;
|
|
62
|
+
}
|
|
63
|
+
export const TagInput = forwardRef(function TagInput({ 'aria-describedby': ariaDescribedBy, 'aria-errormessage': ariaErrorMessage, 'aria-invalid': ariaInvalid, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, className, classNames = {}, clearable = true, createTag, defaultInputValue = '', defaultValue = [], delimiters = defaultTagInputDelimiters, disabled, form, fullWidth = true, getTagValueKey = defaultTagValueKey, id, inputProps = {}, inputValue, invalid = false, labels, maxTags, maxVisibleTags, showAllTagsLabel = count => `Show all ${count} tags`, showFewerTagsLabel = 'Show fewer tags', name, onClick, onInputValueChange, onValidationError, onValueChange, placeholder, readOnly, required, seed, size = 'md', validationMessage, validateTag, value, variant = 'outline', ...rootProps }, forwardedRef) {
|
|
42
64
|
const generatedId = useId();
|
|
43
65
|
const inputRef = useRef(null);
|
|
44
66
|
const removeRefs = useRef(new Map());
|
|
@@ -50,6 +72,8 @@ export const TagInput = forwardRef(function TagInput({ 'aria-describedby': ariaD
|
|
|
50
72
|
const [localValidationMessage, setLocalValidationMessage] = useState();
|
|
51
73
|
const [focusWithin, setFocusWithin] = useState(false);
|
|
52
74
|
const [pendingFocus, setPendingFocus] = useState(null);
|
|
75
|
+
const [expandedTags, setExpandedTags] = useState(false);
|
|
76
|
+
const tagDisplayLimit = maxVisibleTags !== undefined && Number.isFinite(maxVisibleTags) ? Math.max(0, Math.floor(maxVisibleTags)) : Infinity;
|
|
53
77
|
const tagsControlled = value !== undefined;
|
|
54
78
|
const draftControlled = inputValue !== undefined;
|
|
55
79
|
const currentTags = tagsControlled ? value : uncontrolledTags;
|
|
@@ -86,12 +110,16 @@ export const TagInput = forwardRef(function TagInput({ 'aria-describedby': ariaD
|
|
|
86
110
|
useEffect(() => {
|
|
87
111
|
if (!pendingFocus)
|
|
88
112
|
return;
|
|
113
|
+
if ('tagId' in pendingFocus && !expandedTags && currentTags.findIndex(tag => tag.id === pendingFocus.tagId) >= tagDisplayLimit) {
|
|
114
|
+
setExpandedTags(true);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
89
117
|
const target = 'input' in pendingFocus
|
|
90
118
|
? inputRef.current
|
|
91
119
|
: removeRefs.current.get(pendingFocus.tagId) ?? inputRef.current;
|
|
92
120
|
target?.focus({ preventScroll: true });
|
|
93
121
|
setPendingFocus(null);
|
|
94
|
-
}, [currentTags, pendingFocus]);
|
|
122
|
+
}, [currentTags, pendingFocus, expandedTags, tagDisplayLimit]);
|
|
95
123
|
useEffect(() => {
|
|
96
124
|
const input = inputRef.current;
|
|
97
125
|
const ownerForm = input?.form;
|
|
@@ -121,11 +149,11 @@ export const TagInput = forwardRef(function TagInput({ 'aria-describedby': ariaD
|
|
|
121
149
|
setUncontrolledDraft(next);
|
|
122
150
|
onInputValueChange?.(next, details);
|
|
123
151
|
};
|
|
124
|
-
const commitTags = (next, details) => {
|
|
152
|
+
const commitTags = useCallback((next, details) => {
|
|
125
153
|
if (!tagsControlled)
|
|
126
154
|
setUncontrolledTags([...next]);
|
|
127
155
|
onValueChange?.(next, details);
|
|
128
|
-
};
|
|
156
|
+
}, [tagsControlled, onValueChange]);
|
|
129
157
|
const reportValidation = (candidate, reason, code, message, tags) => {
|
|
130
158
|
setLocalValidationMessage(message);
|
|
131
159
|
onValidationError?.(message, { candidate, code, reason, tags });
|
|
@@ -134,6 +162,8 @@ export const TagInput = forwardRef(function TagInput({ 'aria-describedby': ariaD
|
|
|
134
162
|
if (!canEdit)
|
|
135
163
|
return false;
|
|
136
164
|
const working = [...currentTags];
|
|
165
|
+
const valueKeys = new Set(currentTags.map(tag => getTagValueKey(tag.value)));
|
|
166
|
+
const ids = new Set(currentTags.map(tag => tag.id));
|
|
137
167
|
const added = [];
|
|
138
168
|
let rejected;
|
|
139
169
|
for (const rawCandidate of candidates) {
|
|
@@ -144,7 +174,7 @@ export const TagInput = forwardRef(function TagInput({ 'aria-describedby': ariaD
|
|
|
144
174
|
rejected = { candidate, code: 'max-tags', message: copy.maxTags(limit) };
|
|
145
175
|
break;
|
|
146
176
|
}
|
|
147
|
-
if (
|
|
177
|
+
if (valueKeys.has(getTagValueKey(candidate))) {
|
|
148
178
|
rejected = { candidate, code: 'duplicate', message: copy.duplicate(candidate) };
|
|
149
179
|
break;
|
|
150
180
|
}
|
|
@@ -159,11 +189,13 @@ export const TagInput = forwardRef(function TagInput({ 'aria-describedby': ariaD
|
|
|
159
189
|
rejected = { candidate, code: 'invalid', message: copy.invalid(candidate) };
|
|
160
190
|
break;
|
|
161
191
|
}
|
|
162
|
-
if (
|
|
192
|
+
if (ids.has(tag.id) || valueKeys.has(getTagValueKey(tag.value))) {
|
|
163
193
|
rejected = { candidate, code: 'duplicate', message: copy.duplicate(candidate) };
|
|
164
194
|
break;
|
|
165
195
|
}
|
|
166
196
|
working.push(tag);
|
|
197
|
+
ids.add(tag.id);
|
|
198
|
+
valueKeys.add(getTagValueKey(tag.value));
|
|
167
199
|
added.push(tag);
|
|
168
200
|
}
|
|
169
201
|
if (added.length > 0)
|
|
@@ -183,7 +215,7 @@ export const TagInput = forwardRef(function TagInput({ 'aria-describedby': ariaD
|
|
|
183
215
|
const parsed = splitTagInputValue(currentDraft, delimiters);
|
|
184
216
|
return commitCandidates(parsed.parts, reason);
|
|
185
217
|
};
|
|
186
|
-
const removeTag = (index, focus = null) => {
|
|
218
|
+
const removeTag = useCallback((index, focus = null) => {
|
|
187
219
|
if (!canEdit)
|
|
188
220
|
return;
|
|
189
221
|
const tag = currentTags[index];
|
|
@@ -198,7 +230,7 @@ export const TagInput = forwardRef(function TagInput({ 'aria-describedby': ariaD
|
|
|
198
230
|
setLocalValidationMessage(undefined);
|
|
199
231
|
commitTags(next, { reason: 'remove', tags: [tag] });
|
|
200
232
|
setPendingFocus(nextFocus);
|
|
201
|
-
};
|
|
233
|
+
}, [canEdit, currentTags, commitTags]);
|
|
202
234
|
const clearAll = () => {
|
|
203
235
|
if (!canEdit || currentTags.length === 0)
|
|
204
236
|
return;
|
|
@@ -208,11 +240,15 @@ export const TagInput = forwardRef(function TagInput({ 'aria-describedby': ariaD
|
|
|
208
240
|
updateDraft('', { reason: 'clear' });
|
|
209
241
|
setPendingFocus({ input: true });
|
|
210
242
|
};
|
|
211
|
-
const focusRemoveButton = (index) => {
|
|
243
|
+
const focusRemoveButton = useCallback((index) => {
|
|
212
244
|
const tag = currentTags[index];
|
|
213
|
-
if (tag)
|
|
214
|
-
|
|
215
|
-
|
|
245
|
+
if (tag) {
|
|
246
|
+
if (!expandedTags && index >= tagDisplayLimit)
|
|
247
|
+
setPendingFocus({ tagId: tag.id });
|
|
248
|
+
else
|
|
249
|
+
removeRefs.current.get(tag.id)?.focus({ preventScroll: true });
|
|
250
|
+
}
|
|
251
|
+
}, [currentTags, expandedTags, tagDisplayLimit]);
|
|
216
252
|
const handleInputChange = (event) => {
|
|
217
253
|
inputProps.onChange?.(event);
|
|
218
254
|
if (event.defaultPrevented)
|
|
@@ -265,10 +301,10 @@ export const TagInput = forwardRef(function TagInput({ 'aria-describedby': ariaD
|
|
|
265
301
|
commitDraft('delimiter');
|
|
266
302
|
}
|
|
267
303
|
};
|
|
268
|
-
const handleTagKeyDown = (event, index) => {
|
|
304
|
+
const handleTagKeyDown = useCallback((event, index, button) => {
|
|
269
305
|
if (event.defaultPrevented || !canEdit)
|
|
270
306
|
return;
|
|
271
|
-
const direction = getComputedStyle(
|
|
307
|
+
const direction = getComputedStyle(button).direction;
|
|
272
308
|
const previousKey = direction === 'rtl' ? 'ArrowRight' : 'ArrowLeft';
|
|
273
309
|
const nextKey = direction === 'rtl' ? 'ArrowLeft' : 'ArrowRight';
|
|
274
310
|
if (event.key === previousKey) {
|
|
@@ -294,8 +330,12 @@ export const TagInput = forwardRef(function TagInput({ 'aria-describedby': ariaD
|
|
|
294
330
|
event.preventDefault();
|
|
295
331
|
removeTag(index);
|
|
296
332
|
}
|
|
297
|
-
};
|
|
298
|
-
|
|
333
|
+
}, [canEdit, currentTags.length, focusRemoveButton, removeTag]);
|
|
334
|
+
// Stable rows survive immutable edits to other tags. Parent event handlers
|
|
335
|
+
// remain fresh instead of hiding a changing callback behind a memo comparator.
|
|
336
|
+
const tagRows = useMemo(() => (expandedTags || tagDisplayLimit >= currentTags.length ? currentTags : currentTags.slice(0, tagDisplayLimit)).map((tag, index) => _jsx(TagInputTagRow, { className: classNames.tag, disabled: state.disabled, index: index, readOnly: state.readOnly, removeLabel: copy.removeTag, removeRefs: removeRefs, seed: resolvedSeed, tag: tag }, `${tag.id}-${index}`)), [currentTags, state.disabled, state.readOnly, copy.removeTag, classNames.tag, resolvedSeed, expandedTags, tagDisplayLimit]);
|
|
337
|
+
const formValues = useMemo(() => name ? currentTags.map((tag, index) => (_jsx("input", { disabled: state.disabled, form: form, name: name, type: "hidden", value: tag.value }, `${tag.id}-${index}`))) : null, [currentTags, state.disabled, form, name]);
|
|
338
|
+
return (_jsxs("div", { ...rootProps, className: classes('inkra-field-control-hit', 'inkra-tag-input', className, classNames.root), "data-disabled": state.disabled || undefined, "data-draft-empty": currentDraft.length === 0 || undefined, "data-focus-within": focusWithin || undefined, "data-full-width": fullWidth, "data-has-placeholder": Boolean(placeholder) || undefined, "data-has-tags": currentTags.length > 0 || undefined, "data-invalid": state.invalid || undefined, "data-max-reached": atLimit || undefined, "data-readonly": state.readOnly || undefined, "data-size": size, "data-variant": variant, onBlurCapture: event => {
|
|
299
339
|
if (!event.currentTarget.contains(event.relatedTarget))
|
|
300
340
|
setFocusWithin(false);
|
|
301
341
|
rootProps.onBlurCapture?.(event);
|
|
@@ -309,17 +349,21 @@ export const TagInput = forwardRef(function TagInput({ 'aria-describedby': ariaD
|
|
|
309
349
|
}, onFocusCapture: event => {
|
|
310
350
|
setFocusWithin(true);
|
|
311
351
|
rootProps.onFocusCapture?.(event);
|
|
312
|
-
}, children: [_jsxs(PencilSurface, { as: "div", className: classes('inkra-field-control', 'inkra-tag-input__control', classNames.control), radius: 8, seed: resolvedSeed, variant: surfaceVariant, children: [
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
352
|
+
}, children: [_jsxs(PencilSurface, { as: "div", className: classes('inkra-field-control', 'inkra-tag-input__control', classNames.control), radius: 8, seed: resolvedSeed, variant: surfaceVariant, children: [_jsxs("ul", { "aria-label": copy.tagList, className: classes('inkra-tag-input__list', classNames.list), onClick: event => {
|
|
353
|
+
const target = tagRemoveTarget(event, currentTags.length);
|
|
354
|
+
if (target)
|
|
355
|
+
removeTag(target.index);
|
|
356
|
+
}, onKeyDown: event => {
|
|
357
|
+
const target = tagRemoveTarget(event, currentTags.length);
|
|
358
|
+
if (target)
|
|
359
|
+
handleTagKeyDown(event, target.index, target.button);
|
|
360
|
+
}, children: [tagRows, currentTags.length > tagDisplayLimit && _jsx("li", { children: _jsx(Button, { type: "button", size: "sm", variant: "ghost", "aria-expanded": expandedTags, onClick: () => setExpandedTags(current => !current), children: expandedTags ? showFewerTagsLabel : showAllTagsLabel(currentTags.length) }) })] }), _jsx("input", { ...inputProps, "aria-describedby": state.describedBy, "aria-errormessage": state.errorMessage, "aria-invalid": state.ariaInvalid, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-readonly": state.readOnly || undefined, "aria-required": state.required || undefined, autoComplete: inputProps.autoComplete ?? 'off', className: classes('inkra-input', 'inkra-tag-input__input', inputProps.className, classNames.input), disabled: state.disabled, form: form, id: state.id, onChange: handleInputChange, onCompositionEnd: event => {
|
|
318
361
|
composingRef.current = false;
|
|
319
362
|
inputProps.onCompositionEnd?.(event);
|
|
320
363
|
}, onCompositionStart: event => {
|
|
321
364
|
composingRef.current = true;
|
|
322
365
|
inputProps.onCompositionStart?.(event);
|
|
323
|
-
}, onKeyDown: handleInputKeyDown, onPaste: handleInputPaste, placeholder: placeholder, readOnly: state.readOnly, ref: inputRef, required: state.required && currentTags.length === 0, spellCheck: inputProps.spellCheck ?? false, type: "text", value: currentDraft }), clearable && currentTags.length > 0 && !state.readOnly ? (_jsx("button", { "aria-label": copy.clearAll, className: classes('inkra-tag-input__clear', classNames.clear), disabled: state.disabled, onClick: clearAll, onPointerDown: (event) => event.preventDefault(), title: copy.clearAll, type: "button", children: _jsx(PencilIcon, { decorative: true, paths: clearPaths, seed: `${resolvedSeed}-clear`, size: 20, sourceSize: 24 }) })) : null, variant === 'underline' ? (_jsx(PencilLine, { className: "inkra-tag-input__underline", seed: `${resolvedSeed}-underline`, weight: (focusWithin ? 1.5 : 1) + (state.invalid ? 0.1 : 0) })) : null] }),
|
|
366
|
+
}, onKeyDown: handleInputKeyDown, onPaste: handleInputPaste, placeholder: placeholder, readOnly: state.readOnly, ref: inputRef, required: state.required && currentTags.length === 0, spellCheck: inputProps.spellCheck ?? false, type: "text", value: currentDraft }), clearable && currentTags.length > 0 && !state.readOnly ? (_jsx("button", { "aria-label": copy.clearAll, className: classes('inkra-tag-input__clear', classNames.clear), disabled: state.disabled, onClick: clearAll, onPointerDown: (event) => event.preventDefault(), title: copy.clearAll, type: "button", children: _jsx(PencilIcon, { decorative: true, paths: clearPaths, seed: `${resolvedSeed}-clear`, size: 20, sourceSize: 24 }) })) : null, variant === 'underline' ? (_jsx(PencilLine, { className: "inkra-tag-input__underline", seed: `${resolvedSeed}-underline`, weight: (focusWithin ? 1.5 : 1) + (state.invalid ? 0.1 : 0) })) : null] }), formValues, activeValidationMessage ? (_jsx(PencilText, { as: "span", className: classes('inkra-tag-input__message', classNames.message), id: messageId, role: "alert", seed: `${resolvedSeed}-message`, children: activeValidationMessage })) : null] }));
|
|
324
367
|
});
|
|
325
|
-
export { createDefaultTag, defaultTagInputDelimiters, defaultTagValueKey,
|
|
368
|
+
export { createDefaultTag, defaultTagInputDelimiters, defaultTagValueKey, normalizeTagInputValue, splitTagInputValue, };
|
|
369
|
+
export { hasTagValue } from './model.js';
|