@flood-md/react 0.2.10 → 0.2.11
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 +239 -1
- package/dist/app-shell.d.ts +3 -0
- package/dist/app-shell.js +66 -0
- package/dist/bulk-action-bar.d.ts +3 -0
- package/dist/bulk-action-bar.js +17 -0
- package/dist/collection-toolbar.d.ts +3 -0
- package/dist/collection-toolbar.js +26 -0
- package/dist/collection-view-switcher.d.ts +3 -0
- package/dist/collection-view-switcher.js +19 -0
- package/dist/combobox.d.ts +6 -1
- package/dist/combobox.js +77 -29
- package/dist/command-list.d.ts +5 -1
- package/dist/command-list.js +2 -2
- package/dist/data-display.js +1 -1
- package/dist/data-table.js +44 -9
- package/dist/date-picker.js +2 -2
- package/dist/date-range-picker.js +2 -2
- package/dist/date-time-picker.js +2 -2
- package/dist/date-time-range-picker.js +2 -2
- package/dist/disclosure.d.ts +2 -1
- package/dist/disclosure.js +4 -4
- package/dist/filter-condition.d.ts +3 -0
- package/dist/filter-condition.js +21 -0
- package/dist/filter-group.d.ts +2 -0
- package/dist/filter-group.js +62 -0
- package/dist/foundations.css +752 -76
- package/dist/index.d.ts +399 -3
- package/dist/index.js +61 -7
- package/dist/menu.js +5 -4
- package/dist/menubar.d.ts +2 -0
- package/dist/menubar.js +85 -0
- package/dist/navigation-group.d.ts +3 -0
- package/dist/navigation-group.js +13 -0
- package/dist/navigation-item.d.ts +3 -0
- package/dist/navigation-item.js +68 -0
- package/dist/navigation-layout.d.ts +2 -1
- package/dist/navigation-layout.js +12 -4
- package/dist/navigation-tree.js +51 -7
- package/dist/navigation.js +2 -2
- package/dist/overflow-actions.d.ts +2 -0
- package/dist/overflow-actions.js +75 -0
- package/dist/overlay-runtime.d.ts +3 -0
- package/dist/overlay-runtime.js +35 -1
- package/dist/overlays.js +32 -16
- package/dist/select.js +4 -1
- package/dist/sidebar.d.ts +2 -0
- package/dist/sidebar.js +58 -0
- package/dist/toolbar.js +2 -2
- package/package.json +2 -2
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
|
|
3
|
+
import { Button, IconSlot, Menu, Toolbar } from "./index.js";
|
|
4
|
+
const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
5
|
+
export const AdaptiveActions = /* @__PURE__ */ forwardRef(function AdaptiveActions({ label, items, onAction, moreLabel = "More actions", size = "md", variant = "subtle", shape = "pill", containerVariant = "contained", disabled = false, leading, trailing, maxVisibleActions, className, ...props }, ref) {
|
|
6
|
+
if (!label.trim() || !moreLabel.trim() || items.some(item => !item.id.trim() || !item.label.trim() || (item.priority !== undefined && !Number.isFinite(item.priority))) || new Set(items.map(item => item.id)).size !== items.length)
|
|
7
|
+
throw new Error("Flood OverflowActions requires unique named actions and finite priorities");
|
|
8
|
+
if (maxVisibleActions !== undefined && (!Number.isInteger(maxVisibleActions) || maxVisibleActions < 0))
|
|
9
|
+
throw new Error("Flood OverflowActions maxVisibleActions must be a nonnegative integer");
|
|
10
|
+
const root = useRef(null), measure = useRef(null), more = useRef(null);
|
|
11
|
+
const before = useRef(null), after = useRef(null);
|
|
12
|
+
const menuAnchor = useRef(null);
|
|
13
|
+
const setMore = useCallback((node) => { more.current = node; menuAnchor.current?.(node); }, []);
|
|
14
|
+
useImperativeHandle(ref, () => root.current, []);
|
|
15
|
+
const [visibleIds, setVisibleIds] = useState(() => items.map(item => item.id));
|
|
16
|
+
const [open, setOpen] = useState(false), focusAfter = useRef(false);
|
|
17
|
+
const visible = items.filter(item => visibleIds.includes(item.id)), hidden = items.filter(item => !visibleIds.includes(item.id));
|
|
18
|
+
const action = (item, measuring = false) => _jsxs(Button, { size: size, variant: variant, tone: item.tone, disabled: disabled || item.disabled, "data-overflow-action": measuring ? undefined : item.id, "data-measure-action": measuring ? item.id : undefined, tabIndex: measuring ? -1 : undefined, onClick: measuring ? undefined : () => onAction?.(item.id), children: [item.icon && _jsx(IconSlot, { size: size, children: item.icon }), item.label] }, item.id);
|
|
19
|
+
const overflow = (measuring = false) => _jsxs(Button, { size: size, variant: variant, disabled: disabled, tabIndex: measuring ? -1 : undefined, "data-measure-more": measuring || undefined, children: [moreLabel, _jsx(IconSlot, { size: size, children: _jsxs("svg", { viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true", children: [_jsx("circle", { cx: "5", cy: "12", r: "1.5" }), _jsx("circle", { cx: "12", cy: "12", r: "1.5" }), _jsx("circle", { cx: "19", cy: "12", r: "1.5" })] }) })] });
|
|
20
|
+
useBrowserLayoutEffect(() => {
|
|
21
|
+
const node = root.current, measured = measure.current, view = node?.ownerDocument.defaultView;
|
|
22
|
+
if (!node || !measured || !view)
|
|
23
|
+
return;
|
|
24
|
+
const update = () => {
|
|
25
|
+
const widths = new Map([...measured.querySelectorAll('[data-measure-action]')].map(item => [item.dataset.measureAction, item.getBoundingClientRect().width]));
|
|
26
|
+
const bar = node.querySelector('[role="toolbar"]'), gap = bar ? parseFloat(view.getComputedStyle(bar).columnGap) || 0 : 0;
|
|
27
|
+
const barStyle = bar ? view.getComputedStyle(bar) : null;
|
|
28
|
+
const rootStyle = view.getComputedStyle(node);
|
|
29
|
+
const available = Math.max(0, node.clientWidth - parseFloat(rootStyle.paddingLeft) - parseFloat(rootStyle.paddingRight) - (barStyle ? parseFloat(barStyle.paddingLeft) + parseFloat(barStyle.paddingRight) + parseFloat(barStyle.borderLeftWidth) + parseFloat(barStyle.borderRightWidth) : 0)), extra = measured.querySelector('[data-measure-more]')?.getBoundingClientRect().width ?? 0;
|
|
30
|
+
const fixed = [before.current, after.current].filter((item) => item !== null);
|
|
31
|
+
const fixedWidth = fixed.reduce((sum, item) => sum + item.getBoundingClientRect().width, 0);
|
|
32
|
+
const kept = new Set(items.map(item => item.id));
|
|
33
|
+
const width = (withMore) => fixedWidth + items.filter(item => kept.has(item.id)).reduce((sum, item) => sum + (widths.get(item.id) ?? 0), 0) + (withMore ? extra : 0) + Math.max(0, kept.size + fixed.length + (withMore ? 1 : 0) - 1) * gap;
|
|
34
|
+
const order = items.map((item, index) => ({ item, index })).sort((a, b) => (a.item.priority ?? 0) - (b.item.priority ?? 0) || b.index - a.index);
|
|
35
|
+
for (const { item } of order) {
|
|
36
|
+
if (kept.size <= (maxVisibleActions ?? items.length) && width(kept.size < items.length) <= available)
|
|
37
|
+
break;
|
|
38
|
+
kept.delete(item.id);
|
|
39
|
+
}
|
|
40
|
+
const next = items.filter(item => kept.has(item.id)).map(item => item.id);
|
|
41
|
+
const active = node.ownerDocument.activeElement;
|
|
42
|
+
const activeId = active?.dataset.overflowAction;
|
|
43
|
+
if ((activeId && !kept.has(activeId)) || (!items.some(item => !kept.has(item.id)) && more.current?.contains(active)))
|
|
44
|
+
focusAfter.current = true;
|
|
45
|
+
setVisibleIds(previous => previous.join("\0") === next.join("\0") ? previous : next);
|
|
46
|
+
};
|
|
47
|
+
update();
|
|
48
|
+
const observer = view.ResizeObserver ? new view.ResizeObserver(update) : undefined;
|
|
49
|
+
observer?.observe(node);
|
|
50
|
+
observer?.observe(measured);
|
|
51
|
+
if (before.current)
|
|
52
|
+
observer?.observe(before.current);
|
|
53
|
+
if (after.current)
|
|
54
|
+
observer?.observe(after.current);
|
|
55
|
+
let alive = true;
|
|
56
|
+
void node.ownerDocument.fonts?.ready.then(() => { if (alive)
|
|
57
|
+
update(); });
|
|
58
|
+
return () => { alive = false; observer?.disconnect(); };
|
|
59
|
+
}, [items, size, variant, moreLabel, disabled, leading, trailing, maxVisibleActions]);
|
|
60
|
+
useBrowserLayoutEffect(() => {
|
|
61
|
+
if (!hidden.length && open) {
|
|
62
|
+
setOpen(false);
|
|
63
|
+
focusAfter.current = true;
|
|
64
|
+
}
|
|
65
|
+
if (focusAfter.current) {
|
|
66
|
+
focusAfter.current = false;
|
|
67
|
+
(more.current ?? root.current?.querySelector('[data-overflow-action]:not(:disabled)'))?.focus({ preventScroll: true });
|
|
68
|
+
}
|
|
69
|
+
}, [visibleIds, hidden.length, open]);
|
|
70
|
+
return _jsxs("div", { ...props, ref: root, className: ["flood-overflow-actions", className].filter(Boolean).join(" "), children: [_jsxs(Toolbar, { label: label, shape: shape, variant: containerVariant, className: "flood-overflow-actions__bar", children: [leading != null && _jsx("div", { ref: before, className: "flood-overflow-actions__leading", children: leading }), visible.map(item => action(item)), hidden.length > 0 && _jsx(Menu, { label: moreLabel, items: hidden, open: open, onOpenChange: setOpen, onAction: onAction, disabled: disabled, controlSize: size, trigger: trigger => {
|
|
71
|
+
menuAnchor.current = trigger.ref;
|
|
72
|
+
return _jsxs(Button, { ...trigger, ref: setMore, size: size, variant: variant, children: [moreLabel, _jsx(IconSlot, { size: size, children: _jsxs("svg", { viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true", children: [_jsx("circle", { cx: "5", cy: "12", r: "1.5" }), _jsx("circle", { cx: "12", cy: "12", r: "1.5" }), _jsx("circle", { cx: "19", cy: "12", r: "1.5" })] }) })] });
|
|
73
|
+
} }), trailing != null && _jsx("div", { ref: after, className: "flood-overflow-actions__trailing", children: trailing })] }), _jsxs("div", { ref: node => { measure.current = node; if (node)
|
|
74
|
+
node.inert = true; }, className: "flood-overflow-actions__measure", "aria-hidden": "true", children: [items.map(item => action(item, true)), overflow(true)] })] });
|
|
75
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type RefObject } from "react";
|
|
1
2
|
export type Side = "top" | "bottom" | "left" | "right";
|
|
2
3
|
export type Align = "start" | "center" | "end";
|
|
3
4
|
type Rect = {
|
|
@@ -35,4 +36,6 @@ export declare function registerLayer(document: Document, layer: Layer): () => v
|
|
|
35
36
|
export declare function lockScroll(document: Document): () => void;
|
|
36
37
|
export declare function positionLayer(anchor: HTMLElement, panel: HTMLElement, side: Side, align: Align): void;
|
|
37
38
|
export declare function returnFocus(panel: HTMLElement, anchor: HTMLElement | null): void;
|
|
39
|
+
/** Retain pixels for a CSS exit after the native layer has released focus. */
|
|
40
|
+
export declare function useLayerExitContent(panel: RefObject<HTMLElement | null>, open: boolean, enabled?: boolean): boolean;
|
|
38
41
|
export {};
|
package/dist/overlay-runtime.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect, useState } from "react";
|
|
1
2
|
/** Pure collision handling in layout-viewport coordinates. */
|
|
2
3
|
export function placeOverlay(anchor, size, viewport, side, align, gap, rtl = false) {
|
|
3
4
|
const edge = { left: viewport.left + gap, right: viewport.left + viewport.width - gap, top: viewport.top + gap, bottom: viewport.top + viewport.height - gap };
|
|
@@ -115,7 +116,12 @@ export function positionLayer(anchor, panel, side, align) {
|
|
|
115
116
|
const gap = parseFloat(getComputedStyle(panel).columnGap) || 8;
|
|
116
117
|
panel.style.setProperty("--flood-overlay-available-width", `${Math.max(0, viewport.width - 2 * gap)}px`);
|
|
117
118
|
panel.style.setProperty("--flood-overlay-available-height", `${Math.max(0, viewport.height - 2 * gap)}px`);
|
|
118
|
-
const
|
|
119
|
+
const bounds = anchor.getBoundingClientRect(), anchorWidth = `${bounds.width}px`, anchorHeight = `${bounds.height}px`;
|
|
120
|
+
if (panel.style.getPropertyValue("--flood-overlay-anchor-width") !== anchorWidth)
|
|
121
|
+
panel.style.setProperty("--flood-overlay-anchor-width", anchorWidth);
|
|
122
|
+
if (panel.style.getPropertyValue("--flood-overlay-anchor-height") !== anchorHeight)
|
|
123
|
+
panel.style.setProperty("--flood-overlay-anchor-height", anchorHeight);
|
|
124
|
+
const position = placeOverlay(bounds, panel.getBoundingClientRect(), viewport, side, align, gap, getComputedStyle(anchor).direction === "rtl");
|
|
119
125
|
const left = `${position.left}px`, top = `${position.top}px`;
|
|
120
126
|
if (panel.style.left !== left)
|
|
121
127
|
panel.style.left = left;
|
|
@@ -129,3 +135,31 @@ export function returnFocus(panel, anchor) {
|
|
|
129
135
|
if (anchor?.isConnected && !anchor.matches(":disabled") && (panel.contains(active) || active === panel.ownerDocument.body))
|
|
130
136
|
anchor.focus({ preventScroll: true });
|
|
131
137
|
}
|
|
138
|
+
/** Retain pixels for a CSS exit after the native layer has released focus. */
|
|
139
|
+
export function useLayerExitContent(panel, open, enabled = true) {
|
|
140
|
+
const [retained, setRetained] = useState(false);
|
|
141
|
+
useLayerLayoutEffect(() => {
|
|
142
|
+
const node = panel.current;
|
|
143
|
+
if (!enabled || !node) {
|
|
144
|
+
setRetained(false);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (open) {
|
|
148
|
+
setRetained(true);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
node.inert = true;
|
|
152
|
+
const transitions = node.getAnimations().filter(animation => "transitionProperty" in animation && animation.transitionProperty === "opacity");
|
|
153
|
+
let active = true;
|
|
154
|
+
if (!transitions.length)
|
|
155
|
+
setRetained(false);
|
|
156
|
+
else
|
|
157
|
+
void Promise.allSettled(transitions.map(animation => animation.finished)).then(() => {
|
|
158
|
+
if (active)
|
|
159
|
+
setRetained(false);
|
|
160
|
+
});
|
|
161
|
+
return () => { active = false; node.inert = false; };
|
|
162
|
+
}, [panel, open, enabled]);
|
|
163
|
+
return open || enabled && retained;
|
|
164
|
+
}
|
|
165
|
+
const useLayerLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
package/dist/overlays.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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
3
|
import { IconButton, ScrollArea, VisuallyHidden } from "./index.js";
|
|
4
|
-
import { positionLayer, registerLayer, returnFocus, lockScroll, tabbables } from "./overlay-runtime.js";
|
|
4
|
+
import { positionLayer, registerLayer, returnFocus, lockScroll, tabbables, useLayerExitContent } from "./overlay-runtime.js";
|
|
5
5
|
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; }
|
|
@@ -19,7 +19,7 @@ function focusContent(panel, preferred) {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
function OverlayClose({ label, onClick }) {
|
|
22
|
-
return _jsx(IconButton, { label: label, size: "sm", variant: "
|
|
22
|
+
return _jsx(IconButton, { label: label, size: "sm", variant: "ghost", 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
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;
|
|
@@ -38,7 +38,9 @@ export const AnchoredOverlay = /* @__PURE__ */ forwardRef(function AnchoredOverl
|
|
|
38
38
|
suppressed.current = true;
|
|
39
39
|
overPanel.current = false;
|
|
40
40
|
} request(false); };
|
|
41
|
-
const
|
|
41
|
+
const focusOnOpen = pop.focusOnOpen ?? true;
|
|
42
|
+
const current = useCurrent({ close, request, disabled, open, focusOnOpen, initialFocusRef: pop.initialFocusRef, positionReference: pop.positionReference });
|
|
43
|
+
const previousFocusOnOpen = useRef(focusOnOpen);
|
|
42
44
|
const positionReference = () => { const node = current.current.positionReference?.current; return node?.isConnected && node.ownerDocument === panel.current?.ownerDocument ? node : anchor.current; };
|
|
43
45
|
useImperativeHandle(forwardedRef, () => panel.current, []);
|
|
44
46
|
useEffect(() => () => clearTimeout(timer.current), []);
|
|
@@ -74,7 +76,7 @@ export const AnchoredOverlay = /* @__PURE__ */ forwardRef(function AnchoredOverl
|
|
|
74
76
|
element.showPopover();
|
|
75
77
|
positionLayer(positionReference(), element, side, align);
|
|
76
78
|
unregister = registerLayer(document, { panel: element, anchor: () => anchor.current, close: () => current.current.close(), tooltip: kind === "tooltip" });
|
|
77
|
-
if (kind === "popover")
|
|
79
|
+
if (kind === "popover" && current.current.focusOnOpen)
|
|
78
80
|
focusContent(element, current.current.initialFocusRef?.current);
|
|
79
81
|
const update = () => { if (!alive)
|
|
80
82
|
return; if (anchor.current?.isConnected)
|
|
@@ -89,30 +91,40 @@ export const AnchoredOverlay = /* @__PURE__ */ forwardRef(function AnchoredOverl
|
|
|
89
91
|
queueMicrotask(() => { if (alive && current.current.open && element.isConnected && !element.matches(":popover-open") && anchor.current?.isConnected) {
|
|
90
92
|
element.showPopover();
|
|
91
93
|
positionLayer(positionReference(), element, side, align);
|
|
92
|
-
if (kind === "popover")
|
|
94
|
+
if (kind === "popover" && current.current.focusOnOpen)
|
|
93
95
|
focusContent(element, current.current.initialFocusRef?.current);
|
|
94
96
|
} });
|
|
95
97
|
};
|
|
96
98
|
element.addEventListener("toggle", toggle);
|
|
97
|
-
return () => { alive = false; view.cancelAnimationFrame(frame); unregister(); element.removeEventListener("toggle", toggle); if (kind === "popover")
|
|
99
|
+
return () => { alive = false; view.cancelAnimationFrame(frame); unregister(); element.removeEventListener("toggle", toggle); if (kind === "popover" && (previousFocusOnOpen.current || element.contains(document.activeElement)))
|
|
98
100
|
returnFocus(element, anchor.current); if (element.matches(":popover-open"))
|
|
99
101
|
element.hidePopover(); };
|
|
100
102
|
}, [open, side, align, kind]);
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
103
|
+
const titleOnly = kind === "popover" && pop.chrome === "title-only";
|
|
104
|
+
const showContent = useLayerExitContent(panel, open, titleOnly);
|
|
105
|
+
useBrowserLayoutEffect(() => {
|
|
106
|
+
const element = panel.current;
|
|
107
|
+
if (focusOnOpen && !previousFocusOnOpen.current && element?.matches(":popover-open") && !element.contains(element.ownerDocument.activeElement)) {
|
|
108
|
+
focusContent(element, pop.initialFocusRef?.current);
|
|
109
|
+
}
|
|
110
|
+
previousFocusOnOpen.current = focusOnOpen;
|
|
111
|
+
}, [focusOnOpen]);
|
|
112
|
+
const tooltipTriggerProps = { ref: setAnchor,
|
|
113
|
+
"aria-describedby": disabled ? undefined : id,
|
|
114
|
+
onPointerEnter: event => { if (event.pointerType === "touch")
|
|
115
|
+
return; hovered.current = true; enter(); },
|
|
116
|
+
onPointerLeave: () => { hovered.current = false; leave(); }, onFocus: () => { focused.current = true; enter(true); }, onBlur: () => { focused.current = false; leave(); },
|
|
117
|
+
};
|
|
118
|
+
const popoverTriggerProps = { ref: setAnchor, type: "button", disabled, "aria-haspopup": "dialog", "aria-expanded": open, "aria-controls": id, onClick: () => request(!open) };
|
|
107
119
|
// Remove component-only props before forwarding native div attributes.
|
|
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(
|
|
120
|
+
const { title: unusedTitle, chrome: unusedChrome, content: unusedContent, children: unusedChildren, open: unusedOpen, defaultOpen: unusedDefault, onOpenChange: unusedChange, delay: unusedDelay, closeLabel: unusedClose, initialFocusRef: unusedFocus, positionReference: unusedPosition, focusOnOpen: unusedFocusOnOpen, ...attributes } = native;
|
|
121
|
+
return _jsxs(_Fragment, { children: [kind === "tooltip" ? tip.trigger(tooltipTriggerProps) : pop.trigger(popoverTriggerProps), _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, "aria-hidden": titleOnly && !open ? true : attributes["aria-hidden"], "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") {
|
|
110
122
|
overPanel.current = true;
|
|
111
123
|
clear();
|
|
112
124
|
} }, onPointerLeave: event => { attributes.onPointerLeave?.(event); if (kind === "tooltip") {
|
|
113
125
|
overPanel.current = false;
|
|
114
126
|
leave();
|
|
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 })] }),
|
|
127
|
+
} }, 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 }), !titleOnly && _jsx(OverlayClose, { label: pop.closeLabel ?? "Close", onClick: close })] }), showContent && _jsx("div", { className: "flood-overlay-body", children: pop.children })] }) })] });
|
|
116
128
|
});
|
|
117
129
|
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) {
|
|
118
130
|
const [local, setLocal] = useState(defaultOpen), open = controlled ?? local;
|
|
@@ -161,7 +173,11 @@ export const ModalOverlay = /* @__PURE__ */ forwardRef(function ModalOverlay({ s
|
|
|
161
173
|
start();
|
|
162
174
|
return () => { alive = false; view.cancelAnimationFrame(frame); dispose(); };
|
|
163
175
|
}, [open]);
|
|
164
|
-
return _jsxs(_Fragment, { children: [trigger({ ref: setAnchor, type: "button", disabled, "aria-haspopup": "dialog", "aria-expanded": open, "aria-controls": id, onClick:
|
|
176
|
+
return _jsxs(_Fragment, { children: [trigger({ ref: setAnchor, type: "button", disabled, "aria-haspopup": "dialog", "aria-expanded": open, "aria-controls": id, onClick: event => {
|
|
177
|
+
// WebKit can leave pointer-activated buttons unfocused before showModal().
|
|
178
|
+
event.currentTarget.focus({ preventScroll: true });
|
|
179
|
+
request(true);
|
|
180
|
+
} }), _jsxs("dialog", { ...props, ref: dialog, id: id, "aria-modal": "true", "aria-labelledby": `${id}-title`, "aria-describedby": mergeDescriptions(describedBy, Boolean(description) && `${id}-description`), className: ["flood-dialog", className].filter(Boolean).join(" "), onKeyDown: event => {
|
|
165
181
|
onKeyDown?.(event);
|
|
166
182
|
if (event.defaultPrevented || event.key !== "Tab")
|
|
167
183
|
return;
|
package/dist/select.js
CHANGED
|
@@ -187,7 +187,10 @@ export const SingleSelect = /* @__PURE__ */ forwardRef(function SingleSelect({ c
|
|
|
187
187
|
const gap = length("--flood-sys-space-2") || 0;
|
|
188
188
|
const below = Math.max(0, viewportTop + viewportHeight - rect.bottom - gap * 2), above = Math.max(0, rect.top - viewportTop - gap * 2);
|
|
189
189
|
const maxHeight = Math.min(length("--flood-sys-control-md-height") * 8, Math.max(below, above));
|
|
190
|
-
|
|
190
|
+
const availableWidth = Math.max(0, viewportWidth - gap * 2);
|
|
191
|
+
assign(popup, "width", "max-content");
|
|
192
|
+
assign(popup, "min-width", `${Math.min(rect.width, availableWidth)}px`);
|
|
193
|
+
assign(popup, "max-width", `${availableWidth}px`);
|
|
191
194
|
assign(popup, "max-height", `${maxHeight}px`);
|
|
192
195
|
// The shared positioner reads column-gap as its placement gap. On this
|
|
193
196
|
// block listbox it does not add layout space between options.
|
package/dist/sidebar.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from 'react';
|
|
3
|
+
import { IconButton, NavigationTree, NavigationGroup, ScrollArea, Stack } from './index.js';
|
|
4
|
+
const useBrowserLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
|
|
5
|
+
export const WorkspaceSidebar = /* @__PURE__ */ forwardRef(function WorkspaceSidebar({ label, groups, currentValue, currentKind = "page", collapsed: controlled, defaultCollapsed = false, onCollapsedChange, collapsible = true, collapseLabel = 'Collapse sidebar', expandLabel = 'Expand sidebar', expandedValues, defaultExpandedValues = [], onExpandedValuesChange, expandItemLabel, size = 'md', variant = 'quiet', side = 'start', width = '16rem', header, headerLayout = 'stacked', footer, footerSeparator = false, headerSeparator = false, className, style, ...props }, ref) {
|
|
6
|
+
if (!label.trim() || !collapseLabel.trim() || !expandLabel.trim() || new Set(groups.map(group => group.id)).size !== groups.length || groups.some(group => !group.id.trim() || !group.label.trim()))
|
|
7
|
+
throw new Error('Flood Sidebar requires named unique groups and controls');
|
|
8
|
+
const known = new Set(), pending = groups.flatMap(group => [...group.items]);
|
|
9
|
+
while (pending.length) {
|
|
10
|
+
const item = pending.pop();
|
|
11
|
+
if (known.has(item.value))
|
|
12
|
+
throw new Error('Flood Sidebar item values must be globally unique');
|
|
13
|
+
known.add(item.value);
|
|
14
|
+
if (item.children)
|
|
15
|
+
pending.push(...item.children);
|
|
16
|
+
}
|
|
17
|
+
const [local, setLocal] = useState(defaultCollapsed), [expanded, setExpanded] = useState(defaultExpandedValues);
|
|
18
|
+
const collapsed = controlled ?? local, root = useRef(null), toggle = useRef(null), body = useRef(null);
|
|
19
|
+
const lastOwnedFocus = useRef(null);
|
|
20
|
+
const [rtl, setRtl] = useState(false);
|
|
21
|
+
useImperativeHandle(ref, () => root.current, []);
|
|
22
|
+
useBrowserLayoutEffect(() => {
|
|
23
|
+
const node = root.current;
|
|
24
|
+
if (!node)
|
|
25
|
+
return;
|
|
26
|
+
const view = node.ownerDocument.defaultView, update = () => setRtl(view.getComputedStyle(node).direction === 'rtl');
|
|
27
|
+
update();
|
|
28
|
+
const observer = new view.MutationObserver(update);
|
|
29
|
+
for (let ancestor = node; ancestor; ancestor = ancestor.parentElement)
|
|
30
|
+
observer.observe(ancestor, { attributes: true, attributeFilter: ['dir', 'style', 'class'] });
|
|
31
|
+
view.addEventListener('resize', update);
|
|
32
|
+
return () => { observer.disconnect(); view.removeEventListener('resize', update); };
|
|
33
|
+
}, []);
|
|
34
|
+
useBrowserLayoutEffect(() => {
|
|
35
|
+
const node = root.current;
|
|
36
|
+
if (!node)
|
|
37
|
+
return;
|
|
38
|
+
const document = node.ownerDocument;
|
|
39
|
+
const track = (event) => { const target = event.target; lastOwnedFocus.current = node.contains(target) ? target : null; };
|
|
40
|
+
document.addEventListener('focusin', track, true);
|
|
41
|
+
return () => document.removeEventListener('focusin', track, true);
|
|
42
|
+
}, []);
|
|
43
|
+
useBrowserLayoutEffect(() => {
|
|
44
|
+
const node = root.current, previous = lastOwnedFocus.current;
|
|
45
|
+
if (node && previous && !previous.isConnected && node.ownerDocument.activeElement === node.ownerDocument.body)
|
|
46
|
+
(toggle.current ?? node).focus({ preventScroll: true });
|
|
47
|
+
}, [collapsed]);
|
|
48
|
+
const slot = (value) => typeof value === 'function' ? value({ collapsed }) : value;
|
|
49
|
+
const headerContent = slot(header), footerContent = slot(footer), panelOnLeft = (side === 'start') !== rtl;
|
|
50
|
+
return _jsxs("aside", { ...props, ref: root, "aria-label": label, tabIndex: -1, "data-footer-separator": footerSeparator, "data-header-separator": headerSeparator, "data-header-layout": headerLayout, "data-flood-surface": "filled", "data-size": size, "data-variant": variant, "data-collapsed": collapsed, "data-side": side, className: ['flood-sidebar', className].filter(Boolean).join(' '), style: { '--flood-sidebar-width': typeof width === 'number' ? `${width}px` : width, ...style }, children: [(headerContent != null || collapsible) && _jsxs("div", { className: "flood-sidebar__header", "data-collapsible": collapsible, children: [collapsible && _jsx(IconButton, { ref: toggle, className: "flood-sidebar__toggle", label: collapsed ? expandLabel : collapseLabel, title: collapsed ? expandLabel : collapseLabel, size: size, variant: "ghost", "aria-expanded": !collapsed, onClick: () => { toggle.current?.focus({ preventScroll: true }); if (controlled === undefined)
|
|
51
|
+
setLocal(!collapsed); onCollapsedChange?.(!collapsed); }, children: _jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("rect", { x: "3", y: "4", width: "18", height: "16", rx: "4" }), _jsx("path", { d: panelOnLeft ? 'M9 4v16' : 'M15 4v16' }), _jsx("path", { d: panelOnLeft ? (collapsed ? 'm14 9 3 3-3 3' : 'm17 9-3 3 3 3') : (collapsed ? 'm10 9-3 3 3 3' : 'm7 9 3 3-3 3') })] }) }), headerContent != null && _jsx("div", { className: "flood-sidebar__identity", children: headerContent })] }), _jsx(ScrollArea, { ref: body, className: "flood-sidebar__body", contentInset: "none", onFocus: event => {
|
|
52
|
+
// WebKit can focus the tree landmark on pointer down. Keep the row under
|
|
53
|
+
// the pointer instead of scrolling to reveal the entire tree.
|
|
54
|
+
if (event.target.matches('nav.flood-navigation-tree'))
|
|
55
|
+
event.preventDefault();
|
|
56
|
+
}, children: _jsx(Stack, { gap: "4", className: "flood-sidebar__navigation", children: groups.map(group => _jsx(NavigationGroup, { label: group.label, hideLabel: group.hideLabel, headingAction: group.headingAction, separator: group.separator, collapsible: group.collapsible, expanded: group.expanded, defaultExpanded: group.defaultExpanded, onExpandedChange: group.onExpandedChange, compact: collapsed, size: size, className: "flood-sidebar__group", "data-icon-column": group.items.some(item => item.icon != null) || undefined, children: _jsx(NavigationTree, { label: group.label, items: group.items, currentValue: currentValue, currentKind: currentKind, size: size, variant: variant, compact: collapsed, branchGuides: group.branchGuides, flyoutSide: panelOnLeft ? 'right' : 'left', expandedValues: expandedValues ?? expanded, onExpandedValuesChange: values => { if (expandedValues === undefined)
|
|
57
|
+
setExpanded(values); onExpandedValuesChange?.(values); }, expandLabel: expandItemLabel }) }, group.id)) }) }), footerContent != null && _jsx("div", { className: "flood-sidebar__footer", children: footerContent })] });
|
|
58
|
+
});
|
package/dist/toolbar.js
CHANGED
|
@@ -3,7 +3,7 @@ import { forwardRef, useEffect, useImperativeHandle, useLayoutEffect, useRef } f
|
|
|
3
3
|
const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
4
4
|
const selector = 'button:not([role]),button[role="button"],a[href]:not([role]),a[href][role="link"]';
|
|
5
5
|
const independent = '[role="menu"],[role="tablist"],[role="radiogroup"],[role="listbox"],[role="combobox"],.flood-toggle-group';
|
|
6
|
-
export const RovingToolbar = /* @__PURE__ */ forwardRef(function RovingToolbar({ label, orientation = "horizontal", loop = true, className, onKeyDown, onKeyDownCapture, onFocus, ...props }, ref) {
|
|
6
|
+
export const RovingToolbar = /* @__PURE__ */ forwardRef(function RovingToolbar({ label, orientation = "horizontal", loop = true, shape = "rounded", variant = "contained", className, onKeyDown, onKeyDownCapture, onFocus, ...props }, ref) {
|
|
7
7
|
const root = useRef(null), current = useRef(null), within = useRef(false);
|
|
8
8
|
const refresh = useRef(() => []);
|
|
9
9
|
useImperativeHandle(ref, () => root.current, []);
|
|
@@ -73,7 +73,7 @@ export const RovingToolbar = /* @__PURE__ */ forwardRef(function RovingToolbar({
|
|
|
73
73
|
controls[next].scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
74
74
|
refresh.current();
|
|
75
75
|
};
|
|
76
|
-
return _jsx("div", { ...props, ref: root, role: "toolbar", "aria-label": label, "aria-orientation": orientation, tabIndex: -1, "data-orientation": orientation, className: ["flood-toolbar", className].filter(Boolean).join(" "), onFocus: event => { onFocus?.(event); if (event.defaultPrevented)
|
|
76
|
+
return _jsx("div", { ...props, ref: root, role: "toolbar", "aria-label": label, "aria-orientation": orientation, tabIndex: -1, "data-orientation": orientation, "data-shape": shape, "data-variant": variant, className: ["flood-toolbar", className].filter(Boolean).join(" "), onFocus: event => { onFocus?.(event); if (event.defaultPrevented)
|
|
77
77
|
return; within.current = true; refresh.current(); }, onKeyDownCapture: event => { onKeyDownCapture?.(event); if (!event.defaultPrevented && orientation === "vertical" && event.target.getAttribute("aria-haspopup") === "menu" && ["ArrowUp", "ArrowDown"].includes(event.key))
|
|
78
78
|
move(event); }, onKeyDown: event => { onKeyDown?.(event); if (!event.defaultPrevented)
|
|
79
79
|
move(event); } });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flood-md/react",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
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.11"
|
|
34
34
|
},
|
|
35
35
|
"publishConfig": {
|
|
36
36
|
"access": "public"
|