@tensor_1/react 2.43.0 → 2.44.0
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/dist/components/DashboardCanvas.d.ts +78 -0
- package/dist/components/DashboardCanvas.js +227 -0
- package/dist/components/DiscardPrompt.d.ts +10 -4
- package/dist/components/DiscardPrompt.js +5 -5
- package/dist/components/Drawer.d.ts +6 -2
- package/dist/components/Drawer.js +6 -3
- package/dist/components/Modal.d.ts +6 -2
- package/dist/components/Modal.js +1 -1
- package/dist/components/OrgChart.d.ts +6 -1
- package/dist/components/OrgChart.js +8 -5
- package/dist/dashboard-canvas-model.d.ts +104 -0
- package/dist/dashboard-canvas-model.js +113 -0
- package/dist/icons/Icon.d.ts +12 -12
- package/dist/index.cjs +2237 -1882
- package/dist/index.d.cts +200 -5
- package/dist/index.d.ts +3 -1
- package/dist/index.js +31 -0
- package/package.json +1 -1
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { HTMLAttributes, ReactNode } from 'react';
|
|
3
|
+
import { DashboardCanvasItem } from '../dashboard-canvas-model.js';
|
|
4
|
+
|
|
5
|
+
/** Every word the canvas renders. `{label}` is the item's name; the others are grid units. */
|
|
6
|
+
interface DashboardCanvasLabels {
|
|
7
|
+
/** The region's accessible name. */
|
|
8
|
+
region: string;
|
|
9
|
+
/** The move handle's name. */
|
|
10
|
+
move: string;
|
|
11
|
+
/** The resize handle's name. */
|
|
12
|
+
resize: string;
|
|
13
|
+
/** How the handles work, read with each handle (`aria-describedby`). */
|
|
14
|
+
instructions: string;
|
|
15
|
+
/** Announced after a move. `{col}` and `{row}` count from 1. */
|
|
16
|
+
moved: string;
|
|
17
|
+
/** Announced after a resize. */
|
|
18
|
+
resized: string;
|
|
19
|
+
/** Announced when a step would leave the canvas or pass the item's size limit. */
|
|
20
|
+
atEdge: string;
|
|
21
|
+
/** Announced when Escape drops a drag. */
|
|
22
|
+
cancelled: string;
|
|
23
|
+
}
|
|
24
|
+
declare const DEFAULT_DASHBOARD_CANVAS_LABELS: DashboardCanvasLabels;
|
|
25
|
+
/** Space between items: `sm` --uix-space-3, `md` --uix-space-4, `lg` --uix-space-6. */
|
|
26
|
+
type DashboardCanvasGap = 'sm' | 'md' | 'lg';
|
|
27
|
+
/** What changed, for the product's save and its journey event. */
|
|
28
|
+
interface DashboardCanvasChange {
|
|
29
|
+
readonly id: string;
|
|
30
|
+
readonly kind: 'move' | 'resize';
|
|
31
|
+
readonly source: 'pointer' | 'keyboard';
|
|
32
|
+
}
|
|
33
|
+
interface DashboardCanvasProps<T extends DashboardCanvasItem> extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange' | 'children'> {
|
|
34
|
+
/** The layout. Overlaps and out-of-range values are settled before rendering. */
|
|
35
|
+
items: readonly T[];
|
|
36
|
+
/** One cell's content: a widget, a card. Its last child takes the spare height. */
|
|
37
|
+
renderItem: (item: T) => ReactNode;
|
|
38
|
+
/** The item's name in labels and announcements. Default: its id. */
|
|
39
|
+
itemLabel?: (item: T) => string;
|
|
40
|
+
/**
|
|
41
|
+
* Edit mode: each item gets a move handle and a resize handle, both draggable and both
|
|
42
|
+
* operable with the arrow keys. Off, the canvas is a plain layout and server-renderable.
|
|
43
|
+
*/
|
|
44
|
+
editable?: boolean;
|
|
45
|
+
/** The next layout, once per drop or key press. The canvas never keeps its own copy. */
|
|
46
|
+
onLayoutChange?: (items: T[], change: DashboardCanvasChange) => void;
|
|
47
|
+
/** Height of one row, any CSS length. Default `--uix-dashboard-canvas-row` (5rem). */
|
|
48
|
+
rowHeight?: string;
|
|
49
|
+
/** Default `md`. */
|
|
50
|
+
gap?: DashboardCanvasGap;
|
|
51
|
+
labels?: Partial<DashboardCanvasLabels>;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A free dashboard layout (TENSOR HAR-1823): twelve columns, rows of one fixed height, and each
|
|
55
|
+
* item at its own `{ col, row, w, h }` in grid units. In edit mode a person drags an item by its
|
|
56
|
+
* move handle and resizes it by the corner handle; both snap to the grid, push what they land
|
|
57
|
+
* on downwards, and report one new layout per drop. Both handles also take the arrow keys, and
|
|
58
|
+
* the pure functions behind them (`moveCanvasItem`, `resizeCanvasItem`) are exported for the
|
|
59
|
+
* product's own item menu, so dragging is never the only way (WCAG 2.5.7).
|
|
60
|
+
*
|
|
61
|
+
* Below 36rem of its own width the canvas is one column in reading order (row by row, left to
|
|
62
|
+
* right), each item keeping its height, and the handles step aside: the item menu remains.
|
|
63
|
+
*
|
|
64
|
+
* Owns interaction, never persistence: the product stores `items` and passes them back.
|
|
65
|
+
*
|
|
66
|
+
* ```tsx
|
|
67
|
+
* <DashboardCanvas
|
|
68
|
+
* items={layout}
|
|
69
|
+
* editable={editing}
|
|
70
|
+
* itemLabel={(item) => titles[item.id]}
|
|
71
|
+
* renderItem={(item) => <Widget id={item.id} />}
|
|
72
|
+
* onLayoutChange={(next) => save(next)}
|
|
73
|
+
* />
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
declare function DashboardCanvas<T extends DashboardCanvasItem>({ items, renderItem, itemLabel, editable, onLayoutChange, rowHeight, gap, labels: labelOverrides, className, style, ...props }: DashboardCanvasProps<T>): react.JSX.Element;
|
|
77
|
+
|
|
78
|
+
export { DEFAULT_DASHBOARD_CANVAS_LABELS, DashboardCanvas, type DashboardCanvasChange, type DashboardCanvasGap, type DashboardCanvasLabels, type DashboardCanvasProps };
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useId, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { cx } from "../cx.js";
|
|
5
|
+
import { fillLabel } from "../fill-label.js";
|
|
6
|
+
import { GripVerticalIcon, Maximize2Icon } from "../icons/components.js";
|
|
7
|
+
import {
|
|
8
|
+
canvasMoveTarget,
|
|
9
|
+
canvasReadingOrder,
|
|
10
|
+
canvasResizeTarget,
|
|
11
|
+
DASHBOARD_CANVAS_COLUMNS,
|
|
12
|
+
normalizeCanvas,
|
|
13
|
+
placeCanvasItem
|
|
14
|
+
} from "../dashboard-canvas-model.js";
|
|
15
|
+
const DEFAULT_DASHBOARD_CANVAS_LABELS = {
|
|
16
|
+
region: "Dashboard layout",
|
|
17
|
+
move: "Move {label}",
|
|
18
|
+
resize: "Resize {label}",
|
|
19
|
+
instructions: "Arrow keys move or resize one step. Escape cancels a drag.",
|
|
20
|
+
moved: "{label}: column {col}, row {row}",
|
|
21
|
+
resized: "{label}: {w} columns wide, {h} rows tall",
|
|
22
|
+
atEdge: "{label} cannot go further that way",
|
|
23
|
+
cancelled: "Move cancelled"
|
|
24
|
+
};
|
|
25
|
+
const DRAG_THRESHOLD = 4;
|
|
26
|
+
function measure(grid) {
|
|
27
|
+
const style = getComputedStyle(grid);
|
|
28
|
+
const width = grid.clientWidth - (Number.parseFloat(style.paddingLeft) || 0) - (Number.parseFloat(style.paddingRight) || 0);
|
|
29
|
+
const tracks = style.gridTemplateColumns.split(" ").map((track) => Number.parseFloat(track)).filter(Number.isFinite);
|
|
30
|
+
const trackSum = tracks.reduce((sum, track) => sum + track, 0);
|
|
31
|
+
const columnGap = tracks.length > 1 ? Math.max(0, (width - trackSum) / (tracks.length - 1)) : 0;
|
|
32
|
+
const colPitch = (width + columnGap) / DASHBOARD_CANVAS_COLUMNS;
|
|
33
|
+
const rowGap = Number.parseFloat(style.rowGap) || 0;
|
|
34
|
+
const rowTrack = Number.parseFloat(style.gridAutoRows) || 80;
|
|
35
|
+
return { colPitch, rowPitch: rowTrack + rowGap };
|
|
36
|
+
}
|
|
37
|
+
function DashboardCanvas({
|
|
38
|
+
items,
|
|
39
|
+
renderItem,
|
|
40
|
+
itemLabel = (item) => item.id,
|
|
41
|
+
editable = false,
|
|
42
|
+
onLayoutChange,
|
|
43
|
+
rowHeight,
|
|
44
|
+
gap = "md",
|
|
45
|
+
labels: labelOverrides,
|
|
46
|
+
className,
|
|
47
|
+
style,
|
|
48
|
+
...props
|
|
49
|
+
}) {
|
|
50
|
+
const labels = { ...DEFAULT_DASHBOARD_CANVAS_LABELS, ...labelOverrides };
|
|
51
|
+
const instructionsId = useId();
|
|
52
|
+
const gridRef = useRef(null);
|
|
53
|
+
const dragRef = useRef(null);
|
|
54
|
+
const [preview, setPreview] = useState(null);
|
|
55
|
+
const [announcement, setAnnouncement] = useState("");
|
|
56
|
+
const layout = useMemo(() => normalizeCanvas(items), [items]);
|
|
57
|
+
const order = useMemo(() => new Map(canvasReadingOrder(layout).map((item, index) => [item.id, index])), [layout]);
|
|
58
|
+
const live = useRef({ layout, onLayoutChange, labels, itemLabel });
|
|
59
|
+
live.current = { layout, onLayoutChange, labels, itemLabel };
|
|
60
|
+
useEffect(() => () => dragRef.current?.stop(), []);
|
|
61
|
+
const describe = (item, kind) => kind === "move" ? fillLabel(labels.moved, { label: itemLabel(item), col: item.col + 1, row: item.row + 1 }) : fillLabel(labels.resized, { label: itemLabel(item), w: item.w, h: item.h });
|
|
62
|
+
const commit = (id, placement, kind, source) => {
|
|
63
|
+
const state = live.current;
|
|
64
|
+
const next = placeCanvasItem(state.layout, id, placement);
|
|
65
|
+
const placed = next.find((item) => item.id === id);
|
|
66
|
+
if (!placed) return;
|
|
67
|
+
state.onLayoutChange?.(next, { id, kind, source });
|
|
68
|
+
setAnnouncement(describe(placed, kind));
|
|
69
|
+
};
|
|
70
|
+
const onHandleKeyDown = (event, item, kind) => {
|
|
71
|
+
const keys = { ArrowUp: "up", ArrowDown: "down", ArrowLeft: "left", ArrowRight: "right" };
|
|
72
|
+
const resizes = { ArrowRight: "wider", ArrowLeft: "narrower", ArrowDown: "taller", ArrowUp: "shorter" };
|
|
73
|
+
if (event.key === "Escape" && dragRef.current) {
|
|
74
|
+
event.preventDefault();
|
|
75
|
+
dragRef.current.stop();
|
|
76
|
+
dragRef.current = null;
|
|
77
|
+
setPreview(null);
|
|
78
|
+
setAnnouncement(labels.cancelled);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (!(event.key in keys)) return;
|
|
82
|
+
event.preventDefault();
|
|
83
|
+
const target = kind === "move" ? canvasMoveTarget(item, keys[event.key]) : canvasResizeTarget(item, resizes[event.key]);
|
|
84
|
+
if (!target) {
|
|
85
|
+
setAnnouncement(fillLabel(labels.atEdge, { label: itemLabel(item) }));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
commit(item.id, target, kind, "keyboard");
|
|
89
|
+
};
|
|
90
|
+
const onHandlePointerDown = (event, item, kind) => {
|
|
91
|
+
if (event.button !== 0 || dragRef.current || !gridRef.current) return;
|
|
92
|
+
const { colPitch, rowPitch } = measure(gridRef.current);
|
|
93
|
+
const origin = { col: item.col, row: item.row, w: item.w, h: item.h };
|
|
94
|
+
let moved = false;
|
|
95
|
+
let last = origin;
|
|
96
|
+
const targetFor = (move) => {
|
|
97
|
+
const dc = Math.round((move.clientX - drag.startX) / colPitch);
|
|
98
|
+
const dr = Math.round((move.clientY - drag.startY) / rowPitch);
|
|
99
|
+
return kind === "move" ? { ...origin, col: origin.col + dc, row: origin.row + dr } : { ...origin, w: origin.w + dc, h: origin.h + dr };
|
|
100
|
+
};
|
|
101
|
+
const onMove = (move) => {
|
|
102
|
+
if (move.pointerId !== drag.pointerId) return;
|
|
103
|
+
if (!moved && Math.hypot(move.clientX - drag.startX, move.clientY - drag.startY) < DRAG_THRESHOLD) return;
|
|
104
|
+
moved = true;
|
|
105
|
+
const placed = placeCanvasItem(live.current.layout, item.id, targetFor(move)).find((candidate) => candidate.id === item.id);
|
|
106
|
+
if (!placed) return;
|
|
107
|
+
last = { col: placed.col, row: placed.row, w: placed.w, h: placed.h };
|
|
108
|
+
setPreview({ id: item.id, placement: last });
|
|
109
|
+
};
|
|
110
|
+
const finish = (drop) => {
|
|
111
|
+
drag.stop();
|
|
112
|
+
dragRef.current = null;
|
|
113
|
+
setPreview(null);
|
|
114
|
+
const changed = last.col !== origin.col || last.row !== origin.row || last.w !== origin.w || last.h !== origin.h;
|
|
115
|
+
if (drop && moved && changed) commit(item.id, last, kind, "pointer");
|
|
116
|
+
};
|
|
117
|
+
const onUp = (up) => {
|
|
118
|
+
if (up.pointerId === drag.pointerId) finish(true);
|
|
119
|
+
};
|
|
120
|
+
const onCancel = (cancel) => {
|
|
121
|
+
if (cancel.pointerId === drag.pointerId) finish(false);
|
|
122
|
+
};
|
|
123
|
+
window.addEventListener("pointermove", onMove);
|
|
124
|
+
window.addEventListener("pointerup", onUp);
|
|
125
|
+
window.addEventListener("pointercancel", onCancel);
|
|
126
|
+
const drag = {
|
|
127
|
+
id: item.id,
|
|
128
|
+
kind,
|
|
129
|
+
pointerId: event.pointerId,
|
|
130
|
+
startX: event.clientX,
|
|
131
|
+
startY: event.clientY,
|
|
132
|
+
origin,
|
|
133
|
+
colPitch,
|
|
134
|
+
rowPitch,
|
|
135
|
+
stop: () => {
|
|
136
|
+
window.removeEventListener("pointermove", onMove);
|
|
137
|
+
window.removeEventListener("pointerup", onUp);
|
|
138
|
+
window.removeEventListener("pointercancel", onCancel);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
dragRef.current = drag;
|
|
142
|
+
};
|
|
143
|
+
const placementStyle = (placement, readingIndex) => ({
|
|
144
|
+
"--uix-dashboard-canvas-col": placement.col + 1,
|
|
145
|
+
"--uix-dashboard-canvas-row-start": placement.row + 1,
|
|
146
|
+
"--uix-dashboard-canvas-w": placement.w,
|
|
147
|
+
"--uix-dashboard-canvas-h": placement.h,
|
|
148
|
+
"--uix-dashboard-canvas-order": readingIndex
|
|
149
|
+
});
|
|
150
|
+
const rootStyle = rowHeight ? { ...style, "--uix-dashboard-canvas-row": rowHeight } : style;
|
|
151
|
+
return /* @__PURE__ */ jsxs(
|
|
152
|
+
"div",
|
|
153
|
+
{
|
|
154
|
+
...props,
|
|
155
|
+
ref: gridRef,
|
|
156
|
+
role: "region",
|
|
157
|
+
"aria-label": labels.region,
|
|
158
|
+
className: cx(
|
|
159
|
+
"uix-dashboard-canvas",
|
|
160
|
+
gap === "sm" && "uix-dashboard-canvas--gap-sm",
|
|
161
|
+
gap === "lg" && "uix-dashboard-canvas--gap-lg",
|
|
162
|
+
editable && "uix-dashboard-canvas--editing",
|
|
163
|
+
preview && "uix-dashboard-canvas--dragging",
|
|
164
|
+
className
|
|
165
|
+
),
|
|
166
|
+
style: rootStyle,
|
|
167
|
+
children: [
|
|
168
|
+
layout.map((item) => {
|
|
169
|
+
const name = itemLabel(item);
|
|
170
|
+
const dragging = preview?.id === item.id;
|
|
171
|
+
return /* @__PURE__ */ jsxs(
|
|
172
|
+
"div",
|
|
173
|
+
{
|
|
174
|
+
className: cx("uix-dashboard-canvas__item", dragging && "uix-dashboard-canvas__item--dragging"),
|
|
175
|
+
style: placementStyle(item, order.get(item.id) ?? 0),
|
|
176
|
+
"data-canvas-id": item.id,
|
|
177
|
+
children: [
|
|
178
|
+
renderItem(item),
|
|
179
|
+
editable ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
180
|
+
/* @__PURE__ */ jsx(
|
|
181
|
+
"button",
|
|
182
|
+
{
|
|
183
|
+
type: "button",
|
|
184
|
+
className: "uix-dashboard-canvas__handle uix-dashboard-canvas__move",
|
|
185
|
+
"aria-label": fillLabel(labels.move, { label: name }),
|
|
186
|
+
"aria-describedby": instructionsId,
|
|
187
|
+
onKeyDown: (event) => onHandleKeyDown(event, item, "move"),
|
|
188
|
+
onPointerDown: (event) => onHandlePointerDown(event, item, "move"),
|
|
189
|
+
children: /* @__PURE__ */ jsx(GripVerticalIcon, { "aria-hidden": "true" })
|
|
190
|
+
}
|
|
191
|
+
),
|
|
192
|
+
/* @__PURE__ */ jsx(
|
|
193
|
+
"button",
|
|
194
|
+
{
|
|
195
|
+
type: "button",
|
|
196
|
+
className: "uix-dashboard-canvas__handle uix-dashboard-canvas__resize",
|
|
197
|
+
"aria-label": fillLabel(labels.resize, { label: name }),
|
|
198
|
+
"aria-describedby": instructionsId,
|
|
199
|
+
onKeyDown: (event) => onHandleKeyDown(event, item, "resize"),
|
|
200
|
+
onPointerDown: (event) => onHandlePointerDown(event, item, "resize"),
|
|
201
|
+
children: /* @__PURE__ */ jsx(Maximize2Icon, { "aria-hidden": "true" })
|
|
202
|
+
}
|
|
203
|
+
)
|
|
204
|
+
] }) : null
|
|
205
|
+
]
|
|
206
|
+
},
|
|
207
|
+
item.id
|
|
208
|
+
);
|
|
209
|
+
}),
|
|
210
|
+
preview ? /* @__PURE__ */ jsx(
|
|
211
|
+
"div",
|
|
212
|
+
{
|
|
213
|
+
className: "uix-dashboard-canvas__preview",
|
|
214
|
+
style: placementStyle(preview.placement, order.get(preview.id) ?? 0),
|
|
215
|
+
"aria-hidden": "true"
|
|
216
|
+
}
|
|
217
|
+
) : null,
|
|
218
|
+
editable ? /* @__PURE__ */ jsx("span", { id: instructionsId, className: "uix-visually-hidden", children: labels.instructions }) : null,
|
|
219
|
+
/* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", role: "status", "aria-live": "polite", children: announcement })
|
|
220
|
+
]
|
|
221
|
+
}
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
export {
|
|
225
|
+
DEFAULT_DASHBOARD_CANVAS_LABELS,
|
|
226
|
+
DashboardCanvas
|
|
227
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { RefObject, KeyboardEvent, ReactNode } from 'react';
|
|
2
|
+
import { SyntheticEvent, RefObject, KeyboardEvent, ReactNode } from 'react';
|
|
3
3
|
import { ButtonProps } from './Button.js';
|
|
4
4
|
import '../link.js';
|
|
5
5
|
import './Kbd.js';
|
|
@@ -15,6 +15,12 @@ interface DiscardPromptLabels {
|
|
|
15
15
|
/** Close and throw the input away. */
|
|
16
16
|
discard: string;
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* What asked a `Drawer` or `Modal` to close, handed to its `onClose`: the click on the close
|
|
20
|
+
* button, a `DialogClose`, the drawer's backdrop or "Discard". Absent when the browser closed the
|
|
21
|
+
* dialog itself (Escape on a clean dialog, a `<form method="dialog">`), as before 2.42.
|
|
22
|
+
*/
|
|
23
|
+
type DialogCloseEvent = SyntheticEvent | Event;
|
|
18
24
|
type DialogCloseProps = ButtonProps;
|
|
19
25
|
/**
|
|
20
26
|
* A button that closes the surrounding `Drawer` or `Modal` the way its × does (HAR-2029): at once
|
|
@@ -25,7 +31,7 @@ type DialogCloseProps = ButtonProps;
|
|
|
25
31
|
declare const DialogClose: react.ForwardRefExoticComponent<ButtonProps & react.RefAttributes<HTMLButtonElement>>;
|
|
26
32
|
interface GuardedDialogOptions {
|
|
27
33
|
open: boolean;
|
|
28
|
-
onClose?: () => void;
|
|
34
|
+
onClose?: (event?: DialogCloseEvent) => void;
|
|
29
35
|
dirty?: boolean;
|
|
30
36
|
pending?: boolean;
|
|
31
37
|
labels?: Partial<DiscardPromptLabels>;
|
|
@@ -45,10 +51,10 @@ interface GuardedDialogOptions {
|
|
|
45
51
|
*/
|
|
46
52
|
declare function useGuardedDialog({ open, onClose, dirty, pending, labels: labelsProp, closeRef }: GuardedDialogOptions): {
|
|
47
53
|
ref: RefObject<HTMLDialogElement>;
|
|
48
|
-
requestClose: (anchor?: HTMLElement | null) => void;
|
|
54
|
+
requestClose: (anchor?: HTMLElement | null, event?: DialogCloseEvent) => void;
|
|
49
55
|
onKeyDown: (event: KeyboardEvent<HTMLDialogElement>) => void;
|
|
50
56
|
prompt: react.JSX.Element | null;
|
|
51
57
|
provide: (children: ReactNode) => react.JSX.Element;
|
|
52
58
|
};
|
|
53
59
|
|
|
54
|
-
export { DialogClose, type DialogCloseProps, type DiscardPromptLabels, type GuardedDialogOptions, useGuardedDialog };
|
|
60
|
+
export { DialogClose, type DialogCloseEvent, type DialogCloseProps, type DiscardPromptLabels, type GuardedDialogOptions, useGuardedDialog };
|
|
@@ -37,7 +37,7 @@ const DialogClose = forwardRef(({ onClick, type = "button", ...props }, ref) =>
|
|
|
37
37
|
}
|
|
38
38
|
onClick?.(event);
|
|
39
39
|
if (event.defaultPrevented) return;
|
|
40
|
-
dialog?.requestClose(event.currentTarget);
|
|
40
|
+
dialog?.requestClose(event.currentTarget, event);
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
);
|
|
@@ -57,11 +57,11 @@ function useGuardedDialog({ open, onClose, dirty = false, pending = false, label
|
|
|
57
57
|
setPromptState(next);
|
|
58
58
|
}, []);
|
|
59
59
|
const guarded = () => !!latest.current.onClose && (latest.current.dirty || latest.current.pending);
|
|
60
|
-
const requestClose = useCallback((anchor) => {
|
|
60
|
+
const requestClose = useCallback((anchor, event) => {
|
|
61
61
|
const { open: isOpen, onClose: close, dirty: isDirty, pending: isPending, prompt: asking } = latest.current;
|
|
62
62
|
if (!isOpen || !close || isPending || asking) return;
|
|
63
63
|
if (!isDirty) {
|
|
64
|
-
close();
|
|
64
|
+
close(event);
|
|
65
65
|
return;
|
|
66
66
|
}
|
|
67
67
|
const active = document.activeElement;
|
|
@@ -89,9 +89,9 @@ function useGuardedDialog({ open, onClose, dirty = false, pending = false, label
|
|
|
89
89
|
return true;
|
|
90
90
|
});
|
|
91
91
|
dialogRef.current = ref.current;
|
|
92
|
-
const discard = useCallback(() => {
|
|
92
|
+
const discard = useCallback((event) => {
|
|
93
93
|
setPrompt(null);
|
|
94
|
-
latest.current.onClose?.();
|
|
94
|
+
latest.current.onClose?.(event);
|
|
95
95
|
}, [setPrompt]);
|
|
96
96
|
useEffect(() => {
|
|
97
97
|
if (open && dirty && !pending) return;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { HTMLAttributes, ReactNode } from 'react';
|
|
3
|
-
import { DiscardPromptLabels } from './DiscardPrompt.js';
|
|
3
|
+
import { DialogCloseEvent, DiscardPromptLabels } from './DiscardPrompt.js';
|
|
4
4
|
import './Button.js';
|
|
5
5
|
import '../link.js';
|
|
6
6
|
import './Kbd.js';
|
|
@@ -9,7 +9,11 @@ interface DrawerProps extends Omit<HTMLAttributes<HTMLDialogElement>, 'title'> {
|
|
|
9
9
|
/** TENSOR RX-125 (UIX-04): translatable; English default. */
|
|
10
10
|
closeLabel?: string;
|
|
11
11
|
open: boolean;
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Close the drawer. Gets the click that asked (the close button, the backdrop, a `DialogClose`,
|
|
14
|
+
* "Discard"); nothing when the browser closed it (Escape, `<form method="dialog">`).
|
|
15
|
+
*/
|
|
16
|
+
onClose?: (event?: DialogCloseEvent) => void;
|
|
13
17
|
/**
|
|
14
18
|
* The drawer holds input that is not saved (HAR-2029), e.g. react-hook-form's
|
|
15
19
|
* `formState.isDirty`. Escape, the close button, a backdrop click and a `DialogClose` then open
|
|
@@ -3,7 +3,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useId, useRef } from "react";
|
|
4
4
|
import { cx } from "../cx.js";
|
|
5
5
|
import { useUixLabels } from "../labels-context.js";
|
|
6
|
-
import {
|
|
6
|
+
import { isBackdropClick } from "../hooks/backdropDismiss.js";
|
|
7
7
|
import { useGuardedDialog } from "./DiscardPrompt.js";
|
|
8
8
|
const CloseIcon = () => /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M4 4l8 8M12 4l-8 8" }) });
|
|
9
9
|
function Drawer({
|
|
@@ -35,7 +35,10 @@ function Drawer({
|
|
|
35
35
|
className: cx("uix-drawer", side !== "end" && `uix-drawer--${side}`, className),
|
|
36
36
|
"aria-labelledby": title ? titleId : void 0,
|
|
37
37
|
...rest,
|
|
38
|
-
onClick:
|
|
38
|
+
onClick: (event) => {
|
|
39
|
+
onClick?.(event);
|
|
40
|
+
if (dismissOnBackdrop && onClose && isBackdropClick(event)) guard.requestClose(null, event);
|
|
41
|
+
},
|
|
39
42
|
onKeyDown: (event) => {
|
|
40
43
|
onKeyDown?.(event);
|
|
41
44
|
guard.onKeyDown(event);
|
|
@@ -51,7 +54,7 @@ function Drawer({
|
|
|
51
54
|
ref: closeRef,
|
|
52
55
|
type: "button",
|
|
53
56
|
className: "uix-drawer__close",
|
|
54
|
-
onClick: () => guard.requestClose(),
|
|
57
|
+
onClick: (event) => guard.requestClose(event.currentTarget, event),
|
|
55
58
|
"aria-label": closeLabel,
|
|
56
59
|
"aria-disabled": pending || void 0,
|
|
57
60
|
children: /* @__PURE__ */ jsx(CloseIcon, {})
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { HTMLAttributes, ReactNode } from 'react';
|
|
3
|
-
import { DiscardPromptLabels } from './DiscardPrompt.js';
|
|
3
|
+
import { DialogCloseEvent, DiscardPromptLabels } from './DiscardPrompt.js';
|
|
4
4
|
import './Button.js';
|
|
5
5
|
import '../link.js';
|
|
6
6
|
import './Kbd.js';
|
|
7
7
|
|
|
8
8
|
interface ModalProps extends Omit<HTMLAttributes<HTMLDialogElement>, 'title'> {
|
|
9
9
|
open: boolean;
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Close the dialog. Gets the click that asked (the close button, a `DialogClose`, "Discard");
|
|
12
|
+
* nothing when the browser closed it (Escape, `<form method="dialog">`).
|
|
13
|
+
*/
|
|
14
|
+
onClose?: (event?: DialogCloseEvent) => void;
|
|
11
15
|
/**
|
|
12
16
|
* The dialog holds input that is not saved (HAR-2029). Escape, the close button and a
|
|
13
17
|
* `DialogClose` then open an anchored "Discard changes?" step with focus on "Keep editing"
|
package/dist/components/Modal.js
CHANGED
|
@@ -48,7 +48,7 @@ function Modal({
|
|
|
48
48
|
ref: closeRef,
|
|
49
49
|
type: "button",
|
|
50
50
|
className: "uix-dialog__close",
|
|
51
|
-
onClick: () => guard.requestClose(),
|
|
51
|
+
onClick: (event) => guard.requestClose(event.currentTarget, event),
|
|
52
52
|
"aria-label": closeLabel,
|
|
53
53
|
"aria-disabled": pending || void 0,
|
|
54
54
|
children: /* @__PURE__ */ jsx(CloseIcon, {})
|
|
@@ -97,7 +97,12 @@ interface OrgChartProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onSelect'
|
|
|
97
97
|
}
|
|
98
98
|
/** The next zoom step above (`dir` 1) or below (`dir` -1) `zoom`. */
|
|
99
99
|
declare const orgChartZoomStep: (zoom: number, dir: 1 | -1) => number;
|
|
100
|
-
/**
|
|
100
|
+
/**
|
|
101
|
+
* "Mira Kovač" → "MK"; one word → its first letter. Bracketed parts ("(extern)", "[TST]") and
|
|
102
|
+
* a title ending in a full stop ("Dr.", "Prof.") are not part of the initials, so
|
|
103
|
+
* "Dr. Anna-Maria Koenig" → "AK" and "Tenant Admin (dev)" → "TA". The first letter of a word
|
|
104
|
+
* is its first letter in any script, never a quote or a hyphen in front of it.
|
|
105
|
+
*/
|
|
101
106
|
declare const orgChartInitials: (name: string) => string;
|
|
102
107
|
/**
|
|
103
108
|
* An org chart over `.uix-org-chart` (HAR-1807): managers above their direct reports, joined by
|
|
@@ -31,11 +31,14 @@ const orgChartZoomStep = (zoom, dir) => {
|
|
|
31
31
|
return [...ZOOM_STEPS].reverse().find((s) => s < zoom - 1e-3) ?? MIN_ZOOM;
|
|
32
32
|
};
|
|
33
33
|
const orgChartInitials = (name) => {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
|
|
34
|
+
const letterOf = (word) => /\p{L}/u.exec(word)?.[0] ?? "";
|
|
35
|
+
const words = name.replace(/[([{][^)\]}]*[)\]}]/g, " ").trim().split(/\s+/).filter((word) => letterOf(word) !== "");
|
|
36
|
+
const named = words.length > 1 ? words.filter((word, i) => !(word.endsWith(".") && i < words.length - 1)) : words;
|
|
37
|
+
const parts = named.length > 0 ? named : words;
|
|
38
|
+
if (parts.length === 0) return "";
|
|
39
|
+
const first = letterOf(parts[0]);
|
|
40
|
+
const last = parts.length > 1 ? letterOf(parts[parts.length - 1]) : "";
|
|
41
|
+
return `${first}${last}`.toLocaleUpperCase();
|
|
39
42
|
};
|
|
40
43
|
const LOADING = "::uix-loading";
|
|
41
44
|
const FAILED = "::uix-error";
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure model for `DashboardCanvas` (TENSOR HAR-1823): a free dashboard layout on a 12-column
|
|
3
|
+
* grid. Every item has a place and a size in grid units — `col` and `row` (0-based), `w`
|
|
4
|
+
* columns wide and `h` rows tall — and may declare the smallest and largest size it accepts.
|
|
5
|
+
*
|
|
6
|
+
* The canvas is free: an item stays where it is put, and nothing floats up into a gap. When a
|
|
7
|
+
* move or a resize lands on other items, they are pushed down below it, in reading order, and
|
|
8
|
+
* whatever they then land on is pushed in turn. `compactCanvas` is the separate, explicit
|
|
9
|
+
* "close the gaps" step for a product that wants one (after removing a widget, say).
|
|
10
|
+
*
|
|
11
|
+
* The same functions back the pointer drag in the component and the keyboard alternative a
|
|
12
|
+
* product puts in each item's menu (Move up / down / left / right, Wider / Narrower, Taller /
|
|
13
|
+
* Shorter), so dragging is never the only way to arrange the canvas (WCAG 2.5.7).
|
|
14
|
+
*/
|
|
15
|
+
/** The canvas has twelve columns: 12 divides into halves, thirds, quarters and sixths. */
|
|
16
|
+
declare const DASHBOARD_CANVAS_COLUMNS = 12;
|
|
17
|
+
interface DashboardCanvasItem {
|
|
18
|
+
/** Stable id: the key of the cell and of every change reported for it. */
|
|
19
|
+
readonly id: string;
|
|
20
|
+
/** First column, 0-based (0 – 11). */
|
|
21
|
+
readonly col: number;
|
|
22
|
+
/** First row, 0-based. */
|
|
23
|
+
readonly row: number;
|
|
24
|
+
/** Width in columns. */
|
|
25
|
+
readonly w: number;
|
|
26
|
+
/** Height in rows. */
|
|
27
|
+
readonly h: number;
|
|
28
|
+
/** Narrowest the item may be, in columns. Default 1. */
|
|
29
|
+
readonly minW?: number;
|
|
30
|
+
/** Widest the item may be, in columns. Default 12. */
|
|
31
|
+
readonly maxW?: number;
|
|
32
|
+
/** Shortest the item may be, in rows. Default 1. */
|
|
33
|
+
readonly minH?: number;
|
|
34
|
+
/** Tallest the item may be, in rows. Default: no limit. */
|
|
35
|
+
readonly maxH?: number;
|
|
36
|
+
}
|
|
37
|
+
/** A keyboard move: one column or one row at a time. */
|
|
38
|
+
type DashboardCanvasDirection = 'up' | 'down' | 'left' | 'right';
|
|
39
|
+
/** A keyboard resize: one column or one row at a time. */
|
|
40
|
+
type DashboardCanvasResize = 'wider' | 'narrower' | 'taller' | 'shorter';
|
|
41
|
+
/** Where and how big an item is, in grid units. */
|
|
42
|
+
interface DashboardCanvasPlacement {
|
|
43
|
+
readonly col: number;
|
|
44
|
+
readonly row: number;
|
|
45
|
+
readonly w: number;
|
|
46
|
+
readonly h: number;
|
|
47
|
+
}
|
|
48
|
+
/** An item's size limits, made sane: whole numbers, inside the canvas, minimum never above maximum. */
|
|
49
|
+
declare function canvasItemLimits(item: DashboardCanvasItem): {
|
|
50
|
+
readonly minW: number;
|
|
51
|
+
readonly maxW: number;
|
|
52
|
+
readonly minH: number;
|
|
53
|
+
readonly maxH: number;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* The item made valid: whole numbers, its size inside its own limits and the canvas, and its
|
|
57
|
+
* place inside the twelve columns (a too-wide item at the right edge moves left). Limits from
|
|
58
|
+
* an untyped caller are made sane too: a maximum below the minimum is raised to it.
|
|
59
|
+
*/
|
|
60
|
+
declare function clampCanvasItem<T extends DashboardCanvasItem>(item: T): T;
|
|
61
|
+
/** True when two different items share at least one cell. */
|
|
62
|
+
declare function canvasItemsOverlap(a: DashboardCanvasPlacement & {
|
|
63
|
+
readonly id: string;
|
|
64
|
+
}, b: DashboardCanvasPlacement & {
|
|
65
|
+
readonly id: string;
|
|
66
|
+
}): boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Reading order: row by row from the top, left to right in a row. Ties keep the order they
|
|
69
|
+
* came in. This is the order the canvas collapses to on a narrow screen and the order a
|
|
70
|
+
* screen reader meets the items in.
|
|
71
|
+
*/
|
|
72
|
+
declare function canvasReadingOrder<T extends DashboardCanvasItem>(items: readonly T[]): T[];
|
|
73
|
+
/** The number of rows the canvas uses: one past the lowest item's last row. */
|
|
74
|
+
declare function canvasRowCount(items: readonly DashboardCanvasItem[]): number;
|
|
75
|
+
/**
|
|
76
|
+
* A layout with every item valid and no two sharing a cell. Use it on a stored layout before
|
|
77
|
+
* showing it: an overlap someone saved elsewhere is settled the same way a move settles one.
|
|
78
|
+
*/
|
|
79
|
+
declare function normalizeCanvas<T extends DashboardCanvasItem>(items: readonly T[]): T[];
|
|
80
|
+
/**
|
|
81
|
+
* Puts one item at a new place and size (clamped to its limits and the canvas) and pushes
|
|
82
|
+
* what it lands on down. Every other item keeps its place unless it had to move. Returns the
|
|
83
|
+
* items in the order they came in; an unknown id returns them unchanged.
|
|
84
|
+
*/
|
|
85
|
+
declare function placeCanvasItem<T extends DashboardCanvasItem>(items: readonly T[], id: string, placement: Partial<DashboardCanvasPlacement>): T[];
|
|
86
|
+
/** One step of a keyboard move, as a new place. Null when the item is already at that edge. */
|
|
87
|
+
declare function canvasMoveTarget(item: DashboardCanvasItem, direction: DashboardCanvasDirection): DashboardCanvasPlacement | null;
|
|
88
|
+
/**
|
|
89
|
+
* One step of a keyboard resize, as a new place and size. `wider` grows to the right, or to the
|
|
90
|
+
* left when the item already touches the right edge. Null when the item is at its limit.
|
|
91
|
+
*/
|
|
92
|
+
declare function canvasResizeTarget(item: DashboardCanvasItem, change: DashboardCanvasResize): DashboardCanvasPlacement | null;
|
|
93
|
+
/** Moves an item one column or row. At an edge the items come back unchanged. */
|
|
94
|
+
declare function moveCanvasItem<T extends DashboardCanvasItem>(items: readonly T[], id: string, direction: DashboardCanvasDirection): T[];
|
|
95
|
+
/** Resizes an item by one column or row. At a limit the items come back unchanged. */
|
|
96
|
+
declare function resizeCanvasItem<T extends DashboardCanvasItem>(items: readonly T[], id: string, change: DashboardCanvasResize): T[];
|
|
97
|
+
/**
|
|
98
|
+
* Closes the gaps: in reading order, each item rises as far as it can without landing on one
|
|
99
|
+
* above it. Columns and sizes never change. Not called by the canvas itself — a free canvas
|
|
100
|
+
* keeps the gaps a person leaves — but by a product that wants a tidy layout.
|
|
101
|
+
*/
|
|
102
|
+
declare function compactCanvas<T extends DashboardCanvasItem>(items: readonly T[]): T[];
|
|
103
|
+
|
|
104
|
+
export { DASHBOARD_CANVAS_COLUMNS, type DashboardCanvasDirection, type DashboardCanvasItem, type DashboardCanvasPlacement, type DashboardCanvasResize, canvasItemLimits, canvasItemsOverlap, canvasMoveTarget, canvasReadingOrder, canvasResizeTarget, canvasRowCount, clampCanvasItem, compactCanvas, moveCanvasItem, normalizeCanvas, placeCanvasItem, resizeCanvasItem };
|