@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/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,92 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useInkraLocale } from '../i18n/index.js';
|
|
2
3
|
import * as Primitive from '@radix-ui/react-tabs';
|
|
3
|
-
import { createContext, forwardRef, useContext, useRef, useState } from 'react';
|
|
4
|
+
import { createContext, forwardRef, useCallback, useContext, useMemo, useRef, useState } from 'react';
|
|
5
|
+
import { __iconData as chevronLeft } from 'lucide-react/dist/esm/icons/chevron-left.mjs';
|
|
6
|
+
import { __iconData as chevronRight } from 'lucide-react/dist/esm/icons/chevron-right.mjs';
|
|
7
|
+
import { Button } from '../Button.js';
|
|
8
|
+
import { DropdownMenu } from '../dropdown-menu/index.js';
|
|
9
|
+
import { PencilIcon } from '../PencilIcon.js';
|
|
4
10
|
import { PencilSurface } from '../PencilSurface.js';
|
|
5
11
|
import { PencilText } from '../PencilText.js';
|
|
6
12
|
import { PencilLine } from '../PencilLine.js';
|
|
7
13
|
import { useSelectionDrawing } from '../internal/useSelectionDrawing.js';
|
|
14
|
+
import { revealTab, useTabsOverflow } from './useOverflow.js';
|
|
15
|
+
const leftPaths = chevronLeft.node.flatMap(([, attributes]) => attributes.d ? [{ d: attributes.d }] : []);
|
|
16
|
+
const rightPaths = chevronRight.node.flatMap(([, attributes]) => attributes.d ? [{ d: attributes.d }] : []);
|
|
17
|
+
function OverflowIcon({ left = false, down = false }) {
|
|
18
|
+
const paths = left ? leftPaths : rightPaths;
|
|
19
|
+
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)) }) })] });
|
|
20
|
+
}
|
|
8
21
|
const ValueContext = createContext(undefined);
|
|
9
22
|
const AnimatedContext = createContext(true);
|
|
10
23
|
const OrientationContext = createContext('horizontal');
|
|
11
24
|
const DividedContext = createContext(false);
|
|
25
|
+
const ChangeContext = createContext(() => { });
|
|
26
|
+
const HiddenContext = createContext(new Set());
|
|
12
27
|
const Root = forwardRef(function TabsRoot({ value, defaultValue, onValueChange, className, size = 'md', animated = true, orientation = 'horizontal', ...props }, ref) {
|
|
13
28
|
const [internal, setInternal] = useState(defaultValue);
|
|
14
29
|
const current = value === undefined ? internal : value;
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
30
|
+
const pendingRequest = useRef(null);
|
|
31
|
+
const change = (next) => {
|
|
32
|
+
// Radix pointer activation can request once from focus and once from pointer down.
|
|
33
|
+
// Deduplicate only that synchronous interaction so a rejected controlled value can retry.
|
|
34
|
+
if (pendingRequest.current === next)
|
|
35
|
+
return;
|
|
36
|
+
pendingRequest.current = next;
|
|
37
|
+
queueMicrotask(() => {
|
|
38
|
+
if (pendingRequest.current === next)
|
|
39
|
+
pendingRequest.current = null;
|
|
40
|
+
});
|
|
41
|
+
if (value === undefined)
|
|
42
|
+
setInternal(next);
|
|
43
|
+
onValueChange?.(next);
|
|
44
|
+
};
|
|
45
|
+
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
46
|
});
|
|
27
|
-
const List = forwardRef(function TabsList(
|
|
28
|
-
|
|
47
|
+
const List = forwardRef(function TabsList(inkraProps, ref) {
|
|
48
|
+
const { t } = useInkraLocale();
|
|
49
|
+
const { className, align = 'start', fullWidth = false, divided = false, overflow = 'scroll', moreLabel = t('tabs.more'), scrollPreviousLabel = t('tabs.previous'), scrollNextLabel = t('tabs.next'), onFocusCapture, ...props } = inkraProps;
|
|
50
|
+
const current = useContext(ValueContext);
|
|
51
|
+
const change = useContext(ChangeContext);
|
|
52
|
+
const mode = useContext(OrientationContext) === 'horizontal' ? overflow : 'scroll';
|
|
53
|
+
const state = useTabsOverflow(mode, current);
|
|
54
|
+
const { listRef } = state;
|
|
55
|
+
const hidden = useMemo(() => new Set(mode === 'menu' ? state.items.map(item => item.value) : []), [mode, state.items]);
|
|
56
|
+
const selectionPending = useRef(false);
|
|
57
|
+
const setRef = useCallback((node) => {
|
|
58
|
+
listRef.current = node;
|
|
59
|
+
if (typeof ref === 'function')
|
|
60
|
+
ref(node);
|
|
61
|
+
else if (ref)
|
|
62
|
+
ref.current = node;
|
|
63
|
+
}, [listRef, ref]);
|
|
64
|
+
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 => {
|
|
65
|
+
onFocusCapture?.(event);
|
|
66
|
+
if (!event.defaultPrevented && mode !== 'menu')
|
|
67
|
+
revealTab(listRef.current, event.target.closest('[data-tabs-value]'));
|
|
68
|
+
} });
|
|
69
|
+
const directionIcon = (previous) => _jsx(OverflowIcon, { left: previous !== state.rtl });
|
|
70
|
+
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 => {
|
|
71
|
+
if (!selectionPending.current)
|
|
72
|
+
return;
|
|
73
|
+
selectionPending.current = false;
|
|
74
|
+
const selected = Array.from(listRef.current?.querySelectorAll('[data-tabs-value]') ?? [])
|
|
75
|
+
.find(node => node.dataset.tabsValue === current && !node.hasAttribute('data-overflow-hidden') && !node.disabled);
|
|
76
|
+
if (selected) {
|
|
77
|
+
event.preventDefault();
|
|
78
|
+
selected.focus({ preventScroll: true });
|
|
79
|
+
}
|
|
80
|
+
}, 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
81
|
});
|
|
30
|
-
const Trigger = forwardRef(function TabsTrigger({ value, className, children, startIcon, endIcon, ...props }, ref) {
|
|
82
|
+
const Trigger = forwardRef(function TabsTrigger({ value, className, children, startIcon, endIcon, overflowLabel, ...props }, ref) {
|
|
31
83
|
const current = useContext(ValueContext);
|
|
32
84
|
const divided = useContext(DividedContext);
|
|
33
85
|
const orientation = useContext(OrientationContext);
|
|
86
|
+
const hidden = useContext(HiddenContext).has(value);
|
|
34
87
|
const surface = useRef(null);
|
|
35
88
|
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 })] });
|
|
89
|
+
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
90
|
});
|
|
38
91
|
const Content = forwardRef(function TabsContent({ className, value, ...props }, ref) {
|
|
39
92
|
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,34 +1,28 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { useInkraLocale } from '../i18n/index.js';
|
|
3
|
+
import { forwardRef, memo, useCallback, useEffect, useId, useImperativeHandle, useMemo, useRef, useState, } from 'react';
|
|
3
4
|
import { PencilIcon } from '../PencilIcon.js';
|
|
5
|
+
import { Button } from '../Button.js';
|
|
4
6
|
import { PencilLine } from '../PencilLine.js';
|
|
5
7
|
import { PencilSurface } from '../PencilSurface.js';
|
|
6
8
|
import { PencilText } from '../PencilText.js';
|
|
7
9
|
import { useResolvedControlProps } from '../form/controlProps.js';
|
|
8
|
-
import { createDefaultTag, defaultTagInputDelimiters, defaultTagValueKey,
|
|
10
|
+
import { createDefaultTag, defaultTagInputDelimiters, defaultTagValueKey, normalizeTagInputValue, splitTagInputValue, } from './model.js';
|
|
9
11
|
const removePaths = [{ d: 'M18 6 6 18' }, { d: 'm6 6 12 12' }];
|
|
10
12
|
const clearPaths = [{ d: 'M4 7h16' }, { d: 'M9 7V4h6v3' }, { d: 'm7 7 1 13h8l1-13' }];
|
|
11
13
|
const classes = (...values) => values.filter(Boolean).join(' ');
|
|
12
|
-
const defaultLabels = {
|
|
13
|
-
tagList: 'Selected tags',
|
|
14
|
-
removeTag: tag => `Remove ${tag.label ?? tag.value}`,
|
|
15
|
-
clearAll: 'Clear all tags',
|
|
16
|
-
duplicate: candidate => `${candidate} is already added.`,
|
|
17
|
-
maxTags: maximum => `You can add up to ${maximum} tags.`,
|
|
18
|
-
invalid: candidate => `${candidate} is not a valid tag.`,
|
|
19
|
-
};
|
|
20
14
|
function joinAriaIds(...values) {
|
|
21
15
|
const ids = values.flatMap(value => value?.split(/\s+/).filter(Boolean) ?? []);
|
|
22
16
|
return [...new Set(ids)].join(' ') || undefined;
|
|
23
17
|
}
|
|
24
|
-
function resolvedLabels(labels) {
|
|
18
|
+
function resolvedLabels(labels, t) {
|
|
25
19
|
return {
|
|
26
|
-
tagList: labels?.tagList ??
|
|
27
|
-
removeTag: labels?.removeTag ??
|
|
28
|
-
clearAll: labels?.clearAll ??
|
|
29
|
-
duplicate: labels?.duplicate ??
|
|
30
|
-
maxTags: labels?.maxTags ??
|
|
31
|
-
invalid: labels?.invalid ??
|
|
20
|
+
tagList: labels?.tagList ?? t('tagInput.list'),
|
|
21
|
+
removeTag: labels?.removeTag ?? (tag => t('tagInput.remove', { label: tag.label ?? tag.value })),
|
|
22
|
+
clearAll: labels?.clearAll ?? t('tagInput.clear'),
|
|
23
|
+
duplicate: labels?.duplicate ?? (label => t('tagInput.duplicate', { label })),
|
|
24
|
+
maxTags: labels?.maxTags ?? (count => t('tagInput.maximum', { count })),
|
|
25
|
+
invalid: labels?.invalid ?? (label => t('tagInput.invalid', { label })),
|
|
32
26
|
};
|
|
33
27
|
}
|
|
34
28
|
function validationText(result, fallback) {
|
|
@@ -38,7 +32,30 @@ function validationText(result, fallback) {
|
|
|
38
32
|
return fallback;
|
|
39
33
|
return undefined;
|
|
40
34
|
}
|
|
41
|
-
|
|
35
|
+
// Rows contain only their own display/ref inputs. Events bubble to the list,
|
|
36
|
+
// whose current handlers own selection, callbacks, guards and tag positions.
|
|
37
|
+
const TagInputTagRow = memo(function TagInputTagRow({ tag, index, disabled, readOnly, className, seed, removeLabel, removeRefs, }) {
|
|
38
|
+
const removeText = readOnly ? undefined : removeLabel(tag);
|
|
39
|
+
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 => {
|
|
40
|
+
if (node)
|
|
41
|
+
removeRefs.current.set(tag.id, node);
|
|
42
|
+
else
|
|
43
|
+
removeRefs.current.delete(tag.id);
|
|
44
|
+
}, title: removeText, type: "button", children: _jsx(PencilIcon, { decorative: true, paths: removePaths, seed: `${seed}-remove-${tag.id}`, size: 18, sourceSize: 24 }) }) : null] });
|
|
45
|
+
});
|
|
46
|
+
function tagRemoveTarget(event, length) {
|
|
47
|
+
const target = event.target;
|
|
48
|
+
const button = typeof target.closest === 'function'
|
|
49
|
+
? target.closest('button.inkra-tag-input__remove[data-inkra-tag-index]')
|
|
50
|
+
: null;
|
|
51
|
+
if (!button || !event.currentTarget.contains(button))
|
|
52
|
+
return undefined;
|
|
53
|
+
const index = Number(button.getAttribute('data-inkra-tag-index'));
|
|
54
|
+
return Number.isInteger(index) && index >= 0 && index < length ? { button, index } : undefined;
|
|
55
|
+
}
|
|
56
|
+
export const TagInput = forwardRef(function TagInput(inkraProps, forwardedRef) {
|
|
57
|
+
const { t } = useInkraLocale();
|
|
58
|
+
const { '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 => t('tagInput.all', { count }), showFewerTagsLabel = t('tagInput.fewer'), name, onClick, onInputValueChange, onValidationError, onValueChange, placeholder, readOnly, required, seed, size = 'md', validationMessage, validateTag, value, variant = 'outline', ...rootProps } = inkraProps;
|
|
42
59
|
const generatedId = useId();
|
|
43
60
|
const inputRef = useRef(null);
|
|
44
61
|
const removeRefs = useRef(new Map());
|
|
@@ -50,11 +67,13 @@ export const TagInput = forwardRef(function TagInput({ 'aria-describedby': ariaD
|
|
|
50
67
|
const [localValidationMessage, setLocalValidationMessage] = useState();
|
|
51
68
|
const [focusWithin, setFocusWithin] = useState(false);
|
|
52
69
|
const [pendingFocus, setPendingFocus] = useState(null);
|
|
70
|
+
const [expandedTags, setExpandedTags] = useState(false);
|
|
71
|
+
const tagDisplayLimit = maxVisibleTags !== undefined && Number.isFinite(maxVisibleTags) ? Math.max(0, Math.floor(maxVisibleTags)) : Infinity;
|
|
53
72
|
const tagsControlled = value !== undefined;
|
|
54
73
|
const draftControlled = inputValue !== undefined;
|
|
55
74
|
const currentTags = tagsControlled ? value : uncontrolledTags;
|
|
56
75
|
const currentDraft = draftControlled ? inputValue : uncontrolledDraft;
|
|
57
|
-
const copy = useMemo(() => resolvedLabels(labels), [labels]);
|
|
76
|
+
const copy = useMemo(() => resolvedLabels(labels, t), [labels, t]);
|
|
58
77
|
const activeValidationMessage = validationMessage ?? localValidationMessage;
|
|
59
78
|
const messageId = activeValidationMessage ? `inkra-tag-input-${generatedId}-message` : undefined;
|
|
60
79
|
const state = useResolvedControlProps({
|
|
@@ -86,12 +105,16 @@ export const TagInput = forwardRef(function TagInput({ 'aria-describedby': ariaD
|
|
|
86
105
|
useEffect(() => {
|
|
87
106
|
if (!pendingFocus)
|
|
88
107
|
return;
|
|
108
|
+
if ('tagId' in pendingFocus && !expandedTags && currentTags.findIndex(tag => tag.id === pendingFocus.tagId) >= tagDisplayLimit) {
|
|
109
|
+
setExpandedTags(true);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
89
112
|
const target = 'input' in pendingFocus
|
|
90
113
|
? inputRef.current
|
|
91
114
|
: removeRefs.current.get(pendingFocus.tagId) ?? inputRef.current;
|
|
92
115
|
target?.focus({ preventScroll: true });
|
|
93
116
|
setPendingFocus(null);
|
|
94
|
-
}, [currentTags, pendingFocus]);
|
|
117
|
+
}, [currentTags, pendingFocus, expandedTags, tagDisplayLimit]);
|
|
95
118
|
useEffect(() => {
|
|
96
119
|
const input = inputRef.current;
|
|
97
120
|
const ownerForm = input?.form;
|
|
@@ -121,11 +144,11 @@ export const TagInput = forwardRef(function TagInput({ 'aria-describedby': ariaD
|
|
|
121
144
|
setUncontrolledDraft(next);
|
|
122
145
|
onInputValueChange?.(next, details);
|
|
123
146
|
};
|
|
124
|
-
const commitTags = (next, details) => {
|
|
147
|
+
const commitTags = useCallback((next, details) => {
|
|
125
148
|
if (!tagsControlled)
|
|
126
149
|
setUncontrolledTags([...next]);
|
|
127
150
|
onValueChange?.(next, details);
|
|
128
|
-
};
|
|
151
|
+
}, [tagsControlled, onValueChange]);
|
|
129
152
|
const reportValidation = (candidate, reason, code, message, tags) => {
|
|
130
153
|
setLocalValidationMessage(message);
|
|
131
154
|
onValidationError?.(message, { candidate, code, reason, tags });
|
|
@@ -134,6 +157,8 @@ export const TagInput = forwardRef(function TagInput({ 'aria-describedby': ariaD
|
|
|
134
157
|
if (!canEdit)
|
|
135
158
|
return false;
|
|
136
159
|
const working = [...currentTags];
|
|
160
|
+
const valueKeys = new Set(currentTags.map(tag => getTagValueKey(tag.value)));
|
|
161
|
+
const ids = new Set(currentTags.map(tag => tag.id));
|
|
137
162
|
const added = [];
|
|
138
163
|
let rejected;
|
|
139
164
|
for (const rawCandidate of candidates) {
|
|
@@ -144,7 +169,7 @@ export const TagInput = forwardRef(function TagInput({ 'aria-describedby': ariaD
|
|
|
144
169
|
rejected = { candidate, code: 'max-tags', message: copy.maxTags(limit) };
|
|
145
170
|
break;
|
|
146
171
|
}
|
|
147
|
-
if (
|
|
172
|
+
if (valueKeys.has(getTagValueKey(candidate))) {
|
|
148
173
|
rejected = { candidate, code: 'duplicate', message: copy.duplicate(candidate) };
|
|
149
174
|
break;
|
|
150
175
|
}
|
|
@@ -159,11 +184,13 @@ export const TagInput = forwardRef(function TagInput({ 'aria-describedby': ariaD
|
|
|
159
184
|
rejected = { candidate, code: 'invalid', message: copy.invalid(candidate) };
|
|
160
185
|
break;
|
|
161
186
|
}
|
|
162
|
-
if (
|
|
187
|
+
if (ids.has(tag.id) || valueKeys.has(getTagValueKey(tag.value))) {
|
|
163
188
|
rejected = { candidate, code: 'duplicate', message: copy.duplicate(candidate) };
|
|
164
189
|
break;
|
|
165
190
|
}
|
|
166
191
|
working.push(tag);
|
|
192
|
+
ids.add(tag.id);
|
|
193
|
+
valueKeys.add(getTagValueKey(tag.value));
|
|
167
194
|
added.push(tag);
|
|
168
195
|
}
|
|
169
196
|
if (added.length > 0)
|
|
@@ -183,7 +210,7 @@ export const TagInput = forwardRef(function TagInput({ 'aria-describedby': ariaD
|
|
|
183
210
|
const parsed = splitTagInputValue(currentDraft, delimiters);
|
|
184
211
|
return commitCandidates(parsed.parts, reason);
|
|
185
212
|
};
|
|
186
|
-
const removeTag = (index, focus = null) => {
|
|
213
|
+
const removeTag = useCallback((index, focus = null) => {
|
|
187
214
|
if (!canEdit)
|
|
188
215
|
return;
|
|
189
216
|
const tag = currentTags[index];
|
|
@@ -198,7 +225,7 @@ export const TagInput = forwardRef(function TagInput({ 'aria-describedby': ariaD
|
|
|
198
225
|
setLocalValidationMessage(undefined);
|
|
199
226
|
commitTags(next, { reason: 'remove', tags: [tag] });
|
|
200
227
|
setPendingFocus(nextFocus);
|
|
201
|
-
};
|
|
228
|
+
}, [canEdit, currentTags, commitTags]);
|
|
202
229
|
const clearAll = () => {
|
|
203
230
|
if (!canEdit || currentTags.length === 0)
|
|
204
231
|
return;
|
|
@@ -208,11 +235,15 @@ export const TagInput = forwardRef(function TagInput({ 'aria-describedby': ariaD
|
|
|
208
235
|
updateDraft('', { reason: 'clear' });
|
|
209
236
|
setPendingFocus({ input: true });
|
|
210
237
|
};
|
|
211
|
-
const focusRemoveButton = (index) => {
|
|
238
|
+
const focusRemoveButton = useCallback((index) => {
|
|
212
239
|
const tag = currentTags[index];
|
|
213
|
-
if (tag)
|
|
214
|
-
|
|
215
|
-
|
|
240
|
+
if (tag) {
|
|
241
|
+
if (!expandedTags && index >= tagDisplayLimit)
|
|
242
|
+
setPendingFocus({ tagId: tag.id });
|
|
243
|
+
else
|
|
244
|
+
removeRefs.current.get(tag.id)?.focus({ preventScroll: true });
|
|
245
|
+
}
|
|
246
|
+
}, [currentTags, expandedTags, tagDisplayLimit]);
|
|
216
247
|
const handleInputChange = (event) => {
|
|
217
248
|
inputProps.onChange?.(event);
|
|
218
249
|
if (event.defaultPrevented)
|
|
@@ -265,10 +296,10 @@ export const TagInput = forwardRef(function TagInput({ 'aria-describedby': ariaD
|
|
|
265
296
|
commitDraft('delimiter');
|
|
266
297
|
}
|
|
267
298
|
};
|
|
268
|
-
const handleTagKeyDown = (event, index) => {
|
|
299
|
+
const handleTagKeyDown = useCallback((event, index, button) => {
|
|
269
300
|
if (event.defaultPrevented || !canEdit)
|
|
270
301
|
return;
|
|
271
|
-
const direction = getComputedStyle(
|
|
302
|
+
const direction = getComputedStyle(button).direction;
|
|
272
303
|
const previousKey = direction === 'rtl' ? 'ArrowRight' : 'ArrowLeft';
|
|
273
304
|
const nextKey = direction === 'rtl' ? 'ArrowLeft' : 'ArrowRight';
|
|
274
305
|
if (event.key === previousKey) {
|
|
@@ -294,8 +325,12 @@ export const TagInput = forwardRef(function TagInput({ 'aria-describedby': ariaD
|
|
|
294
325
|
event.preventDefault();
|
|
295
326
|
removeTag(index);
|
|
296
327
|
}
|
|
297
|
-
};
|
|
298
|
-
|
|
328
|
+
}, [canEdit, currentTags.length, focusRemoveButton, removeTag]);
|
|
329
|
+
// Stable rows survive immutable edits to other tags. Parent event handlers
|
|
330
|
+
// remain fresh instead of hiding a changing callback behind a memo comparator.
|
|
331
|
+
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]);
|
|
332
|
+
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]);
|
|
333
|
+
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
334
|
if (!event.currentTarget.contains(event.relatedTarget))
|
|
300
335
|
setFocusWithin(false);
|
|
301
336
|
rootProps.onBlurCapture?.(event);
|
|
@@ -309,17 +344,21 @@ export const TagInput = forwardRef(function TagInput({ 'aria-describedby': ariaD
|
|
|
309
344
|
}, onFocusCapture: event => {
|
|
310
345
|
setFocusWithin(true);
|
|
311
346
|
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
|
-
|
|
347
|
+
}, 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 => {
|
|
348
|
+
const target = tagRemoveTarget(event, currentTags.length);
|
|
349
|
+
if (target)
|
|
350
|
+
removeTag(target.index);
|
|
351
|
+
}, onKeyDown: event => {
|
|
352
|
+
const target = tagRemoveTarget(event, currentTags.length);
|
|
353
|
+
if (target)
|
|
354
|
+
handleTagKeyDown(event, target.index, target.button);
|
|
355
|
+
}, 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
356
|
composingRef.current = false;
|
|
319
357
|
inputProps.onCompositionEnd?.(event);
|
|
320
358
|
}, onCompositionStart: event => {
|
|
321
359
|
composingRef.current = true;
|
|
322
360
|
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] }),
|
|
361
|
+
}, 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
362
|
});
|
|
325
|
-
export { createDefaultTag, defaultTagInputDelimiters, defaultTagValueKey,
|
|
363
|
+
export { createDefaultTag, defaultTagInputDelimiters, defaultTagValueKey, normalizeTagInputValue, splitTagInputValue, };
|
|
364
|
+
export { hasTagValue } from './model.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'react';
|
|
2
|
+
export type InkraTheme = 'light' | 'dark' | 'auto';
|
|
3
|
+
export type InkraThemeTokens = Partial<Record<`--inkra-${string}`, string | number>>;
|
|
4
|
+
export interface ThemeProviderProps extends HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
/** Omit to inherit the surrounding theme. auto follows the system through CSS. */
|
|
6
|
+
theme?: InkraTheme;
|
|
7
|
+
/** Scoped CSS variables. Lengths must include units, for example "1.5px". */
|
|
8
|
+
tokens?: InkraThemeTokens;
|
|
9
|
+
}
|
|
10
|
+
/** A CSS scope: never modifies document attributes, storage or sibling themes. */
|
|
11
|
+
export declare const ThemeProvider: import("react").ForwardRefExoticComponent<ThemeProviderProps & import("react").RefAttributes<HTMLDivElement>>;
|