@flood-md/react 0.2.3 → 0.2.4
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 +308 -3
- package/dist/avatar.d.ts +5 -0
- package/dist/avatar.js +8 -0
- package/dist/checkbox.d.ts +4 -0
- package/dist/checkbox.js +15 -0
- package/dist/data-display.d.ts +3 -0
- package/dist/data-display.js +11 -0
- package/dist/disclosure.js +2 -1
- package/dist/feedback.d.ts +8 -0
- package/dist/feedback.js +29 -0
- package/dist/focusable-hidden.d.ts +3 -0
- package/dist/focusable-hidden.js +99 -0
- package/dist/fonts/InterVariable-Italic.woff2 +0 -0
- package/dist/fonts/README.md +9 -5
- package/dist/foundations.css +196 -12
- package/dist/index.d.ts +191 -2
- package/dist/index.js +145 -36
- package/dist/menu.d.ts +2 -0
- package/dist/menu.js +130 -0
- package/dist/navigation.js +8 -8
- package/dist/overlay-runtime.js +10 -4
- package/dist/overlays.js +12 -3
- package/dist/scroll-area.js +17 -2
- package/dist/select.js +96 -47
- package/dist/styles.css +9 -0
- package/dist/toolbar.d.ts +2 -0
- package/dist/toolbar.js +80 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,20 +1,76 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { forwardRef, createElement, useId, useState, } from "react";
|
|
4
|
+
import { useFocusableHidden } from "./focusable-hidden.js";
|
|
4
5
|
import { resolveFieldStatus, hasFieldContent, mergeDescriptions } from "./field-state.js";
|
|
5
6
|
import { SingleSelect } from "./select.js";
|
|
7
|
+
import { MixedCheckbox } from "./checkbox.js";
|
|
6
8
|
import { OverlayScrollArea } from "./scroll-area.js";
|
|
7
9
|
import { ChoiceGroup } from "./choice-group.js";
|
|
8
10
|
import { Disclosure } from "./disclosure.js";
|
|
9
11
|
import { AnchoredOverlay, ModalOverlay } from "./overlays.js";
|
|
10
12
|
import { TabNavigation, BreadcrumbNavigation, PageNavigation } from "./navigation.js";
|
|
13
|
+
import { AvatarArtwork } from "./avatar.js";
|
|
14
|
+
import { ValueIndicator, FeedbackMessage } from "./feedback.js";
|
|
15
|
+
import { DescribedData, TabularData } from "./data-display.js";
|
|
16
|
+
import { ActionMenu } from "./menu.js";
|
|
17
|
+
import { RovingToolbar } from "./toolbar.js";
|
|
11
18
|
const classes = (...values) => values.filter(Boolean).join(" ");
|
|
12
19
|
export const Button = forwardRef(function Button({ variant = "solid", size = "md", loading = false, disabled, type = "button", className, children, ...props }, ref) {
|
|
13
|
-
return (_jsxs("button", { ...props, ref: ref, type: type, disabled: disabled || loading, "aria-busy": loading || undefined, className: classes("flood-button", `flood-button--${variant}`, `flood-button--${size}`, className), children: [loading && _jsx(
|
|
20
|
+
return (_jsxs("button", { ...props, ref: ref, type: type, disabled: disabled || loading, "aria-busy": loading || undefined, className: classes("flood-button", `flood-button--${variant}`, `flood-button--${size}`, className), children: [loading && _jsx(Spinner, { decorative: true }), children] }));
|
|
21
|
+
});
|
|
22
|
+
/** @since 0.2.4 */
|
|
23
|
+
export const Kbd = forwardRef(function Kbd({ size = "sm", className, ...props }, ref) {
|
|
24
|
+
return _jsx("kbd", { ...props, ref: ref, className: classes("flood-kbd", className), "data-size": size });
|
|
25
|
+
});
|
|
26
|
+
/** @since 0.2.4 */
|
|
27
|
+
export const Toolbar = forwardRef(function Toolbar({ label, orientation = "horizontal", loop = true, ...props }, ref) {
|
|
28
|
+
return _jsx(RovingToolbar, { ...props, ref: ref, label: label, orientation: orientation, loop: loop });
|
|
29
|
+
});
|
|
30
|
+
/** @since 0.2.4 */
|
|
31
|
+
export const DescriptionList = forwardRef(function DescriptionList({ items, layout = "inline", density = "compact", dividers = false, ...props }, ref) {
|
|
32
|
+
return _jsx(DescribedData, { ...props, ref: ref, items: items, layout: layout, density: density, dividers: dividers });
|
|
33
|
+
});
|
|
34
|
+
/** @since 0.2.4 */
|
|
35
|
+
export const Table = forwardRef(function Table({ caption, children, hideCaption = false, density = "comfortable", striped = false, scrollLabel, ...props }, ref) {
|
|
36
|
+
return _jsx(TabularData, { ...props, ref: ref, caption: caption, hideCaption: hideCaption, density: density, striped: striped, scrollLabel: scrollLabel, children: children });
|
|
37
|
+
});
|
|
38
|
+
/** @since 0.2.4 */
|
|
39
|
+
export const Badge = forwardRef(function Badge({ size = "md", tone = "neutral", variant = "subtle", className, ...props }, ref) {
|
|
40
|
+
return _jsx("span", { ...props, ref: ref, className: classes("flood-badge", className), "data-size": size, "data-tone": tone, "data-variant": variant });
|
|
41
|
+
});
|
|
42
|
+
/** @since 0.2.4 */
|
|
43
|
+
export const Spinner = forwardRef(function Spinner({ size = "md", label = "Loading", decorative = false, className, ...props }, ref) {
|
|
44
|
+
return _jsxs("span", { ...props, ref: ref, className: classes("flood-loading", className), "data-size": size, role: decorative ? undefined : "status", "aria-hidden": decorative || undefined, children: [_jsx("span", { className: "flood-spinner", "aria-hidden": "true" }), !decorative && _jsx(VisuallyHidden, { children: label })] });
|
|
45
|
+
});
|
|
46
|
+
/** @since 0.2.4 */
|
|
47
|
+
export const Skeleton = forwardRef(function Skeleton({ shape = "text", width, height, animate = true, className, style, ...props }, ref) {
|
|
48
|
+
return _jsx("span", { ...props, ref: ref, className: classes("flood-skeleton", className), "data-shape": shape, "data-animate": animate, "aria-hidden": "true", style: { width, height, ...style } });
|
|
49
|
+
});
|
|
50
|
+
/** @since 0.2.4 */
|
|
51
|
+
export const Avatar = forwardRef(function Avatar({ name, src, fallback, size = "md", shape = "circle", decorative = false, className, ...props }, ref) {
|
|
52
|
+
const initials = name.trim().split(/\s+/u).filter(Boolean).slice(0, 2).map(word => Array.from(word)[0]).join("").toUpperCase();
|
|
53
|
+
return _jsx("span", { ...props, ref: ref, className: classes("flood-avatar", className), "data-size": size, "data-shape": shape, role: decorative ? undefined : "img", "aria-label": decorative ? undefined : name, "aria-hidden": decorative || undefined, children: _jsx(AvatarArtwork, { src: src, fallback: fallback ?? (initials || "?") }, src ?? "") });
|
|
54
|
+
});
|
|
55
|
+
/** @since 0.2.4 */
|
|
56
|
+
export const Progress = forwardRef(function Progress({ label, value, max = 100, valueText, size = "md", tone = "accent", hideLabel = false, ...props }, ref) {
|
|
57
|
+
return _jsx(ValueIndicator, { ...props, ref: ref, kind: "progress", label: label, value: value, min: 0, max: max, valueText: valueText, size: size, tone: tone, hideLabel: hideLabel });
|
|
58
|
+
});
|
|
59
|
+
/** @since 0.2.4 */
|
|
60
|
+
export const Meter = forwardRef(function Meter({ label, value, min = 0, max = 100, valueText, size = "md", tone = "neutral", hideLabel = false, ...props }, ref) {
|
|
61
|
+
return _jsx(ValueIndicator, { ...props, ref: ref, kind: "meter", label: label, value: value, min: min, max: max, valueText: valueText, size: size, tone: tone, hideLabel: hideLabel });
|
|
62
|
+
});
|
|
63
|
+
/** @since 0.2.4 */
|
|
64
|
+
export const Alert = forwardRef(function Alert({ title, tone = "info", icon, announcement = "off", ...props }, ref) {
|
|
65
|
+
return _jsx(FeedbackMessage, { ...props, ref: ref, title: title, tone: tone, icon: icon, announcement: announcement });
|
|
14
66
|
});
|
|
15
67
|
export const Tabs = forwardRef(function Tabs({ activationMode = "automatic", orientation = "horizontal", controlSize = "md", disabled = false, iconOnly = false, ...props }, ref) { return _jsx(TabNavigation, { ...props, ref: ref, activationMode: activationMode, orientation: orientation, controlSize: controlSize, disabled: disabled, iconOnly: iconOnly }); });
|
|
16
68
|
export const Breadcrumb = forwardRef(function Breadcrumb({ label = "Breadcrumb", separator = "/", ...props }, ref) { return _jsx(BreadcrumbNavigation, { ...props, ref: ref, label: label, separator: separator }); });
|
|
17
69
|
export const Pagination = forwardRef(function Pagination({ siblingCount = 1, controlSize = "sm", disabled = false, label = "Pagination", previousLabel = "Previous page", nextLabel = "Next page", ...props }, ref) { return _jsx(PageNavigation, { ...props, ref: ref, siblingCount: siblingCount, controlSize: controlSize, disabled: disabled, label: label, previousLabel: previousLabel, nextLabel: nextLabel }); });
|
|
70
|
+
/** @since 0.2.4 */
|
|
71
|
+
export const Menu = forwardRef(function Menu({ defaultOpen = false, disabled = false, side = "bottom", align = "start", controlSize = "sm", ...props }, ref) {
|
|
72
|
+
return _jsx(ActionMenu, { ...props, ref: ref, defaultOpen: defaultOpen, disabled: disabled, side: side, align: align, controlSize: controlSize });
|
|
73
|
+
});
|
|
18
74
|
export const Tooltip = forwardRef(function Tooltip({ side = "top", align = "center", delay = 300, disabled = false, ...props }, ref) { return _jsx(AnchoredOverlay, { ...props, ref: ref, kind: "tooltip", side: side, align: align, delay: delay, disabled: disabled }); });
|
|
19
75
|
export const Popover = forwardRef(function Popover({ side = "bottom", align = "start", defaultOpen = false, disabled = false, closeLabel = "Close", ...props }, ref) { return _jsx(AnchoredOverlay, { ...props, ref: ref, kind: "popover", side: side, align: align, defaultOpen: defaultOpen, disabled: disabled, closeLabel: closeLabel }); });
|
|
20
76
|
export const Dialog = forwardRef(function Dialog({ defaultOpen = false, disabled = false, closeLabel = "Close", closeOnOutsideClick = true, ...props }, ref) { return _jsx(ModalOverlay, { ...props, ref: ref, defaultOpen: defaultOpen, disabled: disabled, closeLabel: closeLabel, closeOnOutsideClick: closeOnOutsideClick }); });
|
|
@@ -35,7 +91,7 @@ export const Accordion = forwardRef(function Accordion({ label, items, expansion
|
|
|
35
91
|
throw new Error("Flood single Accordion accepts at most one value");
|
|
36
92
|
return _jsx("div", { ...props, ref: ref, role: "group", "aria-label": label, className: classes("flood-accordion", className), onKeyDown: event => {
|
|
37
93
|
onKeyDown?.(event);
|
|
38
|
-
if (event.defaultPrevented)
|
|
94
|
+
if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey || event.nativeEvent.isComposing)
|
|
39
95
|
return;
|
|
40
96
|
const buttons = Array.from(event.currentTarget.querySelectorAll(":scope > .flood-disclosure > .flood-disclosure__heading > .flood-disclosure__trigger:not(:disabled)"));
|
|
41
97
|
const index = buttons.indexOf(event.target);
|
|
@@ -76,39 +132,39 @@ export const ToggleGroup = forwardRef(function ToggleGroup({ label, options, sel
|
|
|
76
132
|
throw new Error("Flood single ToggleGroup accepts at most one value");
|
|
77
133
|
if (iconOnly && options.some(option => !option.icon))
|
|
78
134
|
throw new Error("Flood icon-only toggle options require icons");
|
|
79
|
-
return _jsx("div", { ...props, ref: ref, role: "group", "aria-label": label, "data-orientation": orientation, "data-selection-mode": selectionMode, className: classes("flood-button-group", "flood-toggle-group", className), onKeyDown: event => {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
135
|
+
return _jsx(ScrollArea, { orientation: "horizontal", tabIndex: -1, role: "group", "aria-label": `${label} scrolling`, className: "flood-connected-scroll", hidden: props.hidden, dir: props.dir, lang: props.lang, style: { maxInlineSize: "100%" }, children: _jsx("div", { ...props, ref: ref, role: "group", "aria-label": label, "data-orientation": orientation, "data-selection-mode": selectionMode, className: classes("flood-button-group", "flood-toggle-group", className), onKeyDown: event => {
|
|
136
|
+
onKeyDown?.(event);
|
|
137
|
+
if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey || event.nativeEvent.isComposing)
|
|
138
|
+
return;
|
|
139
|
+
const buttons = Array.from(event.currentTarget.querySelectorAll(":scope > .flood-toggle:not(:disabled)"));
|
|
140
|
+
const index = buttons.indexOf(event.target);
|
|
141
|
+
if (index < 0)
|
|
142
|
+
return;
|
|
143
|
+
const rtl = getComputedStyle(event.currentTarget).direction === "rtl";
|
|
144
|
+
const nextKey = orientation === "vertical" ? "ArrowDown" : rtl ? "ArrowLeft" : "ArrowRight";
|
|
145
|
+
const previousKey = orientation === "vertical" ? "ArrowUp" : rtl ? "ArrowRight" : "ArrowLeft";
|
|
146
|
+
const next = event.key === "Home" ? 0 : event.key === "End" ? buttons.length - 1 : event.key === nextKey ? (index + 1) % buttons.length : event.key === previousKey ? (index - 1 + buttons.length) % buttons.length : -1;
|
|
147
|
+
if (next >= 0) {
|
|
148
|
+
event.preventDefault();
|
|
149
|
+
buttons[next].focus();
|
|
150
|
+
}
|
|
151
|
+
}, children: options.map(option => _jsx(Toggle, { size: size, variant: variant, disabled: disabled || option.disabled, pressed: selected.includes(option.value), iconOnly: iconOnly, "aria-label": iconOnly ? option.label : undefined, onPressedChange: active => {
|
|
152
|
+
const next = active ? selectionMode === "single" ? [option.value] : [...new Set([...selected, option.value])] : selected.filter(item => item !== option.value);
|
|
153
|
+
if (value === undefined)
|
|
154
|
+
setLocal(next);
|
|
155
|
+
onValueChange?.([...next]);
|
|
156
|
+
}, children: iconOnly ? option.icon : _jsxs(_Fragment, { children: [option.icon && _jsx(IconSlot, { size: size, children: option.icon }), option.label] }) }, option.value)) }) });
|
|
101
157
|
});
|
|
102
158
|
/** Connected actions keep their ordinary Tab order; this is not a toolbar. */
|
|
103
159
|
export const ButtonGroup = forwardRef(function ButtonGroup({ label, orientation = "horizontal", className, ...props }, ref) {
|
|
104
|
-
return _jsx("div", { ...props, ref: ref, role: "group", "aria-label": label, "data-orientation": orientation, className: classes("flood-button-group", className) });
|
|
160
|
+
return _jsx(ScrollArea, { orientation: "horizontal", tabIndex: -1, role: "group", "aria-label": `${label} scrolling`, className: "flood-connected-scroll", hidden: props.hidden, dir: props.dir, lang: props.lang, style: { maxInlineSize: "100%" }, children: _jsx("div", { ...props, ref: ref, role: "group", "aria-label": label, "data-orientation": orientation, className: classes("flood-button-group", className) }) });
|
|
105
161
|
});
|
|
106
162
|
export const Input = forwardRef(function Input({ invalid, status, controlSize = "md", className, ...props }, ref) {
|
|
107
163
|
return (_jsx("input", { ...props, ref: ref, "data-status": resolveFieldStatus(status, invalid, props["aria-invalid"]), "aria-invalid": invalid || status === "error" || props["aria-invalid"], className: classes("flood-input", `flood-control--${controlSize}`, className) }));
|
|
108
164
|
});
|
|
109
165
|
/** One real Input with host-owned logical start/end adornments. Ref and native attributes target the input. */
|
|
110
166
|
export const InputGroup = forwardRef(function InputGroup({ start, end, controlSize = "md", className, style, invalid, status, disabled, readOnly, ...props }, ref) {
|
|
111
|
-
return _jsxs("div", { hidden: props.hidden, dir: props.dir, lang: props.lang, className: classes("flood-input-group", `flood-control--${controlSize}`, className), style: style, "data-status": resolveFieldStatus(status, invalid, props["aria-invalid"]), "data-disabled": disabled || undefined, "data-readonly": readOnly || undefined, children: [hasFieldContent(start) && _jsx("div", { className: "flood-input-group__addon flood-input-group__addon--start", children: start }), _jsx(Input, { ...props, ref: ref, controlSize: controlSize, invalid: invalid, status: status, disabled: disabled, readOnly: readOnly }), hasFieldContent(end) && _jsx("div", { className: "flood-input-group__addon flood-input-group__addon--end", children: end })] });
|
|
167
|
+
return _jsxs("div", { hidden: props.hidden, dir: props.dir, lang: props.lang, className: classes("flood-input-group", `flood-control--${controlSize}`, className), style: style, "data-status": resolveFieldStatus(status, invalid, props["aria-invalid"]), "data-disabled": disabled || undefined, "data-readonly": readOnly || undefined, children: [hasFieldContent(start) && _jsx("div", { className: "flood-input-group__addon flood-input-group__addon--start", children: typeof start === "string" ? _jsx("bdi", { dir: "auto", children: start }) : start }), _jsx(Input, { ...props, ref: ref, controlSize: controlSize, invalid: invalid, status: status, disabled: disabled, readOnly: readOnly }), hasFieldContent(end) && _jsx("div", { className: "flood-input-group__addon flood-input-group__addon--end", children: typeof end === "string" ? _jsx("bdi", { dir: "auto", children: end }) : end })] });
|
|
112
168
|
});
|
|
113
169
|
export const Textarea = forwardRef(function Textarea({ invalid, status, controlSize = "md", className, ...props }, ref) {
|
|
114
170
|
return (_jsx("textarea", { ...props, ref: ref, "data-status": resolveFieldStatus(status, invalid, props["aria-invalid"]), "aria-invalid": invalid || status === "error" || props["aria-invalid"], className: classes("flood-input", "flood-textarea", `flood-control--${controlSize}`, className) }));
|
|
@@ -118,16 +174,16 @@ export const Select = forwardRef(function Select({ invalid, status, controlSize
|
|
|
118
174
|
const listbox = multiple || (size !== undefined && size > 1);
|
|
119
175
|
if (!listbox)
|
|
120
176
|
return _jsx(SingleSelect, { ...props, status: status, invalid: invalid, controlSize: controlSize, className: className, size: size, ref: ref });
|
|
121
|
-
return (_jsx("span", { className: classes("flood-select", `flood-control--${controlSize}`, listbox && "flood-select--listbox"), children: _jsx("select", { ...props, ref: ref, multiple: multiple, size: size, "data-status": resolveFieldStatus(status, invalid, props["aria-invalid"]), "aria-invalid": invalid || status === "error" || props["aria-invalid"], className: classes("flood-input", "flood-select__control", `flood-control--${controlSize}`, className) }) }));
|
|
177
|
+
return (_jsx("span", { hidden: props.hidden, dir: props.dir, lang: props.lang, className: classes("flood-select", `flood-control--${controlSize}`, listbox && "flood-select--listbox"), children: _jsx("select", { ...props, ref: ref, multiple: multiple, size: size, "data-status": resolveFieldStatus(status, invalid, props["aria-invalid"]), "aria-invalid": invalid || status === "error" || props["aria-invalid"], className: classes("flood-input", "flood-select__control", `flood-control--${controlSize}`, className) }) }));
|
|
122
178
|
});
|
|
123
|
-
export const Checkbox = forwardRef(function Checkbox({ label, className, ...props }, ref) {
|
|
124
|
-
return (_jsxs("label", { className: classes("flood-choice", className), children: [_jsx(
|
|
179
|
+
export const Checkbox = forwardRef(function Checkbox({ label, indeterminate = false, className, ...props }, ref) {
|
|
180
|
+
return (_jsxs("label", { hidden: props.hidden, dir: props.dir, lang: props.lang, className: classes("flood-choice", className), children: [_jsx(MixedCheckbox, { ...props, ref: ref, indeterminate: indeterminate }), _jsx("span", { children: label })] }));
|
|
125
181
|
});
|
|
126
182
|
export const Radio = forwardRef(function Radio({ label, className, ...props }, ref) {
|
|
127
|
-
return (_jsxs("label", { className: classes("flood-choice", className), children: [_jsx("input", { ...props, ref: ref, type: "radio", className: "flood-check" }), _jsx("span", { children: label })] }));
|
|
183
|
+
return (_jsxs("label", { hidden: props.hidden, dir: props.dir, lang: props.lang, className: classes("flood-choice", className), children: [_jsx("input", { ...props, ref: ref, type: "radio", className: "flood-check" }), _jsx("span", { children: label })] }));
|
|
128
184
|
});
|
|
129
185
|
export const Switch = forwardRef(function Switch({ label, className, ...props }, ref) {
|
|
130
|
-
return (_jsxs("label", { className: classes("flood-choice", "flood-switch", className), children: [_jsx("span", { children: label }), _jsx("input", { ...props, ref: ref, type: "checkbox", role: "switch", className: "flood-switch-input" })] }));
|
|
186
|
+
return (_jsxs("label", { hidden: props.hidden, dir: props.dir, lang: props.lang, className: classes("flood-choice", "flood-switch", className), children: [_jsx("span", { children: label }), _jsx("input", { ...props, ref: ref, type: "checkbox", role: "switch", className: "flood-switch-input" })] }));
|
|
131
187
|
});
|
|
132
188
|
/** A native label; the required marker never substitutes for the control's required prop. */
|
|
133
189
|
export const Label = forwardRef(function Label({ required, children, className, ...props }, ref) {
|
|
@@ -137,10 +193,11 @@ export const Label = forwardRef(function Label({ required, children, className,
|
|
|
137
193
|
export const Fieldset = forwardRef(function Fieldset({ legend, hideLabel = false, hint, error, message, status, children, className, id: explicitId, "aria-describedby": describedBy, "aria-invalid": ariaInvalid, ...props }, ref) {
|
|
138
194
|
const generatedId = useId();
|
|
139
195
|
const id = explicitId ?? `flood-${generatedId}`;
|
|
196
|
+
const legendRef = useFocusableHidden(hideLabel);
|
|
140
197
|
const hasHint = hasFieldContent(hint), hasError = hasFieldContent(error), hasMessage = hasFieldContent(message) && !hasError;
|
|
141
198
|
const resolvedStatus = resolveFieldStatus(status, hasError, ariaInvalid);
|
|
142
199
|
const description = mergeDescriptions(describedBy, hasHint && `${id}-hint`, hasError && `${id}-error`, hasMessage && `${id}-message`);
|
|
143
|
-
return _jsxs("fieldset", { ...props, ref: ref, id: id, className: classes("flood-fieldset", className), "data-hide-label": hideLabel || undefined, "data-status": resolvedStatus, "aria-describedby": description, "aria-invalid": resolvedStatus === "error" || ariaInvalid || undefined, children: [_jsx("legend", { className: classes("flood-legend", hideLabel && "flood-visually-hidden"), children: legend }), _jsxs("div", { className: "flood-fieldset__body", children: [hasHint && _jsx("div", { id: `${id}-hint`, className: "flood-hint", children: hint }), children, hasMessage && _jsx("div", { id: `${id}-message`, className: "flood-message", role: resolvedStatus === "error" ? "alert" : undefined, children: message }), hasError && _jsx("div", { id: `${id}-error`, className: "flood-error", role: "alert", children: error })] })] });
|
|
200
|
+
return _jsxs("fieldset", { ...props, ref: ref, id: id, className: classes("flood-fieldset", className), "data-hide-label": hideLabel || undefined, "data-status": resolvedStatus, "aria-describedby": description, "aria-invalid": resolvedStatus === "error" || ariaInvalid || undefined, children: [_jsx("legend", { ref: legendRef, className: classes("flood-legend", hideLabel && "flood-visually-hidden--focusable"), children: legend }), _jsxs("div", { className: "flood-fieldset__body", children: [hasHint && _jsx("div", { id: `${id}-hint`, className: "flood-hint", children: hint }), children, hasMessage && _jsx("div", { id: `${id}-message`, className: "flood-message", role: resolvedStatus === "error" ? "alert" : undefined, children: message }), hasError && _jsx("div", { id: `${id}-error`, className: "flood-error", role: "alert", children: error })] })] });
|
|
144
201
|
});
|
|
145
202
|
export const RadioGroup = forwardRef(function RadioGroup({ value, defaultValue, onValueChange, orientation = "vertical", ...props }, ref) {
|
|
146
203
|
return _jsx(ChoiceGroup, { ...props, ref: ref, kind: "radio", orientation: orientation, selected: value === undefined ? undefined : [value], initial: defaultValue === undefined ? [] : [defaultValue], onSelectionChange: next => onValueChange?.(next[0] ?? ""), fieldsetComponent: Fieldset, choiceComponent: Radio });
|
|
@@ -151,7 +208,7 @@ export const SegmentedControl = forwardRef(function SegmentedControl({ label, op
|
|
|
151
208
|
throw new Error("Flood icon-only segments require icons");
|
|
152
209
|
return _jsx(RadioGroup, { ...props, ref: ref, legend: label, orientation: orientation, className: classes("flood-segmented", `flood-control--${controlSize}`, iconOnly && "flood-segmented--icons", fullWidth && "flood-segmented--full", className), options: options.map(option => ({ ...option, label: _jsxs(_Fragment, { children: [option.icon && _jsx(IconSlot, { size: controlSize, children: option.icon }), iconOnly ? _jsx("span", { className: "flood-segmented__name", children: option.label }) : option.label] }) })), onKeyDown: event => {
|
|
153
210
|
onKeyDown?.(event);
|
|
154
|
-
if (event.defaultPrevented)
|
|
211
|
+
if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey || event.nativeEvent.isComposing)
|
|
155
212
|
return;
|
|
156
213
|
const inputs = Array.from(event.currentTarget.querySelectorAll(":scope > .flood-fieldset__body > .flood-choice-group > .flood-choice > input[type=radio]:not(:disabled)"));
|
|
157
214
|
const index = inputs.indexOf(event.target);
|
|
@@ -174,12 +231,13 @@ export const CheckboxGroup = forwardRef(function CheckboxGroup({ value, defaultV
|
|
|
174
231
|
export function Field({ status, message, id: explicitId, label, hideLabel = false, hint, error, required, describedBy, children, }) {
|
|
175
232
|
const generatedId = useId();
|
|
176
233
|
const id = explicitId ?? `flood-${generatedId}`;
|
|
234
|
+
const labelRef = useFocusableHidden(hideLabel);
|
|
177
235
|
const hasMessage = hasFieldContent(message);
|
|
178
236
|
const hasHint = hasFieldContent(hint);
|
|
179
237
|
const hasError = hasFieldContent(error);
|
|
180
238
|
const resolvedStatus = hasError ? "error" : status;
|
|
181
239
|
const description = mergeDescriptions(describedBy, hasHint && `${id}-hint`, hasError && `${id}-error`, hasMessage && !hasError && `${id}-message`);
|
|
182
|
-
return (_jsxs("div", { className: "flood-field", "data-hide-label": hideLabel || undefined, "data-status": resolvedStatus, children: [_jsx(Label, { htmlFor: id, required: required, className: hideLabel ? "flood-visually-hidden" : undefined, children: label }), children({
|
|
240
|
+
return (_jsxs("div", { className: "flood-field", "data-hide-label": hideLabel || undefined, "data-status": resolvedStatus, children: [_jsx(Label, { ref: labelRef, htmlFor: id, required: required, className: hideLabel ? "flood-visually-hidden--focusable" : undefined, children: label }), children({
|
|
183
241
|
id,
|
|
184
242
|
"aria-describedby": description,
|
|
185
243
|
"aria-invalid": hasError || resolvedStatus === "error" ? true : undefined,
|
|
@@ -209,6 +267,57 @@ export const Inline = forwardRef(function Inline({ gap = "2", align = "center",
|
|
|
209
267
|
export const Surface = forwardRef(function Surface({ elevation = "default", padding = "6", style, className, ...props }, ref) {
|
|
210
268
|
return _jsx("div", { ...props, ref: ref, "data-flood-elevation": elevation, className: classes("flood-surface", className), style: { background: `var(--flood-sys-color-surface-${elevation})`, padding: `var(--flood-sys-space-${padding})`, ...style } });
|
|
211
269
|
});
|
|
270
|
+
/** Centered content with logical gutters; max width includes padding. @since 0.2.4 */
|
|
271
|
+
export const Container = forwardRef(function Container({ size = "lg", gutter = "6", className, style, ...props }, ref) {
|
|
272
|
+
return _jsx("div", { ...props, ref: ref, className: classes("flood-container", className), style: {
|
|
273
|
+
"--flood-container-max": size === "full" ? "none" : `var(--flood-sys-size-container-${size})`,
|
|
274
|
+
"--flood-container-gutter": `var(--flood-sys-space-${gutter})`, ...style,
|
|
275
|
+
} });
|
|
276
|
+
});
|
|
277
|
+
/** Auto-fit by default; explicit columns retain their count at every width. @since 0.2.4 */
|
|
278
|
+
export const Grid = forwardRef(function Grid({ columns, minItemWidth, gap = "4", className, style, ...props }, ref) {
|
|
279
|
+
return _jsx("div", { ...props, ref: ref, className: classes("flood-grid", className), style: {
|
|
280
|
+
gridTemplateColumns: columns ? `repeat(${columns}, minmax(0, 1fr))` : undefined,
|
|
281
|
+
"--flood-grid-min": minItemWidth, "--flood-layout-gap": `var(--flood-sys-space-${gap})`, ...style,
|
|
282
|
+
} });
|
|
283
|
+
});
|
|
284
|
+
/** Nonvisual content, with optional reveal when a descendant receives focus. @since 0.2.4 */
|
|
285
|
+
export const VisuallyHidden = forwardRef(function VisuallyHidden({ as = "span", focusable = false, className, ...props }, ref) {
|
|
286
|
+
const hiddenRef = useFocusableHidden(focusable, ref);
|
|
287
|
+
return createElement(as, { ...props, ref: hiddenRef, className: classes(focusable ? "flood-visually-hidden--focusable" : "flood-visually-hidden", className) });
|
|
288
|
+
});
|
|
289
|
+
/** Reserve media geometry. Child content may overflow rather than being clipped. @since 0.2.4 */
|
|
290
|
+
export const AspectRatio = forwardRef(function AspectRatio({ ratio = 16 / 9, className, style, children, ...props }, ref) {
|
|
291
|
+
if (!Number.isFinite(ratio) || ratio <= 0)
|
|
292
|
+
throw new RangeError("Flood AspectRatio requires a finite positive ratio");
|
|
293
|
+
return _jsx("div", { ...props, ref: ref, className: classes("flood-aspect-ratio", className), style: { aspectRatio: String(ratio), ...style }, children: _jsx("div", { className: "flood-aspect-ratio__content", children: children }) });
|
|
294
|
+
});
|
|
295
|
+
/** Native image semantics, explicit alternative text and bounded media fitting. @since 0.2.4 */
|
|
296
|
+
export const Image = forwardRef(function Image({ alt, fit = "cover", className, ...props }, ref) {
|
|
297
|
+
if (typeof alt !== "string")
|
|
298
|
+
throw new TypeError("Flood Image requires explicit alt text (empty for decorative images)");
|
|
299
|
+
return _jsx("img", { ...props, ref: ref, alt: alt, "data-fit": fit, className: classes("flood-image", className) });
|
|
300
|
+
});
|
|
301
|
+
/** Native list items supplied by the host; hidden markers retain list semantics. @since 0.2.4 */
|
|
302
|
+
export const List = forwardRef(function List({ ordered = false, start, reversed, marker = true, gap = "2", className, style, ...props }, ref) {
|
|
303
|
+
if (start !== undefined && !Number.isSafeInteger(start))
|
|
304
|
+
throw new RangeError("Flood List start requires a safe integer");
|
|
305
|
+
return createElement(ordered ? "ol" : "ul", { ...props, ref, start: ordered ? start : undefined,
|
|
306
|
+
reversed: ordered ? reversed : undefined, role: marker ? undefined : "list", "data-marker": marker,
|
|
307
|
+
className: classes("flood-list", className), style: { "--flood-layout-gap": `var(--flood-sys-space-${gap})`, ...style } });
|
|
308
|
+
});
|
|
309
|
+
/** Static empty content, not loading, an error boundary or a live announcement. @since 0.2.4 */
|
|
310
|
+
export const EmptyState = forwardRef(function EmptyState({ title, description, icon, actions, headingLevel = 2, density = "comfortable", className, ...props }, ref) {
|
|
311
|
+
if (typeof title !== "string" || !title.trim())
|
|
312
|
+
throw new Error("Flood EmptyState requires a nonempty title");
|
|
313
|
+
return _jsxs(Stack, { ...props, ref: ref, gap: "4", "data-density": density, className: classes("flood-empty-state", className), children: [hasFieldContent(icon) && _jsx("div", { className: "flood-empty-state__icon", children: _jsx(IconSlot, { size: "2xl", children: icon }) }), _jsxs(Stack, { gap: "1", children: [_jsx(Heading, { level: headingLevel, size: "section", children: title }), hasFieldContent(description) && _jsx(Text, { as: "div", size: "compact", tone: "subtle", children: description })] }), hasFieldContent(actions) && _jsx(Inline, { gap: "2", className: "flood-empty-state__actions", children: actions })] });
|
|
314
|
+
});
|
|
315
|
+
/** A structured content surface, not an interactive wrapper. @since 0.2.4 */
|
|
316
|
+
export const Card = forwardRef(function Card({ title, description, actions, footer, headingLevel = 2, density = "comfortable", elevation = "default", children, className, ...props }, ref) {
|
|
317
|
+
const present = hasFieldContent;
|
|
318
|
+
const heading = present(title), hint = present(description), action = present(actions);
|
|
319
|
+
return _jsxs(Surface, { ...props, ref: ref, elevation: elevation, padding: density === "compact" ? "4" : "6", "data-density": density, className: classes("flood-card", className), children: [(heading || hint || action) && _jsxs("div", { className: "flood-card__header", children: [(heading || hint) && _jsxs("div", { className: "flood-card__intro", children: [heading && _jsx(Heading, { level: headingLevel, size: "section", children: title }), hint && _jsx(Text, { as: "div", size: "compact", tone: "subtle", children: description })] }), action && _jsx("div", { className: "flood-card__actions", children: actions })] }), present(children) && _jsx("div", { className: "flood-card__body", children: children }), present(footer) && _jsx("div", { className: "flood-card__footer", children: footer })] });
|
|
320
|
+
});
|
|
212
321
|
/** Adapter for the host's SVG icon package; glyphs are not bundled. */
|
|
213
322
|
export const IconSlot = forwardRef(function IconSlot({ size = "md", label, className, children, ...props }, ref) {
|
|
214
323
|
return _jsx("span", { ...props, ref: ref, role: label ? "img" : undefined, "aria-label": label, "aria-hidden": label ? undefined : true, className: classes("flood-icon", `flood-icon--${size}`, className), children: children });
|
|
@@ -220,8 +329,8 @@ export const Divider = forwardRef(function Divider({ className, ...props }, ref)
|
|
|
220
329
|
return _jsx("hr", { ...props, ref: ref, className: classes("flood-divider", className) });
|
|
221
330
|
});
|
|
222
331
|
/** Selectable code with an optional host-owned action; no clipboard side effects. */
|
|
223
|
-
export const CodeSnippet = forwardRef(function CodeSnippet({ code, action, className, ...props }, ref) {
|
|
224
|
-
return (_jsxs("div", { ...props, ref: ref, className: classes("flood-code-snippet", className), children: [_jsx("code", { className: "flood-code-snippet__code", children: code }), action != null && _jsx("div", { className: "flood-code-snippet__action", children: action })] }));
|
|
332
|
+
export const CodeSnippet = forwardRef(function CodeSnippet({ code, codeDirection = "ltr", action, className, ...props }, ref) {
|
|
333
|
+
return (_jsxs("div", { ...props, ref: ref, className: classes("flood-code-snippet", className), children: [_jsx("code", { dir: codeDirection, className: "flood-code-snippet__code", children: code }), action != null && _jsx("div", { className: "flood-code-snippet__action", children: action })] }));
|
|
225
334
|
});
|
|
226
335
|
/** Native scrolling with inset overlay tracks; the ref targets the scrollable viewport. */
|
|
227
336
|
export const ScrollArea = forwardRef(function ScrollArea({ orientation = "vertical", contentInset = "scrollbar", ...props }, ref) {
|
package/dist/menu.d.ts
ADDED
package/dist/menu.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
|
|
3
|
+
import { Button, Kbd, ScrollArea } from "./index.js";
|
|
4
|
+
import { positionLayer, registerLayer, returnFocus, tabbables } from "./overlay-runtime.js";
|
|
5
|
+
const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
6
|
+
export const ActionMenu = forwardRef(function ActionMenu({ label, trigger, items, onAction, open: controlled, defaultOpen, onOpenChange, disabled, side = "bottom", align = "start", controlSize = "sm", id: explicitId, className, onKeyDown, ...props }, ref) {
|
|
7
|
+
const [local, setLocal] = useState(Boolean(defaultOpen)), open = controlled ?? local;
|
|
8
|
+
const anchor = useRef(null), panel = useRef(null);
|
|
9
|
+
const generated = useId(), id = explicitId ?? `flood-menu-${generated}`;
|
|
10
|
+
const pending = useRef(undefined);
|
|
11
|
+
const edge = useRef("first"), search = useRef({ text: "", time: 0 });
|
|
12
|
+
if (!items.some(item => item.type !== "separator"))
|
|
13
|
+
throw new Error("Flood Menu requires at least one action item");
|
|
14
|
+
if (new Set(items.map(item => item.id)).size !== items.length || items.some(item => !item.id.trim() || (item.type !== "separator" && !item.label.trim())))
|
|
15
|
+
throw new Error("Flood menu items need unique nonempty IDs and action labels");
|
|
16
|
+
const request = (next) => {
|
|
17
|
+
if (pending.current === next)
|
|
18
|
+
return;
|
|
19
|
+
pending.current = next;
|
|
20
|
+
queueMicrotask(() => { pending.current = undefined; });
|
|
21
|
+
if (controlled === undefined)
|
|
22
|
+
setLocal(next);
|
|
23
|
+
onOpenChange?.(next);
|
|
24
|
+
};
|
|
25
|
+
const current = useRef({ open, request });
|
|
26
|
+
useBrowserLayoutEffect(() => { current.current = { open, request }; });
|
|
27
|
+
const setAnchor = useCallback((node) => { anchor.current = node; }, []);
|
|
28
|
+
useImperativeHandle(ref, () => panel.current, []);
|
|
29
|
+
const buttons = () => Array.from(panel.current?.querySelectorAll('[role="menuitem"]') ?? []);
|
|
30
|
+
const focusEdge = () => { const all = buttons(); ((edge.current === "last" ? all.at(-1) : all[0]) ?? panel.current)?.focus({ preventScroll: true }); };
|
|
31
|
+
useBrowserLayoutEffect(() => {
|
|
32
|
+
if (!open || !panel.current)
|
|
33
|
+
return;
|
|
34
|
+
const element = panel.current, document = element.ownerDocument, view = document.defaultView;
|
|
35
|
+
let alive = true, frame = 0, unregister = () => { };
|
|
36
|
+
const start = () => {
|
|
37
|
+
if (!alive || !anchor.current?.isConnected)
|
|
38
|
+
return;
|
|
39
|
+
const parent = anchor.current.closest("dialog");
|
|
40
|
+
if (parent && !parent.open) {
|
|
41
|
+
frame = view.requestAnimationFrame(start);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (typeof element.showPopover !== "function")
|
|
45
|
+
throw new Error("Flood Menu requires the native Popover API");
|
|
46
|
+
element.style.visibility = "hidden";
|
|
47
|
+
element.showPopover();
|
|
48
|
+
positionLayer(anchor.current, element, side, align);
|
|
49
|
+
unregister = registerLayer(document, { panel: element, anchor: () => anchor.current, close: () => current.current.request(false) });
|
|
50
|
+
search.current = { text: "", time: 0 };
|
|
51
|
+
focusEdge();
|
|
52
|
+
const update = () => { if (!alive)
|
|
53
|
+
return; if (anchor.current?.isConnected)
|
|
54
|
+
positionLayer(anchor.current, element, side, align); frame = view.requestAnimationFrame(update); };
|
|
55
|
+
frame = view.requestAnimationFrame(update);
|
|
56
|
+
};
|
|
57
|
+
start();
|
|
58
|
+
const toggle = (event) => {
|
|
59
|
+
if (!alive || event.newState !== "closed" || element.matches(":popover-open") || !current.current.open)
|
|
60
|
+
return;
|
|
61
|
+
current.current.request(false);
|
|
62
|
+
queueMicrotask(() => { if (alive && current.current.open && element.isConnected && !element.matches(":popover-open") && anchor.current?.isConnected) {
|
|
63
|
+
element.showPopover();
|
|
64
|
+
positionLayer(anchor.current, element, side, align);
|
|
65
|
+
focusEdge();
|
|
66
|
+
} });
|
|
67
|
+
};
|
|
68
|
+
element.addEventListener("toggle", toggle);
|
|
69
|
+
return () => { alive = false; view.cancelAnimationFrame(frame); unregister(); element.removeEventListener("toggle", toggle); returnFocus(element, anchor.current); if (element.matches(":popover-open"))
|
|
70
|
+
element.hidePopover(); };
|
|
71
|
+
}, [open, side, align]);
|
|
72
|
+
useBrowserLayoutEffect(() => {
|
|
73
|
+
if (open && panel.current?.matches(":popover-open") && panel.current.ownerDocument.activeElement === panel.current.ownerDocument.body)
|
|
74
|
+
focusEdge();
|
|
75
|
+
}, [items, open]);
|
|
76
|
+
const triggerProps = { ref: setAnchor, type: "button", disabled, "aria-haspopup": "menu", "aria-expanded": open, "aria-controls": id,
|
|
77
|
+
onClick: () => { edge.current = "first"; request(!open); },
|
|
78
|
+
onKeyDown: event => { if (event.defaultPrevented || disabled || event.altKey || event.ctrlKey || event.metaKey || !["ArrowDown", "ArrowUp"].includes(event.key))
|
|
79
|
+
return; event.preventDefault(); edge.current = event.key === "ArrowUp" ? "last" : "first"; if (open && panel.current?.matches(":popover-open"))
|
|
80
|
+
focusEdge();
|
|
81
|
+
else
|
|
82
|
+
request(true); },
|
|
83
|
+
};
|
|
84
|
+
const hasIcons = items.some(item => item.type !== "separator" && item.icon != null);
|
|
85
|
+
return _jsxs(_Fragment, { children: [trigger(triggerProps), _jsx("div", { ...props, ref: panel, id: id, popover: "manual", role: "menu", "aria-label": label, "aria-orientation": "vertical", tabIndex: -1, className: ["flood-overlay-panel", "flood-menu", className].filter(Boolean).join(" "), onKeyDown: event => {
|
|
86
|
+
onKeyDown?.(event);
|
|
87
|
+
if (event.defaultPrevented)
|
|
88
|
+
return;
|
|
89
|
+
const all = buttons(), index = all.indexOf(event.target);
|
|
90
|
+
if (event.key === "Tab") {
|
|
91
|
+
const button = anchor.current;
|
|
92
|
+
if (button) {
|
|
93
|
+
const scope = button.closest("dialog[open]") ?? button.ownerDocument.body;
|
|
94
|
+
const stops = tabbables(scope).filter(node => !panel.current?.contains(node));
|
|
95
|
+
const at = stops.indexOf(button), target = event.shiftKey ? button : at >= 0 ? stops[at + 1] : undefined;
|
|
96
|
+
if (target) {
|
|
97
|
+
event.preventDefault();
|
|
98
|
+
target.focus();
|
|
99
|
+
}
|
|
100
|
+
else
|
|
101
|
+
button.focus();
|
|
102
|
+
}
|
|
103
|
+
request(false);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (event.altKey || event.ctrlKey || event.metaKey || event.nativeEvent.isComposing)
|
|
107
|
+
return;
|
|
108
|
+
let next = event.key === "Home" ? 0 : event.key === "End" ? all.length - 1 : event.key === "ArrowDown" ? (index + 1) % all.length : event.key === "ArrowUp" ? (index - 1 + all.length) % all.length : -1;
|
|
109
|
+
if (["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key))
|
|
110
|
+
search.current = { text: "", time: 0 };
|
|
111
|
+
if (event.key.length === 1 && event.key !== " ") {
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
search.current = { text: (now - search.current.time < 700 ? search.current.text : "") + event.key.toLocaleLowerCase(), time: now };
|
|
114
|
+
const text = Array.from(search.current.text).every(char => char === search.current.text[0]) ? search.current.text[0] : search.current.text;
|
|
115
|
+
const start = text.length === 1 ? index + 1 : Math.max(0, index);
|
|
116
|
+
next = Array.from({ length: all.length }, (_, offset) => (start + offset) % all.length).find(candidate => all[candidate].dataset.label?.toLocaleLowerCase().startsWith(text)) ?? -1;
|
|
117
|
+
event.preventDefault();
|
|
118
|
+
}
|
|
119
|
+
if (next >= 0 && all[next]) {
|
|
120
|
+
event.preventDefault();
|
|
121
|
+
all[next].focus({ preventScroll: true });
|
|
122
|
+
all[next].scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
123
|
+
}
|
|
124
|
+
}, children: open && _jsx(ScrollArea, { orientation: "vertical", role: "group", tabIndex: -1, className: "flood-menu__viewport", children: items.map(item => item.type === "separator" ? _jsx("div", { role: "separator", className: "flood-menu__separator" }, item.id) :
|
|
125
|
+
_jsxs(Button, { role: "menuitem", tabIndex: -1, variant: "ghost", size: controlSize, "aria-label": item.label, "aria-disabled": item.disabled || undefined, "data-label": item.label, "data-tone": item.tone, className: "flood-menu__item", onPointerMove: event => { if (event.pointerType !== "touch" && event.currentTarget.ownerDocument.activeElement !== event.currentTarget)
|
|
126
|
+
event.currentTarget.focus({ preventScroll: true }); }, onClick: event => { if (item.disabled) {
|
|
127
|
+
event.preventDefault();
|
|
128
|
+
return;
|
|
129
|
+
} request(false); onAction?.(item.id); }, children: [hasIcons && _jsx("span", { className: "flood-menu__icon", "aria-hidden": "true", children: item.icon }), _jsx("span", { className: "flood-menu__label", children: item.label }), item.shortcut && _jsx("span", { className: "flood-menu__shortcut", "aria-hidden": "true", children: _jsx(Kbd, { children: item.shortcut }) })] }, item.id)) }) })] });
|
|
130
|
+
});
|
package/dist/navigation.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef, useEffect, useId, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
|
|
3
|
-
import {
|
|
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
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) {
|
|
@@ -31,7 +31,7 @@ export const TabNavigation = forwardRef(function TabNavigation({ label, items, v
|
|
|
31
31
|
}, [selected, id, items]);
|
|
32
32
|
return _jsxs("div", { ...props, ref: root, id: id, className: ["flood-tabs", className].filter(Boolean).join(" "), "data-orientation": orientation, onKeyDown: event => {
|
|
33
33
|
onKeyDown?.(event);
|
|
34
|
-
if (event.defaultPrevented)
|
|
34
|
+
if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey || event.nativeEvent.isComposing)
|
|
35
35
|
return;
|
|
36
36
|
const buttons = Array.from(list.current?.querySelectorAll(':scope > button[role="tab"]:not(:disabled)') ?? []), index = buttons.indexOf(event.target);
|
|
37
37
|
if (index < 0)
|
|
@@ -45,22 +45,22 @@ export const TabNavigation = forwardRef(function TabNavigation({ label, items, v
|
|
|
45
45
|
if (activationMode === "automatic")
|
|
46
46
|
select(buttons[next].dataset.value);
|
|
47
47
|
}
|
|
48
|
-
}, children: [_jsx(
|
|
49
|
-
setFocused(undefined); }, children: items.map((item, index) => _jsxs("
|
|
48
|
+
}, children: [_jsx(ScrollArea, { orientation: orientation, tabIndex: -1, className: "flood-tabs__scroll", children: _jsx("div", { ref: list, role: "tablist", "aria-label": label, "aria-orientation": orientation, className: "flood-tabs__list", onBlur: event => { if (!event.currentTarget.contains(event.relatedTarget))
|
|
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
52
|
export const BreadcrumbNavigation = forwardRef(function BreadcrumbNavigation({ items, label = "Breadcrumb", separator = "/", className, ...props }, ref) {
|
|
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(
|
|
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
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) {
|
|
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;
|
|
59
|
-
const shared = { className:
|
|
59
|
+
const shared = { className: "flood-page", "aria-label": name, "aria-current": current && !unavailable ? "page" : undefined };
|
|
60
60
|
if (getHref)
|
|
61
|
-
return inactive ? _jsx("span", { ...shared, role: "link", "aria-disabled": "true", children: text }) : _jsx(
|
|
61
|
+
return inactive ? _jsx("span", { ...shared, className: `flood-button flood-button--subtle flood-button--${controlSize} flood-page`, role: "link", "aria-disabled": "true", children: text }) : _jsx(Link, { ...shared, variant: "subtle", size: controlSize, href: getHref(target), onClick: event => { if (!event.defaultPrevented && event.button === 0 && !event.ctrlKey && !event.metaKey && !event.shiftKey && !event.altKey && !current)
|
|
62
62
|
onPageChange?.(target); }, children: text });
|
|
63
|
-
return _jsx(
|
|
63
|
+
return _jsx(Button, { ...shared, variant: "subtle", size: controlSize, type: "button", disabled: inactive, onClick: () => { if (!current)
|
|
64
64
|
onPageChange?.(target); }, children: text });
|
|
65
65
|
};
|
|
66
66
|
const arrow = (previous) => _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", focusable: "false", children: _jsx("path", { d: previous ? "m15 5-7 7 7 7" : "m9 5 7 7-7 7" }) });
|
package/dist/overlay-runtime.js
CHANGED
|
@@ -15,11 +15,17 @@ export function placeOverlay(anchor, size, viewport, side, align, gap, rtl = fal
|
|
|
15
15
|
return { left, top, side };
|
|
16
16
|
}
|
|
17
17
|
export function tabbables(root) {
|
|
18
|
-
const candidates = Array.from(root.querySelectorAll('a[href],button,input,select,textarea,[tabindex],[contenteditable
|
|
18
|
+
const candidates = Array.from(root.querySelectorAll('a[href],button,input,select,textarea,[tabindex],[contenteditable]:not([contenteditable="false"]),details > summary:first-of-type,audio[controls],video[controls],iframe')).filter(node => {
|
|
19
|
+
// Editing roots and native media may report -1 without an explicit tabindex
|
|
20
|
+
// while still participating in sequential focus. Nested editing hosts do not.
|
|
21
|
+
const implicit = !node.hasAttribute("tabindex") && ((node.isContentEditable && !node.parentElement?.isContentEditable) || node.matches("audio[controls],video[controls]"));
|
|
22
|
+
return (node.tabIndex >= 0 || implicit) && !node.matches(':disabled,input[type="hidden"]') && !node.closest('[hidden],[inert]') && node.getClientRects().length > 0 && getComputedStyle(node).visibility !== "hidden";
|
|
23
|
+
});
|
|
19
24
|
return candidates.filter(node => {
|
|
20
|
-
if (
|
|
25
|
+
if (node.localName !== "input" || node.type !== "radio" || !node.name)
|
|
21
26
|
return true;
|
|
22
|
-
const
|
|
27
|
+
const radio = node;
|
|
28
|
+
const group = candidates.filter((other) => other.localName === "input" && other.type === "radio" && other.name === radio.name && other.form === radio.form);
|
|
23
29
|
return node === (group.find(other => other.checked) ?? group[0]);
|
|
24
30
|
}).sort((a, b) => (a.tabIndex > 0 ? a.tabIndex : Infinity) - (b.tabIndex > 0 ? b.tabIndex : Infinity));
|
|
25
31
|
}
|
|
@@ -31,7 +37,7 @@ export function registerLayer(document, layer) {
|
|
|
31
37
|
stacks.set(document, layers);
|
|
32
38
|
}
|
|
33
39
|
layers.push(layer);
|
|
34
|
-
const top = () => layers.filter(item => item.panel.isConnected && (item.panel
|
|
40
|
+
const top = () => layers.filter(item => item.panel.isConnected && (item.panel.localName === "dialog" ? item.panel.open : item.panel.matches(":popover-open"))).at(-1);
|
|
35
41
|
const key = (event) => { if (event.defaultPrevented || event.key !== "Escape" || top() !== layer)
|
|
36
42
|
return; event.preventDefault(); event.stopPropagation(); layer.close(); };
|
|
37
43
|
const pointer = (event) => {
|
package/dist/overlays.js
CHANGED
|
@@ -6,8 +6,17 @@ import { mergeDescriptions } from "./field-state.js";
|
|
|
6
6
|
const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
7
7
|
function useCurrent(value) { const ref = useRef(value); useBrowserLayoutEffect(() => { ref.current = value; }); return ref; }
|
|
8
8
|
function focusContent(panel, preferred) {
|
|
9
|
-
const
|
|
10
|
-
|
|
9
|
+
const available = (node) => Boolean(node?.isConnected && panel.contains(node) && !node.matches(":disabled") && !node.closest("[hidden],[inert]") && node.getClientRects().length && getComputedStyle(node).visibility !== "hidden");
|
|
10
|
+
const candidates = [preferred, ...panel.querySelectorAll('[autofocus],[data-flood-autofocus]'), ...tabbables(panel.querySelector(".flood-overlay-body") ?? panel), ...tabbables(panel), panel];
|
|
11
|
+
for (const target of candidates) {
|
|
12
|
+
if (!available(target))
|
|
13
|
+
continue;
|
|
14
|
+
const previous = panel.ownerDocument.activeElement;
|
|
15
|
+
target.focus({ preventScroll: true });
|
|
16
|
+
// A host focus handler may intentionally redirect focus elsewhere.
|
|
17
|
+
if (panel.ownerDocument.activeElement === target || panel.ownerDocument.activeElement !== previous)
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
11
20
|
}
|
|
12
21
|
function OverlayClose({ label, onClick }) {
|
|
13
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" }) }) });
|
|
@@ -71,7 +80,7 @@ export const AnchoredOverlay = forwardRef(function AnchoredOverlay(allProps, for
|
|
|
71
80
|
positionLayer(anchor.current, element, side, align); frame = view.requestAnimationFrame(update); };
|
|
72
81
|
frame = view.requestAnimationFrame(update);
|
|
73
82
|
};
|
|
74
|
-
|
|
83
|
+
start();
|
|
75
84
|
const toggle = (event) => {
|
|
76
85
|
if (!alive || event.newState !== "closed" || element.matches(":popover-open") || !current.current.open)
|
|
77
86
|
return;
|
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, ...props }, forwardedRef) {
|
|
87
|
+
export const OverlayScrollArea = 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);
|
|
@@ -216,5 +216,20 @@ export const OverlayScrollArea = forwardRef(function OverlayScrollArea({ orienta
|
|
|
216
216
|
"--flood-scroll-content-block": measurements.horizontal.visible ? "calc(var(--flood-sys-scrollbar-size) + 2 * var(--flood-sys-scrollbar-inset))" : "0px",
|
|
217
217
|
} : undefined;
|
|
218
218
|
const named = props["aria-label"] || props["aria-labelledby"];
|
|
219
|
-
return _jsxs("div", { className: "flood-scroll-area", style: rootStyle, children: [_jsx("div", { ...props, id: props.id ?? `flood-scroll-${id}`, ref: viewport, tabIndex: tabIndex, role: role ?? (named ? "region" : undefined), className: ["flood-scroll-area__viewport", className].filter(Boolean).join(" "), style: style,
|
|
219
|
+
return _jsxs("div", { className: "flood-scroll-area", hidden: props.hidden, dir: props.dir, lang: props.lang, style: rootStyle, children: [_jsx("div", { ...props, id: props.id ?? `flood-scroll-${id}`, ref: viewport, tabIndex: tabIndex, role: role ?? (named ? "region" : undefined), className: ["flood-scroll-area__viewport", className].filter(Boolean).join(" "), style: style, onFocus: event => {
|
|
220
|
+
onFocus?.(event);
|
|
221
|
+
if (event.defaultPrevented || event.target === event.currentTarget)
|
|
222
|
+
return;
|
|
223
|
+
const target = event.target, node = event.currentTarget;
|
|
224
|
+
if (node.ownerDocument.activeElement !== target)
|
|
225
|
+
return;
|
|
226
|
+
const layer = target.closest("[popover], dialog[open]");
|
|
227
|
+
if (layer && node.contains(layer))
|
|
228
|
+
return;
|
|
229
|
+
const box = target.getBoundingClientRect(), bounds = node.getBoundingClientRect();
|
|
230
|
+
const outsideX = orientation !== "vertical" && (box.left < bounds.left || box.right > bounds.right);
|
|
231
|
+
const outsideY = orientation !== "horizontal" && (box.top < bounds.top || box.bottom > bounds.bottom);
|
|
232
|
+
if (outsideX || outsideY)
|
|
233
|
+
target.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
234
|
+
}, "data-orientation": orientation, "data-scrollbars": measurements.enhanced ? "overlay" : undefined, children: _jsx("div", { className: "flood-scroll-area__content", ref: content, style: contentStyle, children: children }) }), renderBar("vertical"), renderBar("horizontal")] });
|
|
220
235
|
});
|