@flood-md/react 0.2.4 → 0.2.6
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 +91 -1
- package/dist/alert-dialog.d.ts +2 -0
- package/dist/alert-dialog.js +23 -0
- package/dist/combobox.d.ts +11 -0
- package/dist/combobox.js +301 -0
- package/dist/content-elements.d.ts +8 -0
- package/dist/content-elements.js +47 -0
- package/dist/feedback-actions.d.ts +3 -0
- package/dist/feedback-actions.js +106 -0
- package/dist/file-input.d.ts +8 -0
- package/dist/file-input.js +86 -0
- package/dist/form-controls.d.ts +6 -0
- package/dist/form-controls.js +250 -0
- package/dist/foundations.css +348 -1
- package/dist/index.d.ts +323 -1
- package/dist/index.js +78 -0
- package/dist/navigation-layout.d.ts +7 -0
- package/dist/navigation-layout.js +77 -0
- package/dist/numeric-input.d.ts +8 -0
- package/dist/numeric-input.js +68 -0
- package/dist/overlay-runtime.js +33 -12
- package/dist/overlays.d.ts +7 -1
- package/dist/overlays.js +5 -3
- package/dist/select.js +2 -2
- package/dist/slider.d.ts +7 -0
- package/dist/slider.js +89 -0
- package/dist/text-inputs.d.ts +9 -0
- package/dist/text-inputs.js +45 -0
- package/package.json +2 -2
package/dist/overlay-runtime.js
CHANGED
|
@@ -51,6 +51,9 @@ export function registerLayer(document, layer) {
|
|
|
51
51
|
}
|
|
52
52
|
else if (path.includes(layer.panel) || path.includes(layer.anchor()))
|
|
53
53
|
return;
|
|
54
|
+
// A modal backdrop must not move focus to the document after close cleanup.
|
|
55
|
+
if (layer.modal)
|
|
56
|
+
event.preventDefault();
|
|
54
57
|
(layer.outside ?? layer.close)();
|
|
55
58
|
};
|
|
56
59
|
const focus = (event) => {
|
|
@@ -68,24 +71,42 @@ export function registerLayer(document, layer) {
|
|
|
68
71
|
}
|
|
69
72
|
const locks = new WeakMap();
|
|
70
73
|
export function lockScroll(document) {
|
|
71
|
-
const
|
|
74
|
+
const root = document.documentElement, style = root.style;
|
|
72
75
|
let lock = locks.get(document);
|
|
73
76
|
if (!lock) {
|
|
74
|
-
const
|
|
75
|
-
|
|
77
|
+
const view = document.defaultView;
|
|
78
|
+
const computed = getComputedStyle(root);
|
|
79
|
+
const rect = root.getBoundingClientRect();
|
|
80
|
+
// Chromium can include a stable thin gutter in clientWidth while excluding it
|
|
81
|
+
// from the root's actual layout box. Preserve that box, not just clientWidth.
|
|
82
|
+
const gutter = Math.max(0, (view?.innerWidth ?? root.clientWidth) - Math.min(root.clientWidth, rect.width));
|
|
83
|
+
const leftGutter = Math.min(gutter, Math.max(0, rect.left));
|
|
84
|
+
const rightGutter = gutter - leftGutter;
|
|
85
|
+
const paddingLeft = parseFloat(computed.paddingLeft) || 0;
|
|
86
|
+
const paddingRight = parseFloat(computed.paddingRight) || 0;
|
|
87
|
+
lock = { count: 0, saved: ["overflow", "scrollbar-gutter", "padding-left", "padding-right"].map(key => [key, style.getPropertyValue(key), style.getPropertyPriority(key)]) };
|
|
76
88
|
locks.set(document, lock);
|
|
89
|
+
// A stable root gutter clips top-layer painting even when its rect fills the viewport.
|
|
90
|
+
// Replace the gutter with normal-flow padding while keeping fixed modal edges flush.
|
|
91
|
+
style.setProperty("scrollbar-gutter", "auto", "important");
|
|
77
92
|
style.setProperty("overflow", "hidden", "important");
|
|
78
|
-
if (
|
|
79
|
-
style.setProperty("
|
|
93
|
+
if (leftGutter)
|
|
94
|
+
style.setProperty("padding-left", `${paddingLeft + leftGutter}px`, "important");
|
|
95
|
+
if (rightGutter)
|
|
96
|
+
style.setProperty("padding-right", `${paddingRight + rightGutter}px`, "important");
|
|
80
97
|
}
|
|
81
98
|
lock.count++;
|
|
82
|
-
return () => {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
99
|
+
return () => {
|
|
100
|
+
if (--lock.count !== 0)
|
|
101
|
+
return;
|
|
102
|
+
for (const [key, value, priority] of lock.saved) {
|
|
103
|
+
if (value)
|
|
104
|
+
style.setProperty(key, value, priority);
|
|
105
|
+
else
|
|
106
|
+
style.removeProperty(key);
|
|
107
|
+
}
|
|
108
|
+
locks.delete(document);
|
|
109
|
+
};
|
|
89
110
|
}
|
|
90
111
|
export function positionLayer(anchor, panel, side, align) {
|
|
91
112
|
const view = panel.ownerDocument.defaultView;
|
package/dist/overlays.d.ts
CHANGED
|
@@ -3,5 +3,11 @@ type AnchoredProps = (TooltipProps | PopoverProps) & {
|
|
|
3
3
|
kind: "tooltip" | "popover";
|
|
4
4
|
};
|
|
5
5
|
export declare const AnchoredOverlay: import("react").ForwardRefExoticComponent<AnchoredProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
6
|
-
|
|
6
|
+
interface ModalOverlayProps extends DialogProps {
|
|
7
|
+
showCloseButton?: boolean;
|
|
8
|
+
role?: "dialog" | "alertdialog";
|
|
9
|
+
scrollBody?: boolean;
|
|
10
|
+
footer?: import("react").ReactNode;
|
|
11
|
+
}
|
|
12
|
+
export declare const ModalOverlay: import("react").ForwardRefExoticComponent<ModalOverlayProps & import("react").RefAttributes<HTMLDialogElement>>;
|
|
7
13
|
export {};
|
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 } from "./index.js";
|
|
3
|
+
import { IconButton, ScrollArea } 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;
|
|
@@ -113,7 +113,7 @@ export const AnchoredOverlay = forwardRef(function AnchoredOverlay(allProps, for
|
|
|
113
113
|
leave();
|
|
114
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
115
|
});
|
|
116
|
-
export const ModalOverlay = forwardRef(function ModalOverlay({ 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) {
|
|
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
117
|
const [local, setLocal] = useState(defaultOpen), open = controlled ?? local;
|
|
118
118
|
const dialog = useRef(null), anchor = useRef(null), generated = useId(), id = explicitId ?? `flood-dialog-${generated}`;
|
|
119
119
|
const request = (next) => { if (controlled === undefined)
|
|
@@ -138,6 +138,8 @@ export const ModalOverlay = forwardRef(function ModalOverlay({ title, descriptio
|
|
|
138
138
|
if (typeof element.showModal !== "function")
|
|
139
139
|
throw new Error("Flood Dialog requires native showModal");
|
|
140
140
|
const previous = document.activeElement;
|
|
141
|
+
if (scrollBody)
|
|
142
|
+
element.dataset.drawerDirection = getComputedStyle(element).direction;
|
|
141
143
|
element.showModal();
|
|
142
144
|
const unlock = lockScroll(document);
|
|
143
145
|
const unregister = registerLayer(document, { panel: element, anchor: () => anchor.current, modal: true, close: () => current.current.request(false), outside: () => { if (current.current.closeOnOutsideClick)
|
|
@@ -175,5 +177,5 @@ export const ModalOverlay = forwardRef(function ModalOverlay({ title, descriptio
|
|
|
175
177
|
event.preventDefault();
|
|
176
178
|
first.focus();
|
|
177
179
|
}
|
|
178
|
-
}, children: [_jsxs("div", { className: "flood-overlay-header", children: [_jsx("h2", { id: `${id}-title`, className: "flood-overlay-title", children: title }), _jsx(OverlayClose, { label: closeLabel, onClick: () => request(false) })] }), description && _jsx("p", { id: `${id}-description`, className: "flood-overlay-description", children: description }), open && _jsx("div", { className: "flood-overlay-body", children: children })] })] });
|
|
180
|
+
}, children: [_jsxs("div", { className: "flood-overlay-header", children: [_jsx("h2", { id: `${id}-title`, className: "flood-overlay-title", children: title }), showCloseButton && _jsx(OverlayClose, { label: closeLabel, onClick: () => request(false) })] }), description && _jsx("p", { id: `${id}-description`, className: "flood-overlay-description", children: description }), open && (scrollBody ? _jsx("div", { className: "flood-drawer__body", children: _jsx(ScrollArea, { className: "flood-overlay-body", style: { height: "100%" }, children: children }) }) : _jsx("div", { className: "flood-overlay-body", children: children })), open && footer && _jsx("div", { className: "flood-drawer__footer", children: footer })] })] });
|
|
179
181
|
});
|
package/dist/select.js
CHANGED
|
@@ -328,6 +328,6 @@ export const SingleSelect = forwardRef(function SingleSelect({ children, value,
|
|
|
328
328
|
onKeyDown: (event) => { onKeyDown?.(event); event.stopPropagation(); }, onKeyUp: onKeyUp, onClick: onClick, onInvalid: (event) => { onInvalid?.(event); const prevented = event.defaultPrevented; event.preventDefault(); if (!prevented) {
|
|
329
329
|
setValidation(event.currentTarget.validationMessage);
|
|
330
330
|
trigger.current?.focus();
|
|
331
|
-
} }, children: children }), open && _jsx("div", { ref: menu,
|
|
332
|
-
event.preventDefault(); }, onClick: (event) => event.preventDefault(), children: _jsxs(ScrollArea, { ref: scrollViewport, orientation: "vertical", tabIndex: -1, className: "flood-select__viewport", children: [!choices.some((choice) => !choice.hidden) && _jsx("div", { className: "flood-select__empty", children: "No options" }), groups.map((group, index) => group.label ? _jsxs("div", { role: "group", "aria-label": group.label, children: [_jsx("div", { className: "flood-select__group", "aria-hidden": "true", children: group.label }), group.items.map(renderOption)] }, index) : _jsx(Fragment, { children: group.items.map(renderOption) }, index))] }) }), validation && _jsx("span", { className: "flood-select__validation", id: validationId, role: "alert", children: validation })] });
|
|
331
|
+
} }, children: children }), open && _jsx("div", { ref: menu, popover: "manual", className: "flood-select__menu", onPointerDown: (event) => { if (event.pointerType === "mouse")
|
|
332
|
+
event.preventDefault(); }, onClick: (event) => event.preventDefault(), children: _jsxs(ScrollArea, { ref: scrollViewport, id: menuId, role: "listbox", "aria-label": ariaLabel, "aria-labelledby": ariaLabel ? undefined : labelledBy ?? triggerId, orientation: "vertical", tabIndex: -1, className: "flood-select__viewport", children: [!choices.some((choice) => !choice.hidden) && _jsx("div", { className: "flood-select__empty", children: "No options" }), groups.map((group, index) => group.label ? _jsxs("div", { role: "group", "aria-label": group.label, children: [_jsx("div", { className: "flood-select__group", "aria-hidden": "true", children: group.label }), group.items.map(renderOption)] }, index) : _jsx(Fragment, { children: group.items.map(renderOption) }, index))] }) }), validation && _jsx("span", { className: "flood-select__validation", id: validationId, role: "alert", children: validation })] });
|
|
333
333
|
});
|
package/dist/slider.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { SliderProps, Fieldset } from "./index.js";
|
|
2
|
+
interface Props extends SliderProps {
|
|
3
|
+
fieldsetComponent: typeof Fieldset;
|
|
4
|
+
}
|
|
5
|
+
/** Native range inputs keep keyboard/AT interaction; the shared rail handles pointer selection. */
|
|
6
|
+
export declare const RangeControl: import("react").ForwardRefExoticComponent<Props & import("react").RefAttributes<HTMLFieldSetElement>>;
|
|
7
|
+
export {};
|
package/dist/slider.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useEffect, useId, useRef, useState } from "react";
|
|
3
|
+
import { hasFieldContent, mergeDescriptions, resolveFieldStatus } from "./field-state.js";
|
|
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) {
|
|
6
|
+
if (![min, max, step, max - min, (max - min) / step].every(Number.isFinite) || min >= max || step <= 0)
|
|
7
|
+
throw new Error("Flood Slider requires finite increasing bounds and a positive representable step");
|
|
8
|
+
if (!label.trim())
|
|
9
|
+
throw new Error("Flood Slider requires an accessible label");
|
|
10
|
+
const generatedId = useId();
|
|
11
|
+
const id = props.id ?? `flood-slider-${generatedId}`;
|
|
12
|
+
const hasError = hasFieldContent(props.error);
|
|
13
|
+
const status = resolveFieldStatus(props.status, hasError, props["aria-invalid"]);
|
|
14
|
+
const describedBy = mergeDescriptions(props["aria-describedby"], hasFieldContent(props.hint) && `${id}-hint`, hasError && `${id}-error`, !hasError && hasFieldContent(props.message) && `${id}-message`);
|
|
15
|
+
const snap = (number) => Number(Math.max(min, Math.min(min + Math.floor((max - min) / step + 1e-9) * step, min + Math.round((number - min) / step) * step)).toPrecision(14));
|
|
16
|
+
const normalize = (numbers) => {
|
|
17
|
+
if ((numbers.length !== 1 && numbers.length !== 2) || numbers.some(number => !Number.isFinite(number)) || (numbers.length === 2 && numbers[0] > numbers[1]))
|
|
18
|
+
throw new Error("Flood Slider accepts one value or an ordered pair of finite values");
|
|
19
|
+
return numbers.map(snap);
|
|
20
|
+
};
|
|
21
|
+
const initial = useRef(normalize(defaultValue));
|
|
22
|
+
const [local, setLocal] = useState(initial.current);
|
|
23
|
+
const values = normalize(value ?? local);
|
|
24
|
+
if (thumbLabels && (thumbLabels.length !== values.length || thumbLabels.some(text => !text.trim())))
|
|
25
|
+
throw new Error("Flood Slider needs one nonempty label per thumb");
|
|
26
|
+
const inputs = useRef([]);
|
|
27
|
+
const drag = useRef(null);
|
|
28
|
+
const valuesRef = useRef(values);
|
|
29
|
+
valuesRef.current = values;
|
|
30
|
+
const update = (index, next) => {
|
|
31
|
+
const before = valuesRef.current;
|
|
32
|
+
const number = Math.max(index === 1 ? before[0] : min, Math.min(index === 0 && before.length === 2 ? before[1] : max, snap(next)));
|
|
33
|
+
const result = before.map((old, position) => position === index ? number : old);
|
|
34
|
+
if (result.every((number, position) => number === before[position]))
|
|
35
|
+
return;
|
|
36
|
+
if (value === undefined) {
|
|
37
|
+
valuesRef.current = result;
|
|
38
|
+
setLocal(result);
|
|
39
|
+
}
|
|
40
|
+
onValueChange?.([...result]);
|
|
41
|
+
};
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const form = inputs.current[0]?.form;
|
|
44
|
+
if (!form || value !== undefined)
|
|
45
|
+
return;
|
|
46
|
+
const reset = (event) => queueMicrotask(() => { if (!event.defaultPrevented)
|
|
47
|
+
setLocal(initial.current); });
|
|
48
|
+
form.addEventListener("reset", reset);
|
|
49
|
+
return () => form.removeEventListener("reset", reset);
|
|
50
|
+
}, [value, props.form]);
|
|
51
|
+
const percent = (number) => 100 * (number - min) / (max - min);
|
|
52
|
+
const pointValue = (node, clientX) => {
|
|
53
|
+
const box = node.getBoundingClientRect();
|
|
54
|
+
const ratio = Math.max(0, Math.min(1, (clientX - box.left) / box.width));
|
|
55
|
+
return min + (node.ownerDocument.defaultView?.getComputedStyle(node).direction === "rtl" ? 1 - ratio : ratio) * (max - min);
|
|
56
|
+
};
|
|
57
|
+
const inert = (node) => disabled || readOnly || Boolean(node.closest("fieldset:disabled"));
|
|
58
|
+
const start = values.length === 1 ? 0 : percent(values[0]);
|
|
59
|
+
const end = percent(values[values.length - 1]);
|
|
60
|
+
return _jsxs(Group, { ...props, id: id, ref: ref, legend: label, name: name, disabled: disabled, className: ["flood-slider", `flood-control--${controlSize}`, className].filter(Boolean).join(" "), "data-readonly": readOnly || undefined, onKeyDown: event => {
|
|
61
|
+
onKeyDown?.(event);
|
|
62
|
+
if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey || event.nativeEvent.isComposing)
|
|
63
|
+
return;
|
|
64
|
+
if (readOnly && ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Home", "End", "PageUp", "PageDown"].includes(event.key))
|
|
65
|
+
event.preventDefault();
|
|
66
|
+
const index = inputs.current.indexOf(event.target);
|
|
67
|
+
if (index < 0 || inert(event.currentTarget) || (event.key !== "ArrowLeft" && event.key !== "ArrowRight"))
|
|
68
|
+
return;
|
|
69
|
+
// Native WebKit does not reverse horizontal range keys in RTL.
|
|
70
|
+
const rtl = event.currentTarget.ownerDocument.defaultView?.getComputedStyle(event.currentTarget).direction === "rtl";
|
|
71
|
+
event.preventDefault();
|
|
72
|
+
update(index, values[index] + step * (event.key === (rtl ? "ArrowLeft" : "ArrowRight") ? 1 : -1));
|
|
73
|
+
}, children: [showValue && _jsx("div", { className: "flood-slider__value", "aria-hidden": "true", children: _jsx("bdi", { dir: "ltr", children: values.join(" – ") }) }), _jsxs("div", { className: "flood-slider__rail", style: { "--flood-slider-start": `${start}%`, "--flood-slider-end": `${100 - end}%` }, onPointerDown: event => {
|
|
74
|
+
if (inputs.current.includes(event.target) || inert(event.currentTarget) || event.button !== 0)
|
|
75
|
+
return;
|
|
76
|
+
event.preventDefault();
|
|
77
|
+
const next = pointValue(event.currentTarget, event.clientX);
|
|
78
|
+
const lowerDistance = Math.abs(next - values[0]);
|
|
79
|
+
const upperDistance = values.length === 2 ? Math.abs(next - values[1]) : Infinity;
|
|
80
|
+
const index = upperDistance < lowerDistance || (values.length === 2 && upperDistance === lowerDistance && next >= values[0]) ? 1 : 0;
|
|
81
|
+
drag.current = index;
|
|
82
|
+
inputs.current[index]?.focus();
|
|
83
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
84
|
+
update(index, next);
|
|
85
|
+
}, onPointerMove: event => { if (drag.current !== null && !inert(event.currentTarget))
|
|
86
|
+
update(drag.current, pointValue(event.currentTarget, event.clientX)); }, onPointerUp: () => { drag.current = null; }, onLostPointerCapture: () => { drag.current = null; }, onPointerCancel: () => { drag.current = null; }, children: [_jsx("span", { className: "flood-slider__track", "aria-hidden": "true", children: _jsx("span", { className: "flood-slider__fill" }) }), values.map((number, index) => _jsx("input", { ref: node => { inputs.current[index] = node; }, type: "range", className: "flood-slider__input", name: name, form: props.form, value: number, min: min, max: max, step: step, disabled: disabled, "aria-label": thumbLabels?.[index] ?? (values.length === 1 ? label : `${label}: ${index === 0 ? "minimum" : "maximum"}`), "aria-valuemin": index === 1 ? values[0] : min, "aria-valuemax": index === 0 && values.length === 2 ? values[1] : max, "aria-describedby": describedBy, "aria-readonly": readOnly || undefined, "aria-invalid": status === "error" || undefined, onPointerDown: event => { if (readOnly)
|
|
87
|
+
event.preventDefault(); }, onChange: event => { if (!inert(event.currentTarget))
|
|
88
|
+
update(index, event.currentTarget.valueAsNumber); } }, index))] })] });
|
|
89
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SearchInputProps, PasswordInputProps, InputGroup, IconButton, IconSlot } from "./index.js";
|
|
2
|
+
type Parts = {
|
|
3
|
+
group: typeof InputGroup;
|
|
4
|
+
action: typeof IconButton;
|
|
5
|
+
icon: typeof IconSlot;
|
|
6
|
+
};
|
|
7
|
+
export declare const SearchField: import("react").ForwardRefExoticComponent<SearchInputProps & Parts & import("react").RefAttributes<HTMLInputElement>>;
|
|
8
|
+
export declare const PasswordField: import("react").ForwardRefExoticComponent<PasswordInputProps & Parts & import("react").RefAttributes<HTMLInputElement>>;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react";
|
|
3
|
+
const glyph = (kind, visible = false) => _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: kind === "search" ? _jsxs(_Fragment, { children: [_jsx("circle", { cx: "10.5", cy: "10.5", r: "6.5" }), _jsx("path", { d: "m16 16 4 4" })] }) : kind === "clear" ? _jsx("path", { d: "m6 6 12 12M18 6 6 18" }) : _jsxs(_Fragment, { children: [_jsx("path", { d: "M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12Z" }), _jsx("circle", { cx: "12", cy: "12", r: "3" }), visible && _jsx("path", { d: "m3 3 18 18" })] }) });
|
|
4
|
+
export const SearchField = forwardRef(function SearchField({ group: Group, action: Action, icon: Icon, clearable = true, clearLabel = "Clear search", controlSize = "md", className, value, defaultValue, onChange, disabled, readOnly, ...props }, ref) {
|
|
5
|
+
const input = useRef(null);
|
|
6
|
+
useImperativeHandle(ref, () => input.current, []);
|
|
7
|
+
const [text, setText] = useState(String(defaultValue ?? ""));
|
|
8
|
+
const current = value === undefined ? text : String(value);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
const form = input.current?.form;
|
|
11
|
+
if (!form)
|
|
12
|
+
return;
|
|
13
|
+
const reset = (event) => setTimeout(() => { if (!event.defaultPrevented && input.current)
|
|
14
|
+
setText(input.current.value); }, 0);
|
|
15
|
+
form.addEventListener("reset", reset);
|
|
16
|
+
return () => form.removeEventListener("reset", reset);
|
|
17
|
+
}, [props.form]);
|
|
18
|
+
const clear = () => {
|
|
19
|
+
const node = input.current;
|
|
20
|
+
if (!node || readOnly || node.matches(":disabled"))
|
|
21
|
+
return;
|
|
22
|
+
const view = node.ownerDocument.defaultView;
|
|
23
|
+
Object.getOwnPropertyDescriptor(view.HTMLInputElement.prototype, "value").set.call(node, "");
|
|
24
|
+
node.dispatchEvent(new view.Event("input", { bubbles: true }));
|
|
25
|
+
node.focus();
|
|
26
|
+
};
|
|
27
|
+
return _jsx(Group, { ...props, ref: input, type: "search", controlSize: controlSize, className: ["flood-search-input", className].filter(Boolean).join(" "), value: value, defaultValue: defaultValue, disabled: disabled, readOnly: readOnly, onChange: event => { setText(event.currentTarget.value); onChange?.(event); }, start: _jsx(Icon, { size: controlSize, children: glyph("search") }), end: clearable && current.length > 0 && !readOnly ? _jsx(Action, { label: clearLabel, variant: "ghost", size: controlSize, disabled: disabled, onPointerDown: event => event.preventDefault(), onClick: clear, children: glyph("clear") }) : undefined });
|
|
28
|
+
});
|
|
29
|
+
export const PasswordField = forwardRef(function PasswordField({ group: Group, action: Action, icon: _Icon, visible, defaultVisible = false, onVisibleChange, showPasswordLabel = "Show password", hidePasswordLabel = "Hide password", controlSize = "md", className, disabled, ...props }, ref) {
|
|
30
|
+
const input = useRef(null);
|
|
31
|
+
useImperativeHandle(ref, () => input.current, []);
|
|
32
|
+
const [local, setLocal] = useState(defaultVisible);
|
|
33
|
+
const shown = visible ?? local;
|
|
34
|
+
return _jsx(Group, { ...props, ref: input, type: shown ? "text" : "password", disabled: disabled, controlSize: controlSize, className: ["flood-password-input", className].filter(Boolean).join(" "), end: _jsx(Action, { label: shown ? hidePasswordLabel : showPasswordLabel, variant: "ghost", size: controlSize, disabled: disabled, onPointerDown: event => event.preventDefault(), onClick: () => {
|
|
35
|
+
const node = input.current;
|
|
36
|
+
if (!node || node.matches(":disabled"))
|
|
37
|
+
return;
|
|
38
|
+
const start = node.selectionStart, end = node.selectionEnd, direction = node.selectionDirection;
|
|
39
|
+
if (visible === undefined)
|
|
40
|
+
setLocal(!shown);
|
|
41
|
+
onVisibleChange?.(!shown);
|
|
42
|
+
requestAnimationFrame(() => { if (start !== null && end !== null && node.isConnected)
|
|
43
|
+
node.setSelectionRange(start, end, direction ?? undefined); });
|
|
44
|
+
}, children: glyph("eye", shown) }) });
|
|
45
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flood-md/react",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Original Flood components for React and Next.js.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"react-dom": "^18.3.0 || ^19.0.0"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@flood-md/tokens": "0.2.
|
|
33
|
+
"@flood-md/tokens": "0.2.6"
|
|
34
34
|
},
|
|
35
35
|
"publishConfig": {
|
|
36
36
|
"access": "public"
|