@flood-md/react 0.2.11 → 0.2.12
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 +39 -7
- package/dist/app-shell.js +2 -2
- package/dist/collection-toolbar.js +3 -3
- package/dist/data-display.js +27 -3
- package/dist/data-table-columns.d.ts +25 -0
- package/dist/data-table-columns.js +271 -0
- package/dist/data-table-expansion.d.ts +21 -0
- package/dist/data-table-expansion.js +79 -0
- package/dist/data-table.js +100 -7
- package/dist/editable.js +57 -18
- package/dist/foundations.css +393 -31
- package/dist/index.d.ts +71 -3
- package/dist/index.js +11 -2
- package/dist/overlay-runtime.d.ts +4 -3
- package/dist/overlay-runtime.js +10 -8
- package/dist/overlays.d.ts +1 -0
- package/dist/overlays.js +2 -2
- package/dist/sparkline.d.ts +2 -0
- package/dist/sparkline.js +65 -0
- package/package.json +2 -2
package/dist/data-table.js
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { createElement as _createElement } from "react";
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { Table, Checkbox, Button, Badge, IconSlot, VisuallyHidden, Pagination, Text, Spinner, Alert, Stack, Inline, Menu, Tooltip } from "./index.js";
|
|
5
|
+
import { useDataTableColumnLayout } from "./data-table-columns.js";
|
|
6
|
+
import { DataTableExpandableRow, useDataTableExpansion } from "./data-table-expansion.js";
|
|
7
|
+
const columnName = (column) => column.unit ? `${column.label} ${column.unit}` : column.label;
|
|
8
|
+
function DataTableHeading({ column, sortLabel, disabled, descending, onSort }) {
|
|
9
|
+
const label = useRef(null);
|
|
10
|
+
const [truncated, setTruncated] = useState(false);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const node = label.current;
|
|
13
|
+
if (!node)
|
|
14
|
+
return;
|
|
15
|
+
const measure = () => setTruncated(node.scrollWidth > node.clientWidth);
|
|
16
|
+
measure();
|
|
17
|
+
const Observer = node.ownerDocument.defaultView?.ResizeObserver;
|
|
18
|
+
const observer = Observer && new Observer(measure);
|
|
19
|
+
observer?.observe(node);
|
|
20
|
+
const fonts = node.ownerDocument.fonts;
|
|
21
|
+
fonts?.addEventListener("loadingdone", measure);
|
|
22
|
+
return () => {
|
|
23
|
+
observer?.disconnect();
|
|
24
|
+
fonts?.removeEventListener("loadingdone", measure);
|
|
25
|
+
};
|
|
26
|
+
}, [column.label, column.unit]);
|
|
27
|
+
const content = _jsxs(_Fragment, { children: [_jsx("span", { ref: label, className: "flood-data-table__header-label", children: column.label }), " ", column.unit && _jsx(Badge, { size: "sm", variant: "outline", className: "flood-data-table__header-unit", "aria-hidden": "true", children: column.unit })] });
|
|
28
|
+
return _jsx(Tooltip, { content: columnName(column), disabled: !truncated, trigger: trigger => column.sortable ? _jsxs(Button, { ...trigger, "aria-describedby": undefined, variant: "ghost", size: "sm", disabled: disabled, className: "flood-data-table__sort", "aria-label": sortLabel, onClick: onSort, children: [content, _jsx(IconSlot, { size: "sm", children: _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: descending ? _jsx("path", { d: "m7 14 5-5 5 5" }) : _jsx("path", { d: "m8 8 4-4 4 4M8 16l4 4 4-4" }) }) })] }) : _jsx("span", { ...trigger, "aria-describedby": undefined, className: "flood-data-table__header-content", tabIndex: truncated ? 0 : undefined, children: content }) });
|
|
29
|
+
}
|
|
4
30
|
function activatePrimaryRowAction(event) {
|
|
5
31
|
const target = event.target;
|
|
6
32
|
const row = event.currentTarget;
|
|
@@ -19,7 +45,7 @@ function activatePrimaryRowAction(event) {
|
|
|
19
45
|
button.focus({ preventScroll: true });
|
|
20
46
|
button.click();
|
|
21
47
|
}
|
|
22
|
-
export const ManagedDataTable = /* @__PURE__ */ forwardRef(function ManagedDataTable({ caption, hideCaption = false, columns, rows, density = "comfortable", striped = false, minWidth = "40rem", toolbar, inset = "standard", sort: controlledSort, defaultSort = null, onSortChange, locale = "en", selectable = false, selectedIds, defaultSelectedIds = [], onSelectionChange, page: controlledPage, defaultPage = 1, pageSize = 10, onPageChange, showResults = true, paginationMode = "client", totalRows, selectionScope, state = "ready", onRetry, emptyContent, labels = {}, className, ...props }, ref) {
|
|
48
|
+
export const ManagedDataTable = /* @__PURE__ */ forwardRef(function ManagedDataTable({ caption, hideCaption = false, columns, rows, density = "comfortable", striped = false, minWidth = "40rem", toolbar, inset = "standard", maxViewportHeight, stickyHeader, showColumnVisibility = false, hiddenColumnIds, defaultHiddenColumnIds = [], onHiddenColumnIdsChange, columnWidths, defaultColumnWidths, onColumnWidthsChange, expandedIds, defaultExpandedIds, onExpandedChange, sort: controlledSort, defaultSort = null, onSortChange, locale = "en", selectable = false, selectedIds, defaultSelectedIds = [], onSelectionChange, page: controlledPage, defaultPage = 1, pageSize = 10, onPageChange, showResults = true, showSelection = true, paginationMode = "client", totalRows, selectionScope, state = "ready", onRetry, emptyContent, labels = {}, className, ...props }, ref) {
|
|
23
49
|
if (!caption.trim() || columns.length === 0 || columns.some(column => !column.id.trim() || !column.label.trim()) || new Set(columns.map(column => column.id)).size !== columns.length)
|
|
24
50
|
throw new Error("Flood DataTable requires a caption and unique named columns");
|
|
25
51
|
if (rows.some(row => !row.id.trim() || (row.label !== undefined && !row.label.trim())) || new Set(rows.map(row => row.id)).size !== rows.length)
|
|
@@ -39,6 +65,55 @@ export const ManagedDataTable = /* @__PURE__ */ forwardRef(function ManagedDataT
|
|
|
39
65
|
const [localSort, setSort] = useState(defaultSort), [localSelected, setSelected] = useState(defaultSelectedIds), [localPage, setPage] = useState(defaultPage);
|
|
40
66
|
const requestedSort = controlledSort === undefined ? localSort : controlledSort;
|
|
41
67
|
const sorting = requestedSort && columns.some(column => column.id === requestedSort.column && column.sortable) ? requestedSort : null;
|
|
68
|
+
const [localHiddenIds, setHiddenIds] = useState(defaultHiddenColumnIds);
|
|
69
|
+
const hidden = new Set(hiddenColumnIds ?? localHiddenIds);
|
|
70
|
+
const required = new Set(rows.flatMap(row => row.primaryAction ? [row.primaryAction.column] : []));
|
|
71
|
+
if (sorting)
|
|
72
|
+
required.add(sorting.column);
|
|
73
|
+
const displayed = columns.filter(column => column.hideable === false || required.has(column.id) || !hidden.has(column.id));
|
|
74
|
+
const retained = displayed.length > 0 ? displayed : columns.slice(0, 1);
|
|
75
|
+
const visibleColumns = [
|
|
76
|
+
...retained.filter(column => column.pinned === "start"),
|
|
77
|
+
...retained.filter(column => !column.pinned),
|
|
78
|
+
...retained.filter(column => column.pinned === "end"),
|
|
79
|
+
];
|
|
80
|
+
const expansion = useDataTableExpansion(rows, expandedIds, defaultExpandedIds, onExpandedChange);
|
|
81
|
+
const columnLayout = useDataTableColumnLayout(columns, visibleColumns, {
|
|
82
|
+
columnWidths, defaultColumnWidths, onColumnWidthsChange, measureViewport: expansion.enabled,
|
|
83
|
+
});
|
|
84
|
+
const columnCount = visibleColumns.length + Number(selectable) + Number(expansion.enabled);
|
|
85
|
+
const visibleIds = new Set(visibleColumns.map(column => column.id));
|
|
86
|
+
const normalizeHiddenIds = (ids) => columns.filter(column => column.hideable !== false && ids.has(column.id)).map(column => column.id);
|
|
87
|
+
const defaultIds = normalizeHiddenIds(new Set(defaultHiddenColumnIds));
|
|
88
|
+
const requestedIds = normalizeHiddenIds(hidden);
|
|
89
|
+
const visibilityAtDefaults = requestedIds.length === defaultIds.length && defaultIds.every(id => hidden.has(id));
|
|
90
|
+
const changeHiddenIds = (next) => {
|
|
91
|
+
if (hiddenColumnIds === undefined)
|
|
92
|
+
setHiddenIds(next);
|
|
93
|
+
onHiddenColumnIdsChange?.(next);
|
|
94
|
+
};
|
|
95
|
+
const columnItems = showColumnVisibility ? [
|
|
96
|
+
...columns.map((column) => ({
|
|
97
|
+
id: `column:${column.id}`,
|
|
98
|
+
type: "checkbox",
|
|
99
|
+
label: sorting?.column === column.id
|
|
100
|
+
? (labels.sortedColumn ?? (label => `${label} (sorted)`))(columnName(column))
|
|
101
|
+
: columnName(column),
|
|
102
|
+
checked: visibleIds.has(column.id),
|
|
103
|
+
disabled: column.hideable === false || required.has(column.id) || (visibleColumns.length === 1 && visibleIds.has(column.id)),
|
|
104
|
+
onCheckedChange: checked => {
|
|
105
|
+
const next = new Set(hidden);
|
|
106
|
+
if (checked)
|
|
107
|
+
next.delete(column.id);
|
|
108
|
+
else
|
|
109
|
+
next.add(column.id);
|
|
110
|
+
changeHiddenIds(normalizeHiddenIds(next));
|
|
111
|
+
},
|
|
112
|
+
})),
|
|
113
|
+
{ id: "column-layout-separator", type: "separator" },
|
|
114
|
+
{ id: "reset-columns", label: labels.resetColumns ?? "Reset columns",
|
|
115
|
+
disabled: visibilityAtDefaults && columnLayout.atDefaults },
|
|
116
|
+
] : [];
|
|
42
117
|
const sorted = useMemo(() => {
|
|
43
118
|
if (server || !sorting)
|
|
44
119
|
return [...rows];
|
|
@@ -86,8 +161,26 @@ export const ManagedDataTable = /* @__PURE__ */ forwardRef(function ManagedDataT
|
|
|
86
161
|
changePage(1);
|
|
87
162
|
};
|
|
88
163
|
const resultLabel = labels.results ?? ((from, to, total) => `${from}–${to} of ${total}`);
|
|
89
|
-
|
|
90
|
-
|
|
164
|
+
const selectionLabel = selectable
|
|
165
|
+
? (labels.selection ?? ((count, total) => `${count} of ${total} selected`))(selection.length, server && scope === "dataset" ? total : eligible.size)
|
|
166
|
+
: null;
|
|
167
|
+
const renderToolbar = typeof toolbar === "function" ? toolbar : null;
|
|
168
|
+
const columnsControl = showColumnVisibility ? (_jsx(Menu, { label: labels.columns ?? "Columns", items: columnItems, align: "end", onAction: id => {
|
|
169
|
+
if (id !== "reset-columns")
|
|
170
|
+
return;
|
|
171
|
+
if (!visibilityAtDefaults)
|
|
172
|
+
changeHiddenIds(defaultIds);
|
|
173
|
+
columnLayout.reset();
|
|
174
|
+
}, trigger: trigger => _jsx(Button, { ...trigger, className: renderToolbar ? undefined : "flood-data-table__columns", variant: "subtle", size: "sm", children: labels.columns ?? "Columns" }) })) : null;
|
|
175
|
+
const toolbarContent = typeof toolbar === "function" ? toolbar({ columns: columnsControl }) : toolbar;
|
|
176
|
+
const sortLabel = labels.sort ?? ((label) => `Sort by ${label}`);
|
|
177
|
+
return _jsxs("div", { ...props, ref: ref, className: ["flood-data-table", className].filter(Boolean).join(" "), "data-inset": inset, "aria-busy": state === "loading" || undefined, children: [columnsControl && !renderToolbar ? _jsxs(Inline, { gap: "3", className: "flood-data-table__toolbar", children: [toolbarContent != null && _jsx("div", { className: "flood-data-table__toolbar-content", children: toolbarContent }), columnsControl] }) : toolbarContent != null && _jsx("div", { className: "flood-data-table__toolbar", children: toolbarContent }), _jsxs(Table, { ref: columnLayout.table, className: "flood-data-table__table", caption: caption, hideCaption: hideCaption, density: density, striped: striped, maxViewportHeight: maxViewportHeight, stickyHeader: stickyHeader, "data-inset": inset, "data-fixed-columns": columnLayout.fixedWidth !== undefined || undefined, style: columnLayout.fixedWidth === undefined ? { minWidth } : { minWidth, inlineSize: columnLayout.fixedWidth, tableLayout: "fixed" }, children: [_jsx("thead", { children: _jsxs("tr", { children: [expansion.enabled && _jsx("th", { ...columnLayout.utilityCell("expansion", true), scope: "col", "data-data-table-utility": "expansion", className: "flood-data-table__expand", children: _jsx(VisuallyHidden, { children: labels.rowDetails ?? "Row details" }) }), selectable && _jsx("th", { ...columnLayout.utilityCell("selection", true), scope: "col", className: "flood-data-table__choice", children: _jsx(Checkbox, { label: _jsx(VisuallyHidden, { children: labels.selectPage ?? "Select rows on this page" }), checked: all, indeterminate: some && !all, disabled: blocked || pageChoices.length === 0, onChange: event => { const ids = new Set(selection); for (const row of pageChoices)
|
|
178
|
+
event.currentTarget.checked ? ids.add(row.id) : ids.delete(row.id); select([...ids]); } }) }), visibleColumns.map(column => {
|
|
179
|
+
const name = columnName(column);
|
|
180
|
+
return _createElement("th", { ...columnLayout.columnCell(column, true), key: column.id, scope: "col", "data-numeric": column.numeric || undefined, "data-align": column.align, "data-sortable": column.sortable || undefined, "data-resize-edge": column.resizable ? column.pinned === "end" ? "start" : "end" : undefined, "aria-label": column.sortable ? sortLabel(name) : name, "aria-sort": sorting?.column === column.id ? sorting.direction : undefined },
|
|
181
|
+
_jsx(DataTableHeading, { column: column, sortLabel: sortLabel(name), disabled: blocked, descending: sorting?.column === column.id, onSort: () => changeSort(column.id) }),
|
|
182
|
+
column.resizable && columnLayout.renderHandle(column, (labels.resizeColumn ?? (label => `Resize ${label} column`))(name), blocked));
|
|
183
|
+
})] }) }), _jsx("tbody", { children: state === "ready" && visible.length > 0 ? visible.map(row => _jsx(DataTableExpandableRow, { row: row, showDisclosure: expansion.enabled, expanded: expansion.expanded.has(row.id), colSpan: columnCount, labels: labels, onToggle: () => expansion.toggle(row.id), disclosureCellProps: columnLayout.utilityCell("expansion"), viewportWidth: columnLayout.viewportWidth, disclosureWidth: columnLayout.disclosureWidth, children: disclosure => _jsxs("tr", { "data-selected": selectable && chosen.has(row.id) || undefined, "data-primary-action": row.primaryAction && !row.primaryAction.buttonProps.disabled && row.primaryAction.buttonProps["aria-disabled"] !== "true" || undefined, onClick: row.primaryAction ? activatePrimaryRowAction : undefined, children: [disclosure, selectable && _jsx("td", { ...columnLayout.utilityCell("selection"), className: "flood-data-table__choice", children: _jsx(Checkbox, { label: _jsx(VisuallyHidden, { children: (labels.selectRow ?? ((label) => `Select ${label}`))(row.label ?? row.id) }), checked: chosen.has(row.id), disabled: Boolean(row.selectionDisabled), onChange: event => select(event.currentTarget.checked ? [...selection, row.id] : selection.filter(id => id !== row.id)) }) }), visibleColumns.map(column => _createElement("td", { ...columnLayout.columnCell(column), key: column.id, "data-numeric": column.numeric || undefined, "data-align": column.align }, row.primaryAction?.column === column.id
|
|
91
184
|
? _jsxs(Stack, { gap: "1", children: [_jsx(Button, { ...row.primaryAction.buttonProps, type: "button", variant: "ghost", size: "sm", className: ["flood-data-table__primary-action", row.primaryAction.buttonProps.className].filter(Boolean).join(" "), children: row.primaryAction.title }), row.primaryAction.description != null && _jsx(Text, { as: "div", size: "helper", tone: "subtle", children: row.primaryAction.description })] })
|
|
92
|
-
: row.cells[column.id] ?? null
|
|
185
|
+
: row.cells[column.id] ?? null))] }) }, row.id)) : _jsx("tr", { children: _jsx("td", { colSpan: columnCount, className: "flood-data-table__state", children: state === "loading" ? _jsx(Spinner, { label: labels.loading ?? "Loading rows…" }) : state === "error" ? _jsxs(Stack, { gap: "3", children: [_jsx(Alert, { tone: "error", announcement: "polite", children: labels.error ?? "Could not load the rows." }), onRetry && _jsx(Button, { variant: "subtle", size: "sm", onClick: onRetry, children: labels.retry ?? "Try again" })] }) : emptyContent ?? _jsx(Text, { as: "p", size: "compact", tone: "subtle", role: "status", children: labels.empty ?? "No results found." }) }) }) })] }), selectable && !showSelection && _jsx(VisuallyHidden, { role: "status", children: selectionLabel }), (showResults || (selectable && showSelection) || pageCount > 1) && _jsxs("div", { className: "flood-data-table__footer", children: [showResults && _jsx(Text, { as: "p", size: "helper", tone: "subtle", children: state === "ready" ? resultLabel(visible.length ? (page - 1) * pageSize + 1 : 0, visible.length ? (page - 1) * pageSize + visible.length : 0, total) : "" }), selectable && showSelection && _jsx(Text, { as: "p", size: "helper", tone: "subtle", role: "status", children: selectionLabel }), pageCount > 1 && _jsx(Pagination, { page: Math.min(page, pageCount), pageCount: pageCount, onPageChange: changePage, disabled: blocked, label: labels.pagination, previousLabel: labels.previousPage, nextLabel: labels.nextPage, pageLabel: labels.page })] })] });
|
|
93
186
|
});
|
package/dist/editable.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef, useEffect, useId, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
|
|
3
|
-
import { Button, IconButton, IconSlot, Input, InputGroup } from "./index.js";
|
|
3
|
+
import { Button, IconButton, IconSlot, Input, InputGroup, Text } from "./index.js";
|
|
4
|
+
import { mergeDescriptions } from "./field-state.js";
|
|
5
|
+
import { positionLayer, registerLayer } from "./overlay-runtime.js";
|
|
4
6
|
const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
5
7
|
const cx = (...values) => values.filter(Boolean).join(" ");
|
|
6
8
|
/** A draft is private until explicitly committed; native forms receive committed text. */
|
|
7
|
-
export const InlineEditable = /* @__PURE__ */ forwardRef(function InlineEditable({ label, hideLabel = false, value, defaultValue = "", onValueChange, editing, defaultEditing = false, onEditingChange, onCommit, onCancel, blurAction = "commit", readOnly = false, disabled = false, placeholder = "Empty", controlSize = "md", showActions = true, editLabel = "Edit", saveLabel = "Save", cancelLabel = "Cancel", inputRef, name, form, id: explicitId, className, onBlur, onKeyDown, ...props }, forwardedRef) {
|
|
9
|
+
export const InlineEditable = /* @__PURE__ */ forwardRef(function InlineEditable({ label, hideLabel = false, value, defaultValue = "", onValueChange, editing, defaultEditing = false, onEditingChange, onCommit, onCancel, validate, inputMode, previewVariant = "subtle", editorOverlayWidth, blurAction = "commit", readOnly = false, disabled = false, placeholder = "Empty", controlSize = "md", showActions = true, editLabel = "Edit", saveLabel = "Save", cancelLabel = "Cancel", inputRef, name, form, id: explicitId, className, onBlur, onKeyDown, ...props }, forwardedRef) {
|
|
8
10
|
if (![label, editLabel, saveLabel, cancelLabel].every(text => text.trim()))
|
|
9
11
|
throw new Error("Flood Editable requires nonempty control and action labels");
|
|
10
12
|
const initial = useRef(defaultValue), [local, setLocal] = useState(defaultValue);
|
|
@@ -12,9 +14,13 @@ export const InlineEditable = /* @__PURE__ */ forwardRef(function InlineEditable
|
|
|
12
14
|
const [localEditing, setLocalEditing] = useState(defaultEditing);
|
|
13
15
|
const requested = editing ?? localEditing, active = requested && !disabled && !readOnly;
|
|
14
16
|
const [draft, setDraft] = useState(committed);
|
|
17
|
+
const [validationError, setValidationError] = useState();
|
|
15
18
|
const root = useRef(null), editor = useRef(null), bridge = useRef(null), preview = useRef(null);
|
|
16
|
-
const
|
|
17
|
-
|
|
19
|
+
const layer = useRef(null);
|
|
20
|
+
const floating = editorOverlayWidth !== undefined;
|
|
21
|
+
const fitDraft = editorOverlayWidth === "fit-content";
|
|
22
|
+
const current = useRef({ committed, active, draft, value, editing, onValueChange, onEditingChange, onCommit, onCancel, validate });
|
|
23
|
+
current.current = { committed, active, draft, value, editing, onValueChange, onEditingChange, onCommit, onCancel, validate };
|
|
18
24
|
const composing = useRef(false), resolved = useRef(false), wasActive = useRef(false);
|
|
19
25
|
const generated = useId(), id = explicitId ?? `flood-editable-${generated}`, inputId = `${id}-input`, previewId = `${id}-preview`;
|
|
20
26
|
useImperativeHandle(forwardedRef, () => root.current, []);
|
|
@@ -34,8 +40,12 @@ export const InlineEditable = /* @__PURE__ */ forwardRef(function InlineEditable
|
|
|
34
40
|
const finish = (action, restoreFocus) => {
|
|
35
41
|
if (!current.current.active || resolved.current || (composing.current && action === "commit") || editor.current?.matches(":disabled"))
|
|
36
42
|
return;
|
|
37
|
-
resolved.current = true;
|
|
38
43
|
const next = current.current.draft;
|
|
44
|
+
const message = action === "commit" ? current.current.validate?.(next) : undefined;
|
|
45
|
+
setValidationError(message);
|
|
46
|
+
if (message)
|
|
47
|
+
return;
|
|
48
|
+
resolved.current = true;
|
|
39
49
|
if (action === "commit") {
|
|
40
50
|
if (next !== current.current.committed) {
|
|
41
51
|
if (current.current.value === undefined)
|
|
@@ -62,10 +72,35 @@ export const InlineEditable = /* @__PURE__ */ forwardRef(function InlineEditable
|
|
|
62
72
|
// A controlled host may refuse to close and allow a later deliberate attempt.
|
|
63
73
|
queueMicrotask(() => { resolved.current = false; });
|
|
64
74
|
};
|
|
75
|
+
useBrowserLayoutEffect(() => {
|
|
76
|
+
const panel = layer.current, anchor = preview.current;
|
|
77
|
+
if (!active || !floating || !panel || !anchor)
|
|
78
|
+
return;
|
|
79
|
+
const document = panel.ownerDocument, view = document.defaultView;
|
|
80
|
+
panel.showPopover();
|
|
81
|
+
let frame = 0;
|
|
82
|
+
const position = () => {
|
|
83
|
+
positionLayer(anchor, panel, "bottom", "end", true);
|
|
84
|
+
frame = view.requestAnimationFrame(position);
|
|
85
|
+
};
|
|
86
|
+
position();
|
|
87
|
+
const unregister = registerLayer(document, {
|
|
88
|
+
panel, anchor: () => anchor, close: () => finish("cancel", true), closeOnFocusOutside: false,
|
|
89
|
+
outside: () => { if (blurAction !== "none")
|
|
90
|
+
finish(blurAction, false); },
|
|
91
|
+
});
|
|
92
|
+
return () => {
|
|
93
|
+
unregister();
|
|
94
|
+
view.cancelAnimationFrame(frame);
|
|
95
|
+
if (panel.matches(":popover-open"))
|
|
96
|
+
panel.hidePopover();
|
|
97
|
+
};
|
|
98
|
+
}, [active, floating, blurAction]);
|
|
65
99
|
useBrowserLayoutEffect(() => {
|
|
66
100
|
let frame = 0;
|
|
67
101
|
if (active && !wasActive.current) {
|
|
68
102
|
setDraft(current.current.committed);
|
|
103
|
+
setValidationError(undefined);
|
|
69
104
|
resolved.current = false;
|
|
70
105
|
composing.current = false;
|
|
71
106
|
const view = editor.current?.ownerDocument.defaultView;
|
|
@@ -106,7 +141,20 @@ export const InlineEditable = /* @__PURE__ */ forwardRef(function InlineEditable
|
|
|
106
141
|
return () => { owner.removeEventListener("reset", reset); clearTimeout(timer); };
|
|
107
142
|
}, [form]);
|
|
108
143
|
const glyph = (kind) => _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: kind === "save" ? _jsx("path", { d: "m5 12 4 4L19 6" }) : kind === "cancel" ? _jsx("path", { d: "m6 6 12 12M18 6 6 18" }) : _jsx(_Fragment, { children: _jsx("path", { d: "m15 5 4 4M4 20l4-1L20 7a2.1 2.1 0 0 0-3-3L5 16l-1 4Z" }) }) });
|
|
109
|
-
const
|
|
144
|
+
const errorId = `${inputId}-error`;
|
|
145
|
+
const editorProps = { id: inputId, inputMode, "aria-describedby": mergeDescriptions(props["aria-describedby"], active && validationError ? errorId : undefined),
|
|
146
|
+
"aria-invalid": active && validationError ? true : props["aria-invalid"], controlSize, disabled };
|
|
147
|
+
const editButton = _jsxs(Button, { ref: preview, id: previewId, type: "button", variant: previewVariant, size: controlSize, disabled: disabled, className: "flood-editable__preview", "aria-describedby": props["aria-describedby"], "aria-label": `${editLabel}: ${label}. ${committed || placeholder}`, "aria-hidden": active && floating || undefined, tabIndex: active && floating ? -1 : undefined, onClick: start, children: [_jsx("span", { className: "flood-editable__value", "data-empty": !committed || undefined, children: committed || placeholder }), _jsx(IconSlot, { size: controlSize, children: glyph("edit") })] });
|
|
148
|
+
const editorContent = active && _jsxs(_Fragment, { children: [_jsx(InputGroup, { ...editorProps, ref: editor, value: draft, placeholder: placeholder, size: fitDraft ? Math.max(4, (draft || placeholder).length + 1) : undefined, onChange: event => { setValidationError(undefined); setDraft(event.currentTarget.value); current.current.draft = event.currentTarget.value; }, onCompositionStart: () => { composing.current = true; }, onCompositionEnd: event => {
|
|
149
|
+
composing.current = false;
|
|
150
|
+
setDraft(event.currentTarget.value);
|
|
151
|
+
current.current.draft = event.currentTarget.value;
|
|
152
|
+
const document = event.currentTarget.ownerDocument;
|
|
153
|
+
if (blurAction !== "none" && !root.current?.contains(document.activeElement))
|
|
154
|
+
queueMicrotask(() => finish(blurAction, false));
|
|
155
|
+
}, end: showActions ? _jsxs("div", { className: "flood-editable__actions", children: [_jsx(IconButton, { type: "button", label: saveLabel, size: controlSize, variant: "ghost", onPointerDown: event => { if (event.pointerType === "mouse")
|
|
156
|
+
event.preventDefault(); }, onMouseDown: event => event.preventDefault(), onClick: () => finish("commit", true), children: glyph("save") }), _jsx(IconButton, { type: "button", label: cancelLabel, size: controlSize, variant: "ghost", onPointerDown: event => { if (event.pointerType === "mouse")
|
|
157
|
+
event.preventDefault(); }, onMouseDown: event => event.preventDefault(), onClick: () => finish("cancel", true), children: glyph("cancel") })] }) : undefined }), validationError && _jsx(Text, { as: "p", id: errorId, role: "alert", size: "helper", tone: "error", className: "flood-editable__error", children: validationError })] });
|
|
110
158
|
return _jsxs("div", { ...props, ref: root, id: id, className: cx("flood-editable", className), "data-editing": active || undefined, "data-disabled": disabled || undefined, "data-readonly": readOnly || undefined, onKeyDown: event => {
|
|
111
159
|
onKeyDown?.(event);
|
|
112
160
|
if (event.defaultPrevented || !current.current.active || event.target !== editor.current || event.nativeEvent.isComposing || composing.current || event.keyCode === 229 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey)
|
|
@@ -120,16 +168,7 @@ export const InlineEditable = /* @__PURE__ */ forwardRef(function InlineEditable
|
|
|
120
168
|
if (event.defaultPrevented || !current.current.active || event.currentTarget.contains(event.relatedTarget) || blurAction === "none")
|
|
121
169
|
return;
|
|
122
170
|
finish(blurAction, false);
|
|
123
|
-
}, children: [_jsx("label", { htmlFor: active || readOnly ? inputId : previewId, className: cx("flood-label", hideLabel && "flood-visually-hidden"), children: label }), _jsx("input", { ref: bridge, type: "hidden", name: name, form: form, value: committed, disabled: disabled }),
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
current.current.draft = event.currentTarget.value;
|
|
127
|
-
const document = event.currentTarget.ownerDocument;
|
|
128
|
-
if (blurAction !== "none" && !root.current?.contains(document.activeElement))
|
|
129
|
-
queueMicrotask(() => finish(blurAction, false));
|
|
130
|
-
}, end: showActions ? _jsxs("div", { className: "flood-editable__actions", children: [_jsx(IconButton, { type: "button", label: saveLabel, size: controlSize, variant: "ghost", onPointerDown: event => { if (event.pointerType === "mouse")
|
|
131
|
-
event.preventDefault(); }, onMouseDown: event => event.preventDefault(), onClick: () => finish("commit", true), children: glyph("save") }), _jsx(IconButton, { type: "button", label: cancelLabel, size: controlSize, variant: "ghost", onPointerDown: event => { if (event.pointerType === "mouse")
|
|
132
|
-
event.preventDefault(); }, onMouseDown: event => event.preventDefault(), onClick: () => finish("cancel", true), children: glyph("cancel") })] }) : undefined })
|
|
133
|
-
: readOnly ? _jsx(Input, { ...editorProps, ref: editor, value: committed, readOnly: true })
|
|
134
|
-
: _jsxs(Button, { ref: preview, id: previewId, type: "button", variant: "subtle", size: controlSize, disabled: disabled, className: "flood-editable__preview", "aria-describedby": props["aria-describedby"], "aria-label": `${editLabel}: ${label}. ${committed || placeholder}`, onClick: start, children: [_jsx("span", { className: "flood-editable__value", "data-empty": !committed || undefined, children: committed || placeholder }), _jsx(IconSlot, { size: controlSize, children: glyph("edit") })] })] });
|
|
171
|
+
}, children: [_jsx("label", { htmlFor: active || readOnly ? inputId : previewId, className: cx("flood-label", hideLabel && "flood-visually-hidden"), children: label }), _jsx("input", { ref: bridge, type: "hidden", name: name, form: form, value: committed, disabled: disabled }), readOnly ? _jsx(Input, { ...editorProps, ref: editor, value: committed, readOnly: true }) : (!active || floating) && editButton, active && (floating
|
|
172
|
+
? _jsx("div", { ref: layer, popover: "manual", className: "flood-overlay-panel flood-editable__overlay", "data-fit-draft": fitDraft || undefined, style: { inlineSize: editorOverlayWidth }, children: editorContent })
|
|
173
|
+
: editorContent)] });
|
|
135
174
|
});
|