@flood-md/react 0.2.6 → 0.2.9
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 +45 -1
- package/dist/alert-dialog.js +1 -1
- package/dist/calendar-period-grid.d.ts +17 -0
- package/dist/calendar-period-grid.js +13 -0
- package/dist/calendar-value.d.ts +10 -0
- package/dist/calendar-value.js +48 -0
- package/dist/calendar.d.ts +21 -0
- package/dist/calendar.js +259 -0
- package/dist/checkbox.js +1 -1
- package/dist/choice-group.js +1 -1
- package/dist/collection-cards.d.ts +4 -0
- package/dist/collection-cards.js +21 -0
- package/dist/color-controls.d.ts +5 -0
- package/dist/color-controls.js +110 -0
- package/dist/combobox.js +22 -13
- package/dist/command-list.d.ts +3 -0
- package/dist/command-list.js +93 -0
- package/dist/content-elements.js +7 -7
- package/dist/context-menu.d.ts +2 -0
- package/dist/context-menu.js +76 -0
- package/dist/data-display.js +2 -2
- package/dist/data-table.d.ts +2 -0
- package/dist/data-table.js +58 -0
- package/dist/date-field.d.ts +2 -0
- package/dist/date-field.js +172 -0
- package/dist/date-picker.d.ts +3 -0
- package/dist/date-picker.js +108 -0
- package/dist/date-range-field.d.ts +3 -0
- package/dist/date-range-field.js +84 -0
- package/dist/date-range-picker.d.ts +2 -0
- package/dist/date-range-picker.js +81 -0
- package/dist/date-range-value.d.ts +8 -0
- package/dist/date-range-value.js +13 -0
- package/dist/date-time-picker.d.ts +3 -0
- package/dist/date-time-picker.js +31 -0
- package/dist/date-time-range-picker.d.ts +2 -0
- package/dist/date-time-range-picker.js +51 -0
- package/dist/date-value.d.ts +10 -0
- package/dist/date-value.js +31 -0
- package/dist/disclosure.js +1 -1
- package/dist/editable.d.ts +3 -0
- package/dist/editable.js +135 -0
- package/dist/error-summary.d.ts +3 -0
- package/dist/error-summary.js +39 -0
- package/dist/feedback-actions.js +2 -2
- package/dist/feedback.js +2 -2
- package/dist/file-input.js +1 -1
- package/dist/file-records.d.ts +5 -0
- package/dist/file-records.js +55 -0
- package/dist/form-controls.js +25 -13
- package/dist/foundations.css +401 -3
- package/dist/hover-card.d.ts +3 -0
- package/dist/hover-card.js +144 -0
- package/dist/index.d.ts +661 -2
- package/dist/index.js +190 -86
- package/dist/menu.d.ts +27 -1
- package/dist/menu.js +327 -89
- package/dist/navigation-layout.js +6 -6
- package/dist/navigation-tree.d.ts +3 -0
- package/dist/navigation-tree.js +98 -0
- package/dist/navigation.js +3 -3
- package/dist/numeric-input.js +1 -1
- package/dist/overlays.js +11 -10
- package/dist/range-calendar.d.ts +2 -0
- package/dist/range-calendar.js +35 -0
- package/dist/resizable-panels.d.ts +3 -0
- package/dist/resizable-panels.js +205 -0
- package/dist/scroll-area.js +1 -1
- package/dist/select.js +17 -6
- package/dist/slider.js +1 -1
- package/dist/tag-input.d.ts +3 -0
- package/dist/tag-input.js +181 -0
- package/dist/text-inputs.js +2 -2
- package/dist/time-field.d.ts +2 -0
- package/dist/time-field.js +176 -0
- package/dist/time-picker.d.ts +2 -0
- package/dist/time-picker.js +83 -0
- package/dist/time-value.d.ts +12 -0
- package/dist/time-value.js +36 -0
- package/dist/toolbar.js +1 -1
- package/dist/tree-select.d.ts +3 -0
- package/dist/tree-select.js +113 -0
- package/dist/tree.d.ts +3 -0
- package/dist/tree.js +206 -0
- package/package.json +2 -2
package/dist/navigation.js
CHANGED
|
@@ -3,7 +3,7 @@ import { forwardRef, useEffect, useId, useImperativeHandle, useLayoutEffect, use
|
|
|
3
3
|
import { Button, IconSlot, Link, ScrollArea } from "./index.js";
|
|
4
4
|
import { pageWindow } from "./page-window.js";
|
|
5
5
|
const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
6
|
-
export const TabNavigation = forwardRef(function TabNavigation({ label, items, value, defaultValue, onValueChange, activationMode = "automatic", orientation = "horizontal", controlSize = "md", disabled = false, iconOnly = false, className, onKeyDown, id: explicitId, ...props }, ref) {
|
|
6
|
+
export const TabNavigation = /* @__PURE__ */ forwardRef(function TabNavigation({ label, items, value, defaultValue, onValueChange, activationMode = "automatic", orientation = "horizontal", controlSize = "md", disabled = false, iconOnly = false, className, onKeyDown, id: explicitId, ...props }, ref) {
|
|
7
7
|
const generated = useId(), id = explicitId ?? `flood-tabs-${generated}`;
|
|
8
8
|
const [local, setLocal] = useState(defaultValue), [focused, setFocused] = useState();
|
|
9
9
|
const root = useRef(null), list = useRef(null), focusedPanel = useRef(undefined);
|
|
@@ -49,10 +49,10 @@ export const TabNavigation = forwardRef(function TabNavigation({ label, items, v
|
|
|
49
49
|
setFocused(undefined); }, children: items.map((item, index) => _jsxs(Button, { variant: "ghost", size: controlSize, id: `${id}-tab-${index}`, type: "button", role: "tab", "aria-selected": item.value === selected, "aria-controls": `${id}-panel-${index}`, "data-value": item.value, disabled: disabled || item.disabled, tabIndex: item.value === tabStop ? 0 : -1, "aria-label": iconOnly ? item.label : undefined, className: `flood-tab${iconOnly ? " flood-icon-button" : ""}`, onFocus: () => setFocused(item.value), onClick: () => select(item.value), children: [item.icon && _jsx(IconSlot, { size: controlSize, children: item.icon }), !iconOnly && item.label] }, item.value)) }) }), items.map((item, index) => _jsx("div", { id: `${id}-panel-${index}`, role: "tabpanel", "aria-labelledby": `${id}-tab-${index}`, tabIndex: 0, hidden: item.value !== selected, className: "flood-tabs__panel", onFocusCapture: () => { focusedPanel.current = item.value; }, onBlurCapture: event => { if (!event.currentTarget.hidden)
|
|
50
50
|
focusedPanel.current = undefined; }, children: item.content }, item.value))] });
|
|
51
51
|
});
|
|
52
|
-
export const BreadcrumbNavigation = forwardRef(function BreadcrumbNavigation({ items, label = "Breadcrumb", separator = "/", className, ...props }, ref) {
|
|
52
|
+
export const BreadcrumbNavigation = /* @__PURE__ */ forwardRef(function BreadcrumbNavigation({ items, label = "Breadcrumb", separator = "/", className, ...props }, ref) {
|
|
53
53
|
return _jsx("nav", { ...props, ref: ref, "aria-label": label, className: ["flood-breadcrumb", className].filter(Boolean).join(" "), children: _jsx("ol", { children: items.map((item, index) => _jsxs("li", { children: [index > 0 && _jsx("span", { className: "flood-breadcrumb__separator", "aria-hidden": "true", children: separator }), item.href !== undefined ? _jsx(Link, { href: item.href, "aria-current": index === items.length - 1 ? "page" : undefined, children: item.label }) : _jsx("span", { "aria-current": index === items.length - 1 ? "page" : undefined, children: item.label })] }, index)) }) });
|
|
54
54
|
});
|
|
55
|
-
export const PageNavigation = forwardRef(function PageNavigation({ page, pageCount, onPageChange, getHref, siblingCount = 1, controlSize = "sm", disabled = false, label = "Pagination", previousLabel = "Previous page", nextLabel = "Next page", pageLabel = page => `Page ${page}`, className, ...props }, ref) {
|
|
55
|
+
export const PageNavigation = /* @__PURE__ */ forwardRef(function PageNavigation({ page, pageCount, onPageChange, getHref, siblingCount = 1, controlSize = "sm", disabled = false, label = "Pagination", previousLabel = "Previous page", nextLabel = "Next page", pageLabel = page => `Page ${page}`, className, ...props }, ref) {
|
|
56
56
|
const pages = pageWindow(page, pageCount, siblingCount);
|
|
57
57
|
const control = (target, text, name, unavailable = false) => {
|
|
58
58
|
const inactive = disabled || unavailable, current = target === page;
|
package/dist/numeric-input.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react";
|
|
3
3
|
/** The browser owns decimal stepping, validity, editing and form submission. */
|
|
4
|
-
export const NumericStepper = forwardRef(function NumericStepper({ inputComponent: Group, buttonComponent: Action, value, defaultValue = 0, onValueChange, min, max, step = 1, disabled, readOnly, controlSize = "md", decreaseLabel = "Decrease value", increaseLabel = "Increase value", className, onBlur, onKeyDown, ...props }, ref) {
|
|
4
|
+
export const NumericStepper = /* @__PURE__ */ forwardRef(function NumericStepper({ inputComponent: Group, buttonComponent: Action, value, defaultValue = 0, onValueChange, min, max, step = 1, disabled, readOnly, controlSize = "md", decreaseLabel = "Decrease value", increaseLabel = "Increase value", className, onBlur, onKeyDown, ...props }, ref) {
|
|
5
5
|
if (!Number.isFinite(step) || step <= 0 || (min !== undefined && !Number.isFinite(min)) || (max !== undefined && !Number.isFinite(max)) || (min !== undefined && max !== undefined && min > max))
|
|
6
6
|
throw new Error("Flood NumberInput requires finite bounds and a positive step");
|
|
7
7
|
for (const number of [value, defaultValue])
|
package/dist/overlays.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef, useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
|
|
3
|
-
import { IconButton, ScrollArea } from "./index.js";
|
|
3
|
+
import { IconButton, ScrollArea, VisuallyHidden } from "./index.js";
|
|
4
4
|
import { positionLayer, registerLayer, returnFocus, lockScroll, tabbables } from "./overlay-runtime.js";
|
|
5
5
|
import { mergeDescriptions } from "./field-state.js";
|
|
6
6
|
const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
@@ -21,7 +21,7 @@ function focusContent(panel, preferred) {
|
|
|
21
21
|
function OverlayClose({ label, onClick }) {
|
|
22
22
|
return _jsx(IconButton, { label: label, size: "sm", variant: "subtle", className: "flood-overlay-close", onClick: onClick, children: _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", "aria-hidden": "true", focusable: "false", children: _jsx("path", { d: "m4 4 16 16M20 4 4 20" }) }) });
|
|
23
23
|
}
|
|
24
|
-
export const AnchoredOverlay = forwardRef(function AnchoredOverlay(allProps, forwardedRef) {
|
|
24
|
+
export const AnchoredOverlay = /* @__PURE__ */ forwardRef(function AnchoredOverlay(allProps, forwardedRef) {
|
|
25
25
|
const { kind, trigger, side = "bottom", align = "start", disabled = false, id: explicitId, className, style, ...native } = allProps;
|
|
26
26
|
const pop = allProps, tip = allProps;
|
|
27
27
|
const [local, setLocal] = useState(kind === "popover" ? Boolean(pop.defaultOpen) : false);
|
|
@@ -38,7 +38,8 @@ export const AnchoredOverlay = forwardRef(function AnchoredOverlay(allProps, for
|
|
|
38
38
|
suppressed.current = true;
|
|
39
39
|
overPanel.current = false;
|
|
40
40
|
} request(false); };
|
|
41
|
-
const current = useCurrent({ close, request, disabled, open, initialFocusRef: pop.initialFocusRef });
|
|
41
|
+
const current = useCurrent({ close, request, disabled, open, initialFocusRef: pop.initialFocusRef, positionReference: pop.positionReference });
|
|
42
|
+
const positionReference = () => { const node = current.current.positionReference?.current; return node?.isConnected && node.ownerDocument === panel.current?.ownerDocument ? node : anchor.current; };
|
|
42
43
|
useImperativeHandle(forwardedRef, () => panel.current, []);
|
|
43
44
|
useEffect(() => () => clearTimeout(timer.current), []);
|
|
44
45
|
useEffect(() => { if (kind === "tooltip" && disabled) {
|
|
@@ -71,13 +72,13 @@ export const AnchoredOverlay = forwardRef(function AnchoredOverlay(allProps, for
|
|
|
71
72
|
throw new Error("Flood overlays require the native Popover API");
|
|
72
73
|
element.style.visibility = "hidden";
|
|
73
74
|
element.showPopover();
|
|
74
|
-
positionLayer(
|
|
75
|
+
positionLayer(positionReference(), element, side, align);
|
|
75
76
|
unregister = registerLayer(document, { panel: element, anchor: () => anchor.current, close: () => current.current.close(), tooltip: kind === "tooltip" });
|
|
76
77
|
if (kind === "popover")
|
|
77
78
|
focusContent(element, current.current.initialFocusRef?.current);
|
|
78
79
|
const update = () => { if (!alive)
|
|
79
80
|
return; if (anchor.current?.isConnected)
|
|
80
|
-
positionLayer(
|
|
81
|
+
positionLayer(positionReference(), element, side, align); frame = view.requestAnimationFrame(update); };
|
|
81
82
|
frame = view.requestAnimationFrame(update);
|
|
82
83
|
};
|
|
83
84
|
start();
|
|
@@ -87,7 +88,7 @@ export const AnchoredOverlay = forwardRef(function AnchoredOverlay(allProps, for
|
|
|
87
88
|
current.current.request(false);
|
|
88
89
|
queueMicrotask(() => { if (alive && current.current.open && element.isConnected && !element.matches(":popover-open") && anchor.current?.isConnected) {
|
|
89
90
|
element.showPopover();
|
|
90
|
-
positionLayer(
|
|
91
|
+
positionLayer(positionReference(), element, side, align);
|
|
91
92
|
if (kind === "popover")
|
|
92
93
|
focusContent(element, current.current.initialFocusRef?.current);
|
|
93
94
|
} });
|
|
@@ -104,16 +105,16 @@ export const AnchoredOverlay = forwardRef(function AnchoredOverlay(allProps, for
|
|
|
104
105
|
onPointerLeave: () => { hovered.current = false; leave(); }, onFocus: () => { focused.current = true; enter(true); }, onBlur: () => { focused.current = false; leave(); },
|
|
105
106
|
} : { disabled, "aria-haspopup": "dialog", "aria-expanded": open, "aria-controls": id, onClick: () => request(!open) }) };
|
|
106
107
|
// Remove component-only props before forwarding native div attributes.
|
|
107
|
-
const { title: unusedTitle, content: unusedContent, children: unusedChildren, open: unusedOpen, defaultOpen: unusedDefault, onOpenChange: unusedChange, delay: unusedDelay, closeLabel: unusedClose, initialFocusRef: unusedFocus, ...attributes } = native;
|
|
108
|
-
return _jsxs(_Fragment, { children: [trigger(triggerProps), _jsx("div", { ...attributes, ref: panel, id: id, popover: "manual", role: kind === "tooltip" ? "tooltip" : "dialog", "aria-labelledby": kind === "popover" ? `${id}-title` : undefined, tabIndex: kind === "popover" ? -1 : undefined, className: ["flood-overlay-panel", kind === "tooltip" ? "flood-tooltip" : "flood-popover", className].filter(Boolean).join(" "), style: style, onPointerEnter: event => { attributes.onPointerEnter?.(event); if (kind === "tooltip") {
|
|
108
|
+
const { title: unusedTitle, chrome: unusedChrome, content: unusedContent, children: unusedChildren, open: unusedOpen, defaultOpen: unusedDefault, onOpenChange: unusedChange, delay: unusedDelay, closeLabel: unusedClose, initialFocusRef: unusedFocus, positionReference: unusedPosition, ...attributes } = native;
|
|
109
|
+
return _jsxs(_Fragment, { children: [trigger(triggerProps), _jsx("div", { ...attributes, ref: panel, id: id, popover: "manual", role: kind === "tooltip" ? "tooltip" : "dialog", "aria-labelledby": kind === "popover" ? `${id}-title` : undefined, tabIndex: kind === "popover" ? -1 : undefined, "data-chrome": kind === "popover" ? pop.chrome : undefined, className: ["flood-overlay-panel", kind === "tooltip" ? "flood-tooltip" : "flood-popover", className].filter(Boolean).join(" "), style: style, onPointerEnter: event => { attributes.onPointerEnter?.(event); if (kind === "tooltip") {
|
|
109
110
|
overPanel.current = true;
|
|
110
111
|
clear();
|
|
111
112
|
} }, onPointerLeave: event => { attributes.onPointerLeave?.(event); if (kind === "tooltip") {
|
|
112
113
|
overPanel.current = false;
|
|
113
114
|
leave();
|
|
114
|
-
} }, children: kind === "tooltip" ? tip.content : _jsxs(_Fragment, { children: [_jsxs("div", { className: "flood-overlay-header", children: [_jsx("h2", { id: `${id}-title`, className: "flood-overlay-title", children: pop.title }), _jsx(OverlayClose, { label: pop.closeLabel ?? "Close", onClick: close })] }), open && _jsx("div", { className: "flood-overlay-body", children: pop.children })] }) })] });
|
|
115
|
+
} }, children: kind === "tooltip" ? tip.content : _jsxs(_Fragment, { children: [pop.chrome === "minimal" ? _jsx(VisuallyHidden, { children: _jsx("h2", { id: `${id}-title`, children: pop.title }) }) : _jsxs("div", { className: "flood-overlay-header", children: [_jsx("h2", { id: `${id}-title`, className: "flood-overlay-title", children: pop.title }), _jsx(OverlayClose, { label: pop.closeLabel ?? "Close", onClick: close })] }), open && _jsx("div", { className: "flood-overlay-body", children: pop.children })] }) })] });
|
|
115
116
|
});
|
|
116
|
-
export const ModalOverlay = forwardRef(function ModalOverlay({ showCloseButton = true, scrollBody = false, footer, title, description, trigger, children, open: controlled, defaultOpen = false, onOpenChange, disabled = false, closeLabel = "Close", closeOnOutsideClick = true, initialFocusRef, id: explicitId, className, onKeyDown, "aria-describedby": describedBy, ...props }, forwardedRef) {
|
|
117
|
+
export const ModalOverlay = /* @__PURE__ */ forwardRef(function ModalOverlay({ showCloseButton = true, scrollBody = false, footer, title, description, trigger, children, open: controlled, defaultOpen = false, onOpenChange, disabled = false, closeLabel = "Close", closeOnOutsideClick = true, initialFocusRef, id: explicitId, className, onKeyDown, "aria-describedby": describedBy, ...props }, forwardedRef) {
|
|
117
118
|
const [local, setLocal] = useState(defaultOpen), open = controlled ?? local;
|
|
118
119
|
const dialog = useRef(null), anchor = useRef(null), generated = useId(), id = explicitId ?? `flood-dialog-${generated}`;
|
|
119
120
|
const request = (next) => { if (controlled === undefined)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useEffect, useState } from "react";
|
|
3
|
+
import { CalendarGrid } from "./calendar.js";
|
|
4
|
+
import { availableDateRange, checkDateRange, completeDateRange, emptyDateRange, orderedDateRange, sameDateRange } from "./date-range-value.js";
|
|
5
|
+
export const DateRangeCalendar = /* @__PURE__ */ forwardRef(function DateRangeCalendar({ value, defaultValue = emptyDateRange(), onValueChange, onRangeSelect, isRangeDisabled, rangeLabels, min, max, isDateDisabled, className, ...props }, ref) {
|
|
6
|
+
checkDateRange(defaultValue, "defaultValue");
|
|
7
|
+
if (value !== undefined)
|
|
8
|
+
checkDateRange(value);
|
|
9
|
+
const [local, setLocal] = useState(defaultValue), range = value ?? local;
|
|
10
|
+
const [preview, setPreview] = useState();
|
|
11
|
+
const anchor = range.start || range.end, partial = !range.start || !range.end;
|
|
12
|
+
const tentative = anchor && partial && preview ? orderedDateRange(anchor, preview) : undefined;
|
|
13
|
+
const available = (next) => availableDateRange(next, min, max, isDateDisabled, isRangeDisabled);
|
|
14
|
+
const unavailable = !!tentative && !available(tentative);
|
|
15
|
+
const labels = { chooseStart: rangeLabels?.chooseStart ?? "Choose a start date.", chooseEnd: rangeLabels?.chooseEnd ?? "Choose an end date.", unavailable: rangeLabels?.unavailable ?? "This date range is unavailable." };
|
|
16
|
+
if (!Object.values(labels).every(text => text.trim()))
|
|
17
|
+
throw new Error("Flood RangeCalendar labels must be nonempty");
|
|
18
|
+
useEffect(() => setPreview(undefined), [range.start, range.end]);
|
|
19
|
+
const visual = tentative ?? (completeDateRange(range) ? range : anchor ? { start: anchor, end: anchor } : undefined);
|
|
20
|
+
return _jsx(CalendarGrid, { ...props, ref: ref, min: min, max: max, isDateDisabled: isDateDisabled, className: ["flood-range-calendar", className].filter(Boolean).join(" "), selectedDate: range.start || range.end, multiple: true, isSelected: date => completeDateRange(range) ? date >= range.start && date <= range.end : date === anchor, dayMetadata: date => ({ rangeStart: visual?.start === date, rangeEnd: visual?.end === date, inRange: !!visual && date >= visual.start && date <= visual.end, preview: !!tentative && date >= tentative.start && date <= tentative.end, unavailable: unavailable && !!tentative && date >= tentative.start && date <= tentative.end }), onDayPreview: setPreview, onDayActivate: date => {
|
|
21
|
+
const next = anchor && partial ? orderedDateRange(anchor, date) : { start: date, end: "" };
|
|
22
|
+
if (next.end && !available(next)) {
|
|
23
|
+
setPreview(date);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
setPreview(undefined);
|
|
27
|
+
if (!sameDateRange(next, range)) {
|
|
28
|
+
if (value === undefined)
|
|
29
|
+
setLocal(next);
|
|
30
|
+
onValueChange?.(next);
|
|
31
|
+
}
|
|
32
|
+
if (next.end)
|
|
33
|
+
onRangeSelect?.(next);
|
|
34
|
+
}, footer: _jsx("div", { className: "flood-range-calendar__instruction", role: "status", "aria-live": "polite", children: unavailable ? labels.unavailable : anchor && partial ? labels.chooseEnd : labels.chooseStart }) });
|
|
35
|
+
});
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ResizablePanelsProps } from "./index.js";
|
|
2
|
+
/** Two bounded panes with a dedicated separator track; percentages exclude that track. */
|
|
3
|
+
export declare const ResizablePaneGroup: import("react").ForwardRefExoticComponent<ResizablePanelsProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useEffect, useId, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
|
|
3
|
+
const clamp = (value, min, max) => Math.max(min, Math.min(max, Math.round(value * 1e6) / 1e6));
|
|
4
|
+
const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
5
|
+
const resizeKeys = new Set(["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End"]);
|
|
6
|
+
function lockDocument(document, cursor) {
|
|
7
|
+
const style = document.documentElement.style;
|
|
8
|
+
const entries = ["user-select", "-webkit-user-select", "cursor"].map(name => ({ name, value: style.getPropertyValue(name), priority: style.getPropertyPriority(name), owned: name === "cursor" ? cursor : "none" }));
|
|
9
|
+
for (const entry of entries)
|
|
10
|
+
style.setProperty(entry.name, entry.owned, "important");
|
|
11
|
+
return () => { for (const entry of entries) {
|
|
12
|
+
if (style.getPropertyValue(entry.name) !== entry.owned || style.getPropertyPriority(entry.name) !== "important")
|
|
13
|
+
continue;
|
|
14
|
+
if (entry.value)
|
|
15
|
+
style.setProperty(entry.name, entry.value, entry.priority);
|
|
16
|
+
else
|
|
17
|
+
style.removeProperty(entry.name);
|
|
18
|
+
} };
|
|
19
|
+
}
|
|
20
|
+
/** Two bounded panes with a dedicated separator track; percentages exclude that track. */
|
|
21
|
+
export const ResizablePaneGroup = /* @__PURE__ */ forwardRef(function ResizablePaneGroup({ label, main, secondary, orientation = "horizontal", value, defaultValue = 50, onValueChange, onValueCommit, min = 10, max = 90, step = 1, largeStep = 10, disabled = false, id: explicitId, className, style, onPointerDown, onPointerMove, onPointerUp, onPointerCancel, onLostPointerCapture, onKeyDown, onKeyUp, onBlur, ...props }, forwardedRef) {
|
|
22
|
+
if (!label.trim())
|
|
23
|
+
throw new Error("Flood ResizablePanels requires a nonempty separator label");
|
|
24
|
+
if (orientation !== "horizontal" && orientation !== "vertical")
|
|
25
|
+
throw new Error("Flood ResizablePanels orientation must be horizontal or vertical");
|
|
26
|
+
if (![min, max].every(Number.isFinite) || min < 0 || max > 100 || min >= max)
|
|
27
|
+
throw new Error("Flood ResizablePanels bounds must satisfy 0 <= min < max <= 100");
|
|
28
|
+
if (![step, largeStep].every(number => Number.isFinite(number) && number > 0))
|
|
29
|
+
throw new Error("Flood ResizablePanels keyboard steps must be positive finite percentages");
|
|
30
|
+
for (const entry of [value, defaultValue])
|
|
31
|
+
if (entry !== undefined && (!Number.isFinite(entry) || entry < 0 || entry > 100))
|
|
32
|
+
throw new Error("Flood ResizablePanels values must be finite percentages from 0 to 100");
|
|
33
|
+
const generated = useId(), id = explicitId ?? `flood-resizable-${generated}`, mainId = `${id}-main`, secondaryId = `${id}-secondary`;
|
|
34
|
+
const root = useRef(null), handle = useRef(null), first = useRef(null), second = useRef(null);
|
|
35
|
+
useImperativeHandle(forwardedRef, () => root.current, []);
|
|
36
|
+
const [local, setLocal] = useState(defaultValue), [dragging, setDragging] = useState(false), percent = clamp(value ?? local, min, max);
|
|
37
|
+
const drag = useRef(undefined), keyboard = useRef(false), alive = useRef(true), restoreSeparatorFocus = useRef(false);
|
|
38
|
+
const current = useRef({ percent, value, min, max, orientation, disabled, onValueChange, onValueCommit });
|
|
39
|
+
current.current = { percent, value, min, max, orientation, disabled, onValueChange, onValueCommit };
|
|
40
|
+
const bindPane = (node, reference, inactive) => {
|
|
41
|
+
reference.current = node;
|
|
42
|
+
if (!node)
|
|
43
|
+
return;
|
|
44
|
+
if (inactive && node.contains(node.ownerDocument.activeElement))
|
|
45
|
+
restoreSeparatorFocus.current = true;
|
|
46
|
+
// Set the native property so React 18 does not receive an unknown boolean prop.
|
|
47
|
+
node.inert = inactive;
|
|
48
|
+
};
|
|
49
|
+
useBrowserLayoutEffect(() => {
|
|
50
|
+
if (restoreSeparatorFocus.current) {
|
|
51
|
+
restoreSeparatorFocus.current = false;
|
|
52
|
+
handle.current?.focus({ preventScroll: true });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
const publish = (next) => {
|
|
56
|
+
const state = current.current, bounded = clamp(next, state.min, state.max);
|
|
57
|
+
if (state.value === undefined) {
|
|
58
|
+
current.current.percent = bounded;
|
|
59
|
+
setLocal(bounded);
|
|
60
|
+
}
|
|
61
|
+
state.onValueChange?.(bounded);
|
|
62
|
+
};
|
|
63
|
+
const commitAccepted = () => queueMicrotask(() => { if (alive.current)
|
|
64
|
+
current.current.onValueCommit?.(current.current.percent); });
|
|
65
|
+
const finish = (commit, restore = true) => {
|
|
66
|
+
const gesture = drag.current;
|
|
67
|
+
if (!gesture)
|
|
68
|
+
return;
|
|
69
|
+
drag.current = undefined;
|
|
70
|
+
gesture.restore();
|
|
71
|
+
try {
|
|
72
|
+
if (gesture.handle.hasPointerCapture(gesture.pointerId))
|
|
73
|
+
gesture.handle.releasePointerCapture(gesture.pointerId);
|
|
74
|
+
}
|
|
75
|
+
catch { /* Capture may already be released by the browser or node removal. */ }
|
|
76
|
+
if (alive.current)
|
|
77
|
+
setDragging(false);
|
|
78
|
+
if (!alive.current)
|
|
79
|
+
return;
|
|
80
|
+
if (!commit && restore && current.current.value === undefined) {
|
|
81
|
+
const start = clamp(gesture.initial, current.current.min, current.current.max);
|
|
82
|
+
if (start !== current.current.percent)
|
|
83
|
+
publish(start);
|
|
84
|
+
}
|
|
85
|
+
if (commit)
|
|
86
|
+
commitAccepted();
|
|
87
|
+
};
|
|
88
|
+
const move = (event) => {
|
|
89
|
+
const gesture = drag.current;
|
|
90
|
+
if (!gesture || gesture.pointerId !== event.pointerId || current.current.disabled)
|
|
91
|
+
return;
|
|
92
|
+
const a = first.current?.getBoundingClientRect(), b = second.current?.getBoundingClientRect();
|
|
93
|
+
if (!a || !b)
|
|
94
|
+
return;
|
|
95
|
+
const available = gesture.orientation === "horizontal" ? a.width + b.width : a.height + b.height;
|
|
96
|
+
if (!Number.isFinite(available) || available <= 0)
|
|
97
|
+
return;
|
|
98
|
+
const coordinate = gesture.orientation === "horizontal" ? event.clientX : event.clientY;
|
|
99
|
+
if (!Number.isFinite(coordinate))
|
|
100
|
+
return;
|
|
101
|
+
const direction = gesture.orientation === "horizontal" && gesture.rtl ? -1 : 1;
|
|
102
|
+
const next = clamp(gesture.initial + (coordinate - gesture.origin) * direction / available * 100, current.current.min, current.current.max);
|
|
103
|
+
if (next !== gesture.requested) {
|
|
104
|
+
gesture.requested = next;
|
|
105
|
+
publish(next);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
alive.current = true;
|
|
110
|
+
return () => { alive.current = false; keyboard.current = false; finish(false, false); };
|
|
111
|
+
}, []);
|
|
112
|
+
useEffect(() => { if (drag.current)
|
|
113
|
+
finish(false); keyboard.current = false; }, [disabled, orientation, min, max]);
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (!dragging || !root.current)
|
|
116
|
+
return;
|
|
117
|
+
const document = root.current.ownerDocument, view = document.defaultView;
|
|
118
|
+
const cancel = () => finish(false), visibility = () => { if (document.hidden)
|
|
119
|
+
cancel(); };
|
|
120
|
+
view?.addEventListener("blur", cancel);
|
|
121
|
+
document.addEventListener("visibilitychange", visibility);
|
|
122
|
+
return () => { view?.removeEventListener("blur", cancel); document.removeEventListener("visibilitychange", visibility); };
|
|
123
|
+
}, [dragging]);
|
|
124
|
+
return _jsxs("div", { ...props, id: id, ref: root, className: ["flood-resizable-panels", className].filter(Boolean).join(" "), "data-orientation": orientation, "data-disabled": disabled || undefined, "data-dragging": dragging || undefined, style: { ...style, "--flood-main-weight": `${percent}fr`, "--flood-secondary-weight": `${100 - percent}fr` }, onPointerDown: event => {
|
|
125
|
+
onPointerDown?.(event);
|
|
126
|
+
const separator = handle.current, node = root.current;
|
|
127
|
+
if (event.defaultPrevented || current.current.disabled || drag.current || event.button !== 0 || event.isPrimary === false || !separator?.contains(event.target) || !node)
|
|
128
|
+
return;
|
|
129
|
+
const a = first.current?.getBoundingClientRect(), b = second.current?.getBoundingClientRect(), horizontal = orientation === "horizontal";
|
|
130
|
+
if (!a || !b || (horizontal ? a.width + b.width : a.height + b.height) <= 0 || !Number.isFinite(horizontal ? event.clientX : event.clientY))
|
|
131
|
+
return;
|
|
132
|
+
separator.focus({ preventScroll: true });
|
|
133
|
+
if (current.current.disabled || current.current.orientation !== orientation || !separator.isConnected)
|
|
134
|
+
return;
|
|
135
|
+
try {
|
|
136
|
+
separator.setPointerCapture(event.pointerId);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
event.preventDefault();
|
|
142
|
+
keyboard.current = false;
|
|
143
|
+
drag.current = { pointerId: event.pointerId, origin: horizontal ? event.clientX : event.clientY, initial: current.current.percent, requested: current.current.percent, orientation, rtl: node.ownerDocument.defaultView?.getComputedStyle(node).direction === "rtl", handle: separator, restore: lockDocument(node.ownerDocument, horizontal ? "col-resize" : "row-resize") };
|
|
144
|
+
setDragging(true);
|
|
145
|
+
}, onPointerMove: event => { onPointerMove?.(event); if (!event.defaultPrevented)
|
|
146
|
+
move(event); }, onPointerUp: event => {
|
|
147
|
+
let accepted = false;
|
|
148
|
+
try {
|
|
149
|
+
onPointerUp?.(event);
|
|
150
|
+
accepted = !event.defaultPrevented;
|
|
151
|
+
if (accepted)
|
|
152
|
+
move(event);
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
if (drag.current?.pointerId === event.pointerId)
|
|
156
|
+
finish(accepted);
|
|
157
|
+
}
|
|
158
|
+
}, onPointerCancel: event => { try {
|
|
159
|
+
onPointerCancel?.(event);
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
if (drag.current?.pointerId === event.pointerId)
|
|
163
|
+
finish(false);
|
|
164
|
+
} }, onLostPointerCapture: event => { try {
|
|
165
|
+
onLostPointerCapture?.(event);
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
if (drag.current?.pointerId === event.pointerId)
|
|
169
|
+
finish(false);
|
|
170
|
+
} }, onKeyDown: event => {
|
|
171
|
+
onKeyDown?.(event);
|
|
172
|
+
if (event.defaultPrevented || event.target !== handle.current || event.nativeEvent.isComposing || event.keyCode === 229 || event.altKey || event.ctrlKey || event.metaKey)
|
|
173
|
+
return;
|
|
174
|
+
if (event.key === "Escape" && drag.current) {
|
|
175
|
+
event.preventDefault();
|
|
176
|
+
finish(false);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (current.current.disabled || drag.current || !resizeKeys.has(event.key))
|
|
180
|
+
return;
|
|
181
|
+
const horizontal = orientation === "horizontal", rtl = root.current?.ownerDocument.defaultView?.getComputedStyle(root.current).direction === "rtl";
|
|
182
|
+
const direction = horizontal ? event.key === "ArrowRight" ? (rtl ? -1 : 1) : event.key === "ArrowLeft" ? (rtl ? 1 : -1) : 0 : event.key === "ArrowDown" ? 1 : event.key === "ArrowUp" ? -1 : 0;
|
|
183
|
+
if (!direction && event.key !== "Home" && event.key !== "End")
|
|
184
|
+
return;
|
|
185
|
+
event.preventDefault();
|
|
186
|
+
const state = current.current, next = clamp(event.key === "Home" ? min : event.key === "End" ? max : state.percent + direction * (event.shiftKey ? largeStep : step), min, max);
|
|
187
|
+
if (next !== state.percent) {
|
|
188
|
+
keyboard.current = true;
|
|
189
|
+
publish(next);
|
|
190
|
+
}
|
|
191
|
+
}, onKeyUp: event => { onKeyUp?.(event); if (event.target === handle.current && (event.key === "Home" || event.key === "End" || (orientation === "horizontal" ? ["ArrowLeft", "ArrowRight"] : ["ArrowUp", "ArrowDown"]).includes(event.key)) && keyboard.current) {
|
|
192
|
+
keyboard.current = false;
|
|
193
|
+
if (!event.defaultPrevented)
|
|
194
|
+
commitAccepted();
|
|
195
|
+
} }, onBlur: event => { try {
|
|
196
|
+
onBlur?.(event);
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
if (event.target === handle.current && keyboard.current) {
|
|
200
|
+
keyboard.current = false;
|
|
201
|
+
if (!event.defaultPrevented)
|
|
202
|
+
commitAccepted();
|
|
203
|
+
}
|
|
204
|
+
} }, children: [_jsx("div", { ref: node => bindPane(node, first, percent === 0), id: mainId, "aria-hidden": percent === 0 || undefined, className: "flood-resizable-panels__panel", children: main }), _jsx("div", { ref: handle, role: "separator", tabIndex: disabled ? -1 : 0, "aria-disabled": disabled || undefined, "aria-label": label, "aria-orientation": orientation === "horizontal" ? "vertical" : "horizontal", "aria-controls": `${mainId} ${secondaryId}`, "aria-valuemin": min, "aria-valuemax": max, "aria-valuenow": percent, "aria-valuetext": `${Number(percent.toFixed(2))}%`, className: "flood-resizable-panels__separator" }), _jsx("div", { ref: node => bindPane(node, second, percent === 100), id: secondaryId, "aria-hidden": percent === 100 || undefined, className: "flood-resizable-panels__panel", children: secondary })] });
|
|
205
|
+
});
|
package/dist/scroll-area.js
CHANGED
|
@@ -84,7 +84,7 @@ function cornerBounds(node, axis, crossStart, crossEnd, start, end, inset) {
|
|
|
84
84
|
return { start, end: Math.max(start, end) };
|
|
85
85
|
}
|
|
86
86
|
/** Original overlay chrome; the viewport itself remains a browser-native scroll container. */
|
|
87
|
-
export const OverlayScrollArea = forwardRef(function OverlayScrollArea({ orientation = "vertical", contentInset = "none", children, className, style, tabIndex = 0, role, onFocus, ...props }, forwardedRef) {
|
|
87
|
+
export const OverlayScrollArea = /* @__PURE__ */ forwardRef(function OverlayScrollArea({ orientation = "vertical", contentInset = "none", children, className, style, tabIndex = 0, role, onFocus, ...props }, forwardedRef) {
|
|
88
88
|
const id = useId();
|
|
89
89
|
const viewport = useRef(null), content = useRef(null);
|
|
90
90
|
const vertical = useRef(null), horizontal = useRef(null);
|
package/dist/select.js
CHANGED
|
@@ -20,7 +20,7 @@ function initialChoices(children, group, groupDisabled = false, groupHidden = fa
|
|
|
20
20
|
});
|
|
21
21
|
}
|
|
22
22
|
/** Internal implementation. The public entry retains the native SelectProps/ref contract. */
|
|
23
|
-
export const SingleSelect = forwardRef(function SingleSelect({ children, value, defaultValue, onChange, onInvalid, onFocus, onBlur, onKeyDown, onKeyUp, onClick, disabled, required, id, className, style, title, autoFocus, invalid, status, controlSize = "md", "aria-label": ariaLabel, "aria-labelledby": labelledBy, "aria-describedby": describedBy, "aria-invalid": ariaInvalid, ...nativeProps }, forwardedRef) {
|
|
23
|
+
export const SingleSelect = /* @__PURE__ */ forwardRef(function SingleSelect({ children, value, defaultValue, onChange, onInvalid, onFocus, onBlur, onKeyDown, onKeyUp, onClick, disabled, required, id, className, style, title, autoFocus, invalid, status, controlSize = "md", "aria-label": ariaLabel, "aria-labelledby": labelledBy, "aria-describedby": describedBy, "aria-invalid": ariaInvalid, ...nativeProps }, forwardedRef) {
|
|
24
24
|
const generatedId = useId();
|
|
25
25
|
const triggerId = id ?? `flood-select-${generatedId}`;
|
|
26
26
|
const menuId = `${triggerId}-options`, validationId = `${triggerId}-validation`;
|
|
@@ -269,11 +269,22 @@ export const SingleSelect = forwardRef(function SingleSelect({ children, value,
|
|
|
269
269
|
commit(index); }, children: [_jsx("span", { children: choice.label }), _jsx("span", { className: "flood-select__selected", "aria-hidden": "true", children: choice.value === currentValue ? "✓" : "" })] }, `${index}-${choice.value}`);
|
|
270
270
|
return _jsxs("span", { className: `flood-select flood-select--custom flood-control--${controlSize}`, dir: nativeProps.dir, lang: nativeProps.lang, hidden: nativeProps.hidden, children: [_jsx("button", { ...visibleAttributes, ref: trigger, id: triggerId, type: "button", role: "combobox", disabled: disabled, tabIndex: nativeProps.tabIndex, accessKey: nativeProps.accessKey, dir: nativeProps.dir, lang: nativeProps.lang, hidden: nativeProps.hidden, className: ["flood-input flood-select__control", `flood-control--${controlSize}`, className].filter(Boolean).join(" "), style: style, title: title, "aria-label": ariaLabel, "aria-labelledby": labelledBy, "aria-describedby": descriptions, "data-status": resolveFieldStatus(status, invalid || Boolean(validation), ariaInvalid), "aria-invalid": invalid || status === "error" || ariaInvalid || Boolean(validation) || undefined, "aria-required": required || undefined, "aria-expanded": open, "aria-haspopup": "listbox", "aria-controls": menuId, "aria-activedescendant": open && active >= 0 && choices[active] && !choices[active].hidden ? `${menuId}-${active}` : undefined, onFocus: (event) => { if (onFocus)
|
|
271
271
|
mirror(new FocusEvent("focusin", { bubbles: true, relatedTarget: event.relatedTarget })); }, onBlur: (event) => { if (onBlur)
|
|
272
|
-
mirror(new FocusEvent("focusout", { bubbles: true, relatedTarget: event.relatedTarget })); },
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
272
|
+
mirror(new FocusEvent("focusout", { bubbles: true, relatedTarget: event.relatedTarget })); }, onMouseDown: (event) => {
|
|
273
|
+
// Keep an already-focused combobox from blurring to the page before
|
|
274
|
+
// Safari's click. Activation below owns focus; touch scrolling is native.
|
|
275
|
+
if (event.button === 0)
|
|
276
|
+
event.preventDefault();
|
|
277
|
+
}, onClick: (event) => {
|
|
278
|
+
// Safari does not give buttons mouse focus. The combobox must own keys
|
|
279
|
+
// before opening; focus first so a host click callback can still move it.
|
|
280
|
+
event.currentTarget.focus({ preventScroll: true });
|
|
281
|
+
if (onClick && !mirror(new MouseEvent("click", { bubbles: true, cancelable: true })))
|
|
282
|
+
return;
|
|
283
|
+
if (open)
|
|
284
|
+
setOpen(false);
|
|
285
|
+
else
|
|
286
|
+
openMenu();
|
|
287
|
+
}, onKeyUp: (event) => { if (onKeyUp)
|
|
277
288
|
mirror(new KeyboardEvent("keyup", { key: event.key, bubbles: true, cancelable: true })); }, onKeyDown: (event) => {
|
|
278
289
|
if (onKeyDown && !mirror(new KeyboardEvent("keydown", { key: event.key, code: event.code, bubbles: true, cancelable: true, ctrlKey: event.ctrlKey, shiftKey: event.shiftKey, altKey: event.altKey, metaKey: event.metaKey }))) {
|
|
279
290
|
event.preventDefault();
|
package/dist/slider.js
CHANGED
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { forwardRef, useEffect, useId, useRef, useState } from "react";
|
|
3
3
|
import { hasFieldContent, mergeDescriptions, resolveFieldStatus } from "./field-state.js";
|
|
4
4
|
/** Native range inputs keep keyboard/AT interaction; the shared rail handles pointer selection. */
|
|
5
|
-
export const RangeControl = forwardRef(function RangeControl({ fieldsetComponent: Group, label, value, defaultValue = [50], onValueChange, min = 0, max = 100, step = 1, thumbLabels, showValue = true, readOnly = false, controlSize = "md", disabled, className, name, onKeyDown, ...props }, ref) {
|
|
5
|
+
export const RangeControl = /* @__PURE__ */ forwardRef(function RangeControl({ fieldsetComponent: Group, label, value, defaultValue = [50], onValueChange, min = 0, max = 100, step = 1, thumbLabels, showValue = true, readOnly = false, controlSize = "md", disabled, className, name, onKeyDown, ...props }, ref) {
|
|
6
6
|
if (![min, max, step, max - min, (max - min) / step].every(Number.isFinite) || min >= max || step <= 0)
|
|
7
7
|
throw new Error("Flood Slider requires finite increasing bounds and a positive representable step");
|
|
8
8
|
if (!label.trim())
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type TagInputProps } from "./index.js";
|
|
2
|
+
/** Free text tokens. Known-option IDs and suggestion layers belong to MultiSelect. */
|
|
3
|
+
export declare const FreeTagEntry: import("react").ForwardRefExoticComponent<TagInputProps & import("react").RefAttributes<HTMLInputElement>>;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useEffect, useId, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
|
|
3
|
+
import { Input, Label, Tag } from "./index.js";
|
|
4
|
+
import { mergeDescriptions, resolveFieldStatus } from "./field-state.js";
|
|
5
|
+
const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
6
|
+
const normalize = (values) => [...new Set(values.map(value => value.trim()).filter(Boolean))];
|
|
7
|
+
const same = (a, b) => a.length === b.length && a.every((value, index) => value === b[index]);
|
|
8
|
+
/** Free text tokens. Known-option IDs and suggestion layers belong to MultiSelect. */
|
|
9
|
+
export const FreeTagEntry = /* @__PURE__ */ forwardRef(function FreeTagEntry({ label, hideLabel = false, value, defaultValue = [], onValueChange, delimiters = [","], removeLabel = "Remove", requiredMessage = "Add at least one tag.", controlSize = "md", disabled, readOnly, required, name, form, id: explicitId, className, style, onKeyDown, onPaste, onInvalid, onCompositionStart, onCompositionEnd, ...props }, forwardedRef) {
|
|
10
|
+
if (!label.trim() || !removeLabel.trim() || !requiredMessage.trim())
|
|
11
|
+
throw new Error("Flood TagInput requires nonempty accessible labels and validation message");
|
|
12
|
+
if (delimiters.some(delimiter => [...delimiter].length !== 1 || delimiter === "\n" || delimiter === "\r"))
|
|
13
|
+
throw new Error("Flood TagInput delimiters must be single characters other than newlines");
|
|
14
|
+
for (const values of [value, defaultValue])
|
|
15
|
+
if (values !== undefined && (!Array.isArray(values) || values.some(item => typeof item !== "string")))
|
|
16
|
+
throw new Error("Flood TagInput values must be arrays of strings");
|
|
17
|
+
const generated = useId(), id = explicitId ?? `flood-tag-input-${generated}`;
|
|
18
|
+
const input = useRef(null), root = useRef(null), composing = useRef(false);
|
|
19
|
+
useImperativeHandle(forwardedRef, () => input.current, []);
|
|
20
|
+
const initial = useRef(normalize(defaultValue));
|
|
21
|
+
const [local, setLocal] = useState(initial.current), [draft, setDraft] = useState(""), [validation, setValidation] = useState("");
|
|
22
|
+
const tags = value === undefined ? local : normalize(value);
|
|
23
|
+
const controlled = useRef(value !== undefined);
|
|
24
|
+
controlled.current = value !== undefined;
|
|
25
|
+
const pending = useRef(undefined);
|
|
26
|
+
const blocked = () => Boolean(disabled || readOnly || input.current?.matches(":disabled"));
|
|
27
|
+
const publish = (next) => {
|
|
28
|
+
if (same(tags, next))
|
|
29
|
+
return;
|
|
30
|
+
if (value === undefined)
|
|
31
|
+
setLocal(next);
|
|
32
|
+
onValueChange?.([...next]);
|
|
33
|
+
setValidation("");
|
|
34
|
+
};
|
|
35
|
+
const add = (parts, remaining = "", source = draft) => {
|
|
36
|
+
const next = normalize([...tags, ...parts]);
|
|
37
|
+
if (same(tags, next)) {
|
|
38
|
+
pending.current = undefined;
|
|
39
|
+
setDraft(remaining);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (value === undefined)
|
|
43
|
+
setDraft(remaining);
|
|
44
|
+
else {
|
|
45
|
+
pending.current = { tags: next, draft: source, remaining };
|
|
46
|
+
setDraft(source);
|
|
47
|
+
}
|
|
48
|
+
publish(next);
|
|
49
|
+
};
|
|
50
|
+
useBrowserLayoutEffect(() => {
|
|
51
|
+
const request = pending.current;
|
|
52
|
+
if (request && request.tags.every(tag => tags.includes(tag))) {
|
|
53
|
+
pending.current = undefined;
|
|
54
|
+
setDraft(current => current === request.draft ? request.remaining : current);
|
|
55
|
+
}
|
|
56
|
+
const control = input.current;
|
|
57
|
+
control?.setCustomValidity(required && tags.length === 0 ? requiredMessage : "");
|
|
58
|
+
if (validation && control?.validity.valid)
|
|
59
|
+
setValidation("");
|
|
60
|
+
});
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
const owner = input.current?.form;
|
|
63
|
+
if (!owner)
|
|
64
|
+
return;
|
|
65
|
+
const timers = new Set();
|
|
66
|
+
const reset = (event) => {
|
|
67
|
+
// React's delegated onReset may cancel after this native listener returns.
|
|
68
|
+
const timer = setTimeout(() => {
|
|
69
|
+
timers.delete(timer);
|
|
70
|
+
if (event.defaultPrevented || !input.current)
|
|
71
|
+
return;
|
|
72
|
+
if (!controlled.current)
|
|
73
|
+
setLocal([...initial.current]);
|
|
74
|
+
pending.current = undefined;
|
|
75
|
+
setDraft("");
|
|
76
|
+
setValidation("");
|
|
77
|
+
}, 0);
|
|
78
|
+
timers.add(timer);
|
|
79
|
+
};
|
|
80
|
+
owner.addEventListener("reset", reset);
|
|
81
|
+
return () => { owner.removeEventListener("reset", reset); timers.forEach(clearTimeout); };
|
|
82
|
+
}, [form]);
|
|
83
|
+
const remove = (tag) => {
|
|
84
|
+
if (blocked())
|
|
85
|
+
return;
|
|
86
|
+
// Removal is a separate edit, not acceptance of a pending addition.
|
|
87
|
+
pending.current = undefined;
|
|
88
|
+
publish(tags.filter(value => value !== tag));
|
|
89
|
+
input.current?.focus({ preventScroll: true });
|
|
90
|
+
};
|
|
91
|
+
const split = (text, includeNewlines = false) => {
|
|
92
|
+
const separators = new Set([...delimiters, ...(includeNewlines ? ["\n", "\r"] : [])]);
|
|
93
|
+
const parts = [""];
|
|
94
|
+
for (const character of text) {
|
|
95
|
+
if (separators.has(character))
|
|
96
|
+
parts.push("");
|
|
97
|
+
else
|
|
98
|
+
parts[parts.length - 1] += character;
|
|
99
|
+
}
|
|
100
|
+
return parts;
|
|
101
|
+
};
|
|
102
|
+
const removeButtons = () => [...root.current?.querySelectorAll(".flood-tag__remove:not(:disabled)") ?? []];
|
|
103
|
+
const rtl = () => input.current?.ownerDocument.defaultView?.getComputedStyle(input.current).direction === "rtl";
|
|
104
|
+
return _jsxs("div", { ref: root, className: "flood-tag-input", hidden: props.hidden, dir: props.dir, lang: props.lang, children: [_jsx(Label, { htmlFor: id, required: required, className: hideLabel ? "flood-visually-hidden" : undefined, children: label }), _jsxs("div", { className: ["flood-input-group", "flood-tag-input__control", `flood-control--${controlSize}`, className].filter(Boolean).join(" "), style: style, "data-disabled": disabled || undefined, "data-readonly": readOnly || undefined, "data-status": resolveFieldStatus(props.status, props.invalid || Boolean(validation), props["aria-invalid"]), onClick: event => { if (!blocked() && !event.target.closest("button,input"))
|
|
105
|
+
input.current?.focus(); }, children: [tags.map(tag => _jsx(Tag, { size: controlSize === "sm" ? "sm" : controlSize === "md" ? "md" : "lg", disabled: disabled, onRemove: readOnly ? undefined : () => remove(tag), removeLabel: `${removeLabel} ${tag}`, removeButtonProps: { tabIndex: -1, onKeyDown: event => {
|
|
106
|
+
if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey || event.nativeEvent.isComposing || blocked())
|
|
107
|
+
return;
|
|
108
|
+
const buttons = removeButtons(), index = buttons.indexOf(event.currentTarget);
|
|
109
|
+
if (event.key === "Backspace" || event.key === "Delete") {
|
|
110
|
+
event.preventDefault();
|
|
111
|
+
remove(tag);
|
|
112
|
+
}
|
|
113
|
+
else if (event.key === "Escape") {
|
|
114
|
+
event.preventDefault();
|
|
115
|
+
event.stopPropagation();
|
|
116
|
+
input.current?.focus();
|
|
117
|
+
}
|
|
118
|
+
else if (event.key === (rtl() ? "ArrowLeft" : "ArrowRight")) {
|
|
119
|
+
event.preventDefault();
|
|
120
|
+
(buttons[index + 1] ?? input.current)?.focus();
|
|
121
|
+
}
|
|
122
|
+
else if (event.key === (rtl() ? "ArrowRight" : "ArrowLeft")) {
|
|
123
|
+
event.preventDefault();
|
|
124
|
+
buttons[Math.max(0, index - 1)]?.focus();
|
|
125
|
+
}
|
|
126
|
+
} }, children: tag }, tag)), _jsx(Input, { ...props, ref: input, id: id, type: "text", form: form, value: draft, controlSize: controlSize, disabled: disabled, readOnly: readOnly, required: required && tags.length === 0, "aria-required": required || undefined, "aria-describedby": mergeDescriptions(props["aria-describedby"], tags.length > 0 && `${id}-selection`, Boolean(validation) && `${id}-validation`), invalid: props.invalid || Boolean(validation), onChange: event => {
|
|
127
|
+
if (blocked())
|
|
128
|
+
return;
|
|
129
|
+
pending.current = undefined;
|
|
130
|
+
const text = event.currentTarget.value;
|
|
131
|
+
if (composing.current || ("isComposing" in event.nativeEvent && event.nativeEvent.isComposing)) {
|
|
132
|
+
setDraft(text);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const parts = split(text);
|
|
136
|
+
if (parts.length > 1)
|
|
137
|
+
add(parts.slice(0, -1), parts.at(-1), text);
|
|
138
|
+
else
|
|
139
|
+
setDraft(text);
|
|
140
|
+
}, onCompositionStart: event => { composing.current = true; onCompositionStart?.(event); }, onCompositionEnd: event => { composing.current = false; onCompositionEnd?.(event); }, onInvalid: event => { onInvalid?.(event); if (!event.defaultPrevented)
|
|
141
|
+
setValidation(event.currentTarget.validationMessage); }, onKeyDown: event => {
|
|
142
|
+
onKeyDown?.(event);
|
|
143
|
+
if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey || event.nativeEvent.isComposing || composing.current)
|
|
144
|
+
return;
|
|
145
|
+
if (readOnly && event.key === "Backspace") {
|
|
146
|
+
event.preventDefault();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (blocked())
|
|
150
|
+
return;
|
|
151
|
+
if (event.key === "Enter" && draft.trim()) {
|
|
152
|
+
event.preventDefault();
|
|
153
|
+
add([draft]);
|
|
154
|
+
}
|
|
155
|
+
else if (delimiters.includes(event.key)) {
|
|
156
|
+
event.preventDefault();
|
|
157
|
+
add([draft]);
|
|
158
|
+
}
|
|
159
|
+
else if (event.key === "Backspace" && !draft && tags.length && !event.shiftKey) {
|
|
160
|
+
event.preventDefault();
|
|
161
|
+
remove(tags.at(-1));
|
|
162
|
+
}
|
|
163
|
+
else if (event.key === (rtl() ? "ArrowRight" : "ArrowLeft") && !event.shiftKey && event.currentTarget.selectionStart === 0 && event.currentTarget.selectionEnd === 0) {
|
|
164
|
+
const last = removeButtons().at(-1);
|
|
165
|
+
if (last) {
|
|
166
|
+
event.preventDefault();
|
|
167
|
+
last.focus();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}, onPaste: event => {
|
|
171
|
+
onPaste?.(event);
|
|
172
|
+
if (event.defaultPrevented || blocked() || composing.current)
|
|
173
|
+
return;
|
|
174
|
+
const pasted = event.clipboardData.getData("text/plain");
|
|
175
|
+
if (split(pasted, true).length === 1)
|
|
176
|
+
return;
|
|
177
|
+
event.preventDefault();
|
|
178
|
+
const node = event.currentTarget, text = draft.slice(0, node.selectionStart ?? draft.length) + pasted + draft.slice(node.selectionEnd ?? draft.length);
|
|
179
|
+
add(split(text, true), "", text);
|
|
180
|
+
} })] }), name && tags.map(tag => _jsx("input", { type: "hidden", name: name, value: tag, form: form, disabled: disabled }, tag)), tags.length > 0 && _jsx("span", { id: `${id}-selection`, className: "flood-visually-hidden", children: tags.join(", ") }), validation && _jsx("span", { id: `${id}-validation`, role: "alert", className: "flood-error", children: validation })] });
|
|
181
|
+
});
|