@tensor_1/react 2.47.0 → 2.48.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/README.md +470 -1
- package/dist/{SchedulingCalendar-B6TakO2h.d.ts → SchedulingCalendar-WWyFL3Eo.d.ts} +51 -2
- package/dist/announce.d.ts +10 -0
- package/dist/announce.js +30 -0
- package/dist/chart-analytic.cjs +33 -25
- package/dist/chart-preset.cjs +23 -15
- package/dist/chart.cjs +23 -15
- package/dist/combobox-model.d.ts +10 -1
- package/dist/combobox-model.js +15 -0
- package/dist/command-palette-model.d.ts +44 -0
- package/dist/command-palette-model.js +36 -0
- package/dist/components/AccountMenu.d.ts +44 -0
- package/dist/components/AccountMenu.js +43 -0
- package/dist/components/AppShell.d.ts +45 -2
- package/dist/components/AppShell.js +116 -5
- package/dist/components/AuditLog.d.ts +128 -0
- package/dist/components/AuditLog.js +196 -0
- package/dist/components/BrandProfiles.js +4 -3
- package/dist/components/Breadcrumbs.d.ts +9 -2
- package/dist/components/Breadcrumbs.js +3 -2
- package/dist/components/Button.d.ts +12 -5
- package/dist/components/Button.js +7 -3
- package/dist/components/Card.d.ts +18 -1
- package/dist/components/Card.js +5 -1
- package/dist/components/ChangeHighlight.d.ts +30 -0
- package/dist/components/ChangeHighlight.js +21 -0
- package/dist/components/ChartCore.js +2 -1
- package/dist/components/Checkbox.d.ts +27 -1
- package/dist/components/Checkbox.js +31 -2
- package/dist/components/CodeDisplay.d.ts +65 -0
- package/dist/components/CodeDisplay.js +124 -0
- package/dist/components/Combobox.d.ts +27 -6
- package/dist/components/Combobox.js +195 -58
- package/dist/components/CommandPalette.d.ts +36 -1
- package/dist/components/CommandPalette.js +254 -2
- package/dist/components/ContactCard.d.ts +66 -0
- package/dist/components/ContactCard.js +74 -0
- package/dist/components/Count.d.ts +24 -0
- package/dist/components/Count.js +32 -0
- package/dist/components/DataWorkspace.d.ts +167 -0
- package/dist/components/DataWorkspace.js +383 -0
- package/dist/components/DateFieldParts.d.ts +2 -1
- package/dist/components/DateFieldParts.js +7 -0
- package/dist/components/DatePicker.d.ts +9 -2
- package/dist/components/DatePicker.js +67 -28
- package/dist/components/DateRangePicker.d.ts +12 -2
- package/dist/components/DateRangePicker.js +13 -12
- package/dist/components/DetailPage.d.ts +7 -1
- package/dist/components/DetailPage.js +2 -1
- package/dist/components/Dialogs.d.ts +71 -10
- package/dist/components/Dialogs.js +130 -30
- package/dist/components/DiffViewer.d.ts +23 -3
- package/dist/components/DiffViewer.js +26 -12
- package/dist/components/DiscardPrompt.js +16 -0
- package/dist/components/DisplayPreferences.d.ts +83 -0
- package/dist/components/DisplayPreferences.js +185 -0
- package/dist/components/EditorialHome.d.ts +23 -3
- package/dist/components/EditorialHome.js +16 -11
- package/dist/components/EntityPicker.d.ts +20 -3
- package/dist/components/EntityPicker.js +32 -13
- package/dist/components/FilterEditor.d.ts +18 -4
- package/dist/components/FilterEditor.js +141 -19
- package/dist/components/InfoTip.js +11 -12
- package/dist/components/InlineEdit.d.ts +87 -20
- package/dist/components/InlineEdit.js +197 -54
- package/dist/components/Input.d.ts +12 -2
- package/dist/components/Input.js +5 -3
- package/dist/components/Kbd.d.ts +6 -1
- package/dist/components/Kbd.js +6 -6
- package/dist/components/Lightbox.d.ts +41 -5
- package/dist/components/Lightbox.js +95 -9
- package/dist/components/MatchReview.d.ts +40 -1
- package/dist/components/MatchReview.js +130 -4
- package/dist/components/MetricInput.js +3 -2
- package/dist/components/Modal.d.ts +2 -1
- package/dist/components/Modal.js +13 -4
- package/dist/components/NavFavourites.d.ts +5 -0
- package/dist/components/NavFavourites.js +7 -7
- package/dist/components/OtpInput.d.ts +33 -0
- package/dist/components/OtpInput.js +123 -0
- package/dist/components/PageHeader.d.ts +7 -1
- package/dist/components/PageHeader.js +2 -2
- package/dist/components/Pagination.js +6 -5
- package/dist/components/PasswordInput.d.ts +34 -0
- package/dist/components/PasswordInput.js +101 -0
- package/dist/components/Peek.js +3 -3
- package/dist/components/Popover.js +2 -0
- package/dist/components/PrintLayout.d.ts +63 -0
- package/dist/components/PrintLayout.js +114 -0
- package/dist/components/QrCode.d.ts +47 -0
- package/dist/components/QrCode.js +55 -0
- package/dist/components/QrScanner.d.ts +95 -0
- package/dist/components/QrScanner.js +266 -0
- package/dist/components/RelationshipGraph.d.ts +32 -1
- package/dist/components/RelationshipGraph.js +18 -5
- package/dist/components/RelationshipGraphLayered.d.ts +1 -1
- package/dist/components/RelationshipGraphLayered.js +190 -115
- package/dist/components/RelationshipGraphList.js +33 -6
- package/dist/components/RuleBuilder.js +39 -7
- package/dist/components/SchedulingAgenda.d.ts +3 -1
- package/dist/components/SchedulingCalendar.d.ts +3 -1
- package/dist/components/SchedulingCalendar.js +43 -4
- package/dist/components/SchedulingResourceDay.d.ts +57 -0
- package/dist/components/SchedulingResourceDay.js +287 -0
- package/dist/components/SchedulingTimeGrid.d.ts +3 -1
- package/dist/components/SchedulingTimeline.d.ts +3 -1
- package/dist/components/SearchSuggest.d.ts +23 -3
- package/dist/components/SearchSuggest.js +48 -17
- package/dist/components/Segmented.d.ts +39 -4
- package/dist/components/Segmented.js +31 -3
- package/dist/components/Select.d.ts +3 -2
- package/dist/components/Select.js +1 -1
- package/dist/components/ShortcutHint.d.ts +11 -10
- package/dist/components/ShortcutHint.js +44 -38
- package/dist/components/Sidebar.d.ts +36 -4
- package/dist/components/Sidebar.js +37 -12
- package/dist/components/SortableList.d.ts +45 -0
- package/dist/components/SortableList.js +134 -0
- package/dist/components/Stat.d.ts +28 -3
- package/dist/components/Stat.js +13 -3
- package/dist/components/StatusPill.d.ts +14 -3
- package/dist/components/StatusPill.js +11 -3
- package/dist/components/Steps.js +2 -1
- package/dist/components/Table.d.ts +9 -2
- package/dist/components/Table.js +4 -2
- package/dist/components/TableControls.js +18 -58
- package/dist/components/TableOverflowProbe.d.ts +12 -0
- package/dist/components/TableOverflowProbe.js +27 -0
- package/dist/components/Tabs.d.ts +2 -1
- package/dist/components/Tabs.js +1 -0
- package/dist/components/TagInput.d.ts +67 -0
- package/dist/components/TagInput.js +342 -0
- package/dist/components/Textarea.d.ts +5 -0
- package/dist/components/Textarea.js +2 -2
- package/dist/components/TimePicker.d.ts +80 -0
- package/dist/components/TimePicker.js +347 -0
- package/dist/components/Toast.js +3 -2
- package/dist/components/Tooltip.d.ts +8 -4
- package/dist/components/Tooltip.js +24 -21
- package/dist/components/Tree.d.ts +8 -0
- package/dist/components/Tree.js +11 -5
- package/dist/display-preferences-model.d.ts +53 -0
- package/dist/display-preferences-model.js +64 -0
- package/dist/emoji.cjs +28 -35
- package/dist/filter-model.d.ts +59 -3
- package/dist/filter-model.js +67 -2
- package/dist/hooks/useChangeHighlight.d.ts +33 -0
- package/dist/hooks/useChangeHighlight.js +44 -0
- package/dist/hooks/useDialog.d.ts +5 -0
- package/dist/hooks/useDialog.js +3 -2
- package/dist/hooks/useOptimisticValue.d.ts +51 -0
- package/dist/hooks/useOptimisticValue.js +61 -0
- package/dist/hooks/useReducedMotion.d.ts +4 -1
- package/dist/hooks/useReducedMotion.js +7 -1
- package/dist/hooks/useResolvedFilterLabels.d.ts +15 -0
- package/dist/hooks/useResolvedFilterLabels.js +34 -0
- package/dist/hooks/useVirtualRows.d.ts +21 -2
- package/dist/hooks/useVirtualRows.js +30 -3
- package/dist/icons/Icon.d.ts +1 -1
- package/dist/index.cjs +18644 -13210
- package/dist/index.d.cts +3003 -699
- package/dist/index.d.ts +42 -16
- package/dist/index.js +139 -7
- package/dist/labels-context.d.ts +90 -3
- package/dist/overlay-stack.d.ts +6 -0
- package/dist/overlay-stack.js +13 -0
- package/dist/qr-model.d.ts +65 -0
- package/dist/qr-model.js +323 -0
- package/dist/relationship-graph-model.d.ts +78 -3
- package/dist/relationship-graph-model.js +112 -5
- package/dist/reorder.d.ts +30 -0
- package/dist/reorder.js +74 -0
- package/dist/rich-text.cjs +4 -2
- package/dist/scheduling-resource-model.d.ts +76 -0
- package/dist/scheduling-resource-model.js +74 -0
- package/dist/search-suggest-model.js +3 -1
- package/dist/shell-context.d.ts +11 -0
- package/dist/shell-context.js +7 -0
- package/dist/time-field-model.d.ts +46 -0
- package/dist/time-field-model.js +152 -0
- package/dist/toast-store.d.ts +17 -1
- package/dist/toast-store.js +23 -7
- package/package.json +2 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
type ThemePreference = 'light' | 'dark' | 'system';
|
|
2
|
+
type ContrastPreference = 'standard' | 'more' | 'system';
|
|
3
|
+
type TextScalePreference = 'sm' | 'md' | 'lg' | 'xl';
|
|
4
|
+
type MotionPreference = 'system' | 'reduce';
|
|
5
|
+
type DensityPreference = 'compact' | 'standard' | 'comfortable';
|
|
6
|
+
interface DisplayPreferenceValues {
|
|
7
|
+
theme: ThemePreference;
|
|
8
|
+
contrast: ContrastPreference;
|
|
9
|
+
textScale: TextScalePreference;
|
|
10
|
+
motion: MotionPreference;
|
|
11
|
+
density: DensityPreference;
|
|
12
|
+
}
|
|
13
|
+
/** What the system says, for the `system` choices. */
|
|
14
|
+
interface SystemDisplayPreferences {
|
|
15
|
+
dark: boolean;
|
|
16
|
+
moreContrast: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** The attributes written on <html>; `undefined` means the attribute is removed. */
|
|
19
|
+
interface DisplayAttributes {
|
|
20
|
+
'data-theme': 'light' | 'dark';
|
|
21
|
+
'data-contrast'?: 'more';
|
|
22
|
+
'data-text-scale'?: 'sm' | 'lg' | 'xl';
|
|
23
|
+
'data-motion'?: 'reduce';
|
|
24
|
+
'data-density'?: 'compact' | 'comfortable';
|
|
25
|
+
}
|
|
26
|
+
declare const DEFAULT_DISPLAY_PREFERENCES: DisplayPreferenceValues;
|
|
27
|
+
/** The default `localStorage` key. */
|
|
28
|
+
declare const DISPLAY_PREFERENCES_STORAGE_KEY = "uix-display-preferences";
|
|
29
|
+
declare const DISPLAY_PREFERENCE_OPTIONS: {
|
|
30
|
+
readonly theme: readonly ["light", "dark", "system"];
|
|
31
|
+
readonly contrast: readonly ["standard", "more", "system"];
|
|
32
|
+
readonly textScale: readonly ["sm", "md", "lg", "xl"];
|
|
33
|
+
readonly motion: readonly ["system", "reduce"];
|
|
34
|
+
readonly density: readonly ["compact", "standard", "comfortable"];
|
|
35
|
+
};
|
|
36
|
+
/** Read stored preferences; anything missing or unknown falls back to `defaults`. */
|
|
37
|
+
declare function parseDisplayPreferences(raw: string | null | undefined, defaults?: DisplayPreferenceValues): DisplayPreferenceValues;
|
|
38
|
+
/** The <html> attributes for a set of preferences, resolving `system` against the system. */
|
|
39
|
+
declare function resolveDisplayAttributes(prefs: DisplayPreferenceValues, system: SystemDisplayPreferences): DisplayAttributes;
|
|
40
|
+
/** Write (or remove) the attributes on an element, normally `document.documentElement`. */
|
|
41
|
+
declare function applyDisplayAttributes(root: Element, attributes: DisplayAttributes): void;
|
|
42
|
+
/** The system's answers from `matchMedia`; all false where there is none (SSR, tests). */
|
|
43
|
+
declare function readSystemDisplayPreferences(): SystemDisplayPreferences;
|
|
44
|
+
/**
|
|
45
|
+
* An inline script for the document head that applies the stored preferences before the first
|
|
46
|
+
* paint, so a dark or large-text page never flashes light or small (HAR-1372). Self-contained ES5;
|
|
47
|
+
* it never throws. Put it in a `<script>` before the stylesheets:
|
|
48
|
+
* <script dangerouslySetInnerHTML={{ __html: displayPreferencesScript() }} />
|
|
49
|
+
* It mirrors parseDisplayPreferences + resolveDisplayAttributes (tested to agree).
|
|
50
|
+
*/
|
|
51
|
+
declare function displayPreferencesScript(storageKey?: string, defaults?: DisplayPreferenceValues): string;
|
|
52
|
+
|
|
53
|
+
export { type ContrastPreference, DEFAULT_DISPLAY_PREFERENCES, DISPLAY_PREFERENCES_STORAGE_KEY, DISPLAY_PREFERENCE_OPTIONS, type DensityPreference, type DisplayAttributes, type DisplayPreferenceValues, type MotionPreference, type SystemDisplayPreferences, type TextScalePreference, type ThemePreference, applyDisplayAttributes, displayPreferencesScript, parseDisplayPreferences, readSystemDisplayPreferences, resolveDisplayAttributes };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const DEFAULT_DISPLAY_PREFERENCES = {
|
|
2
|
+
theme: "system",
|
|
3
|
+
contrast: "system",
|
|
4
|
+
textScale: "md",
|
|
5
|
+
motion: "system",
|
|
6
|
+
density: "standard"
|
|
7
|
+
};
|
|
8
|
+
const DISPLAY_PREFERENCES_STORAGE_KEY = "uix-display-preferences";
|
|
9
|
+
const DISPLAY_PREFERENCE_OPTIONS = {
|
|
10
|
+
theme: ["light", "dark", "system"],
|
|
11
|
+
contrast: ["standard", "more", "system"],
|
|
12
|
+
textScale: ["sm", "md", "lg", "xl"],
|
|
13
|
+
motion: ["system", "reduce"],
|
|
14
|
+
density: ["compact", "standard", "comfortable"]
|
|
15
|
+
};
|
|
16
|
+
function parseDisplayPreferences(raw, defaults = DEFAULT_DISPLAY_PREFERENCES) {
|
|
17
|
+
let stored = {};
|
|
18
|
+
try {
|
|
19
|
+
stored = raw ? JSON.parse(raw) ?? {} : {};
|
|
20
|
+
} catch {
|
|
21
|
+
stored = {};
|
|
22
|
+
}
|
|
23
|
+
if (typeof stored !== "object") stored = {};
|
|
24
|
+
const pick = (key) => {
|
|
25
|
+
const value = stored[key];
|
|
26
|
+
return DISPLAY_PREFERENCE_OPTIONS[key].includes(value) ? value : defaults[key];
|
|
27
|
+
};
|
|
28
|
+
return { theme: pick("theme"), contrast: pick("contrast"), textScale: pick("textScale"), motion: pick("motion"), density: pick("density") };
|
|
29
|
+
}
|
|
30
|
+
function resolveDisplayAttributes(prefs, system) {
|
|
31
|
+
const theme = prefs.theme === "system" ? system.dark ? "dark" : "light" : prefs.theme;
|
|
32
|
+
const more = prefs.contrast === "more" || prefs.contrast === "system" && system.moreContrast;
|
|
33
|
+
return {
|
|
34
|
+
"data-theme": theme,
|
|
35
|
+
"data-contrast": more ? "more" : void 0,
|
|
36
|
+
"data-text-scale": prefs.textScale === "md" ? void 0 : prefs.textScale,
|
|
37
|
+
"data-motion": prefs.motion === "reduce" ? "reduce" : void 0,
|
|
38
|
+
"data-density": prefs.density === "standard" ? void 0 : prefs.density
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function applyDisplayAttributes(root, attributes) {
|
|
42
|
+
for (const name of ["data-theme", "data-contrast", "data-text-scale", "data-motion", "data-density"]) {
|
|
43
|
+
const value = attributes[name];
|
|
44
|
+
if (value == null) root.removeAttribute(name);
|
|
45
|
+
else if (root.getAttribute(name) !== value) root.setAttribute(name, value);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function readSystemDisplayPreferences() {
|
|
49
|
+
const mm = (query) => typeof matchMedia === "function" && matchMedia(query).matches;
|
|
50
|
+
return { dark: mm("(prefers-color-scheme: dark)"), moreContrast: mm("(prefers-contrast: more)") };
|
|
51
|
+
}
|
|
52
|
+
function displayPreferencesScript(storageKey = DISPLAY_PREFERENCES_STORAGE_KEY, defaults = DEFAULT_DISPLAY_PREFERENCES) {
|
|
53
|
+
return `(function(){try{var d=document.documentElement,p={};try{p=JSON.parse(localStorage.getItem(${JSON.stringify(storageKey)})||'{}')||{}}catch(e){}if(typeof p!=='object')p={};var D=${JSON.stringify(defaults)},O=${JSON.stringify(DISPLAY_PREFERENCE_OPTIONS)};for(var k in O)if(O[k].indexOf(p[k])<0)p[k]=D[k];var m=function(q){return !!(window.matchMedia&&window.matchMedia(q).matches)};var s=function(n,v){if(v)d.setAttribute(n,v);else d.removeAttribute(n)};s('data-theme',p.theme==='light'||p.theme==='dark'?p.theme:(m('(prefers-color-scheme: dark)')?'dark':'light'));s('data-contrast',p.contrast==='more'||(p.contrast!=='standard'&&m('(prefers-contrast: more)'))?'more':'');s('data-text-scale',p.textScale==='sm'||p.textScale==='lg'||p.textScale==='xl'?p.textScale:'');s('data-motion',p.motion==='reduce'?'reduce':'');s('data-density',p.density==='compact'||p.density==='comfortable'?p.density:'')}catch(e){}})();`;
|
|
54
|
+
}
|
|
55
|
+
export {
|
|
56
|
+
DEFAULT_DISPLAY_PREFERENCES,
|
|
57
|
+
DISPLAY_PREFERENCES_STORAGE_KEY,
|
|
58
|
+
DISPLAY_PREFERENCE_OPTIONS,
|
|
59
|
+
applyDisplayAttributes,
|
|
60
|
+
displayPreferencesScript,
|
|
61
|
+
parseDisplayPreferences,
|
|
62
|
+
readSystemDisplayPreferences,
|
|
63
|
+
resolveDisplayAttributes
|
|
64
|
+
};
|
package/dist/emoji.cjs
CHANGED
|
@@ -500,6 +500,8 @@ function Popover({
|
|
|
500
500
|
};
|
|
501
501
|
const onKeyDown = (event) => {
|
|
502
502
|
if (event.key !== "Escape" || !isPopoverOpen(el)) return;
|
|
503
|
+
event.preventDefault();
|
|
504
|
+
event.stopPropagation();
|
|
503
505
|
window.clearTimeout(timer);
|
|
504
506
|
const hadFocus = el.contains(el.ownerDocument.activeElement);
|
|
505
507
|
try {
|
|
@@ -1122,32 +1124,21 @@ function useKbdPlatform(platform) {
|
|
|
1122
1124
|
const detected = (0, import_react5.useSyncExternalStore)(noSubscription, detectPlatform, serverPlatform);
|
|
1123
1125
|
return platform ?? detected;
|
|
1124
1126
|
}
|
|
1125
|
-
var ARIA_KEYS = {
|
|
1126
|
-
Mod: { mac: "Meta", other: "Control" },
|
|
1127
|
-
Ctrl: { mac: "Control", other: "Control" },
|
|
1128
|
-
Esc: { mac: "Escape", other: "Escape" },
|
|
1129
|
-
Del: { mac: "Delete", other: "Delete" },
|
|
1130
|
-
" ": { mac: "Space", other: "Space" },
|
|
1131
|
-
"+": { mac: "Plus", other: "Plus" }
|
|
1132
|
-
};
|
|
1133
|
-
function ariaKeyShortcuts(keys, platform) {
|
|
1134
|
-
return keys.map((key) => ARIA_KEYS[key]?.[platform] ?? (key.length === 1 ? key.toUpperCase() : key)).join("+");
|
|
1135
|
-
}
|
|
1136
1127
|
var isKeyShortcut = (value) => Array.isArray(value) && value.length > 0 && value.every((key) => typeof key === "string");
|
|
1128
|
+
function spokenKeys(keys, platform, keyLabels) {
|
|
1129
|
+
return keys.map((key) => keyLabels?.[key] ?? KEYS[key]?.[platform]?.[1] ?? key).join("+");
|
|
1130
|
+
}
|
|
1137
1131
|
function KbdCombo({ keys, platform, keyLabels, className, ...props }) {
|
|
1138
1132
|
const active = useKbdPlatform(platform);
|
|
1139
|
-
const parts = keys.map((key) => {
|
|
1140
|
-
const known = KEYS[key]?.[active];
|
|
1141
|
-
return { glyph: known?.[0] ?? key, spoken: keyLabels?.[key] ?? known?.[1] ?? key };
|
|
1142
|
-
});
|
|
1143
1133
|
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: cx("uix-kbd-combo", className), ...props, children: [
|
|
1144
|
-
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "uix-visually-hidden", children:
|
|
1145
|
-
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "uix-kbd-combo__keys", "aria-hidden": "true", children:
|
|
1134
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "uix-visually-hidden", children: spokenKeys(keys, active, keyLabels) }),
|
|
1135
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "uix-kbd-combo__keys", "aria-hidden": "true", children: keys.map((key, i) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("kbd", { className: "uix-kbd", children: KEYS[key]?.[active]?.[0] ?? key }, `${key}-${i}`)) })
|
|
1146
1136
|
] });
|
|
1147
1137
|
}
|
|
1148
1138
|
|
|
1149
1139
|
// src/components/Tooltip.tsx
|
|
1150
1140
|
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
1141
|
+
var useIsomorphicLayoutEffect4 = typeof document !== "undefined" ? import_react6.useLayoutEffect : import_react6.useEffect;
|
|
1151
1142
|
function Tooltip({ label, content, placement = "top", shortcut, className, children, ...props }) {
|
|
1152
1143
|
const triggerRef = (0, import_react6.useRef)(null);
|
|
1153
1144
|
const bubbleRef = (0, import_react6.useRef)(null);
|
|
@@ -1158,7 +1149,6 @@ function Tooltip({ label, content, placement = "top", shortcut, className, child
|
|
|
1158
1149
|
const childShortcut = (0, import_react6.isValidElement)(children) ? children.props.shortcut : void 0;
|
|
1159
1150
|
const own = shortcut ?? (isKeyShortcut(childShortcut) ? childShortcut : void 0);
|
|
1160
1151
|
const keys = own && own.length > 0 ? own : null;
|
|
1161
|
-
const keyshortcuts = keys ? ariaKeyShortcuts(keys, platform) : void 0;
|
|
1162
1152
|
useAnchoredPosition(triggerRef, bubbleRef, { open, placement, offset: 6 });
|
|
1163
1153
|
const cancelClose = () => {
|
|
1164
1154
|
if (closeTimer.current != null) {
|
|
@@ -1192,13 +1182,16 @@ function Tooltip({ label, content, placement = "top", shortcut, className, child
|
|
|
1192
1182
|
cancelClose();
|
|
1193
1183
|
closeTimer.current = window.setTimeout(hide, 100);
|
|
1194
1184
|
};
|
|
1195
|
-
(
|
|
1196
|
-
if (!open) return;
|
|
1185
|
+
useIsomorphicLayoutEffect4(() => {
|
|
1186
|
+
if (!open) return void 0;
|
|
1197
1187
|
const onKey = (e) => {
|
|
1198
|
-
if (e.key
|
|
1188
|
+
if (e.key !== "Escape") return;
|
|
1189
|
+
e.preventDefault();
|
|
1190
|
+
e.stopPropagation();
|
|
1191
|
+
hide();
|
|
1199
1192
|
};
|
|
1200
|
-
document.addEventListener("keydown", onKey);
|
|
1201
|
-
return () => document.removeEventListener("keydown", onKey);
|
|
1193
|
+
document.addEventListener("keydown", onKey, true);
|
|
1194
|
+
return () => document.removeEventListener("keydown", onKey, true);
|
|
1202
1195
|
}, [open]);
|
|
1203
1196
|
(0, import_react6.useEffect)(() => () => {
|
|
1204
1197
|
if (closeTimer.current != null) clearTimeout(closeTimer.current);
|
|
@@ -1211,30 +1204,27 @@ function Tooltip({ label, content, placement = "top", shortcut, className, child
|
|
|
1211
1204
|
}
|
|
1212
1205
|
}, []);
|
|
1213
1206
|
const bubbleAttr = { popover: "manual" };
|
|
1207
|
+
const text = content ?? label;
|
|
1208
|
+
const description = keys ? [text != null ? `${id}-text` : null, `${id}-keys`].filter(Boolean).join(" ") : id;
|
|
1214
1209
|
const single = (0, import_react6.isValidElement)(children);
|
|
1215
1210
|
const child = single ? (0, import_react6.cloneElement)(children, {
|
|
1216
|
-
"aria-describedby": [children.props["aria-describedby"],
|
|
1217
|
-
...keyshortcuts ? { "aria-keyshortcuts": keyshortcuts } : {}
|
|
1211
|
+
"aria-describedby": [children.props["aria-describedby"], description].filter(Boolean).join(" ")
|
|
1218
1212
|
}) : children;
|
|
1219
1213
|
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
1220
1214
|
"span",
|
|
1221
1215
|
{
|
|
1222
1216
|
ref: triggerRef,
|
|
1223
1217
|
className: cx("uix-tooltip", className),
|
|
1224
|
-
"aria-describedby": single ? void 0 :
|
|
1225
|
-
"aria-keyshortcuts": single ? void 0 : keyshortcuts,
|
|
1218
|
+
"aria-describedby": single ? void 0 : description,
|
|
1226
1219
|
tabIndex: single ? void 0 : 0,
|
|
1227
1220
|
onMouseEnter: show,
|
|
1228
1221
|
onMouseLeave: scheduleHide,
|
|
1229
1222
|
onFocus: show,
|
|
1230
1223
|
onBlur: hide,
|
|
1231
|
-
onKeyDown: (e) => {
|
|
1232
|
-
if (e.key === "Escape") hide();
|
|
1233
|
-
},
|
|
1234
1224
|
...props,
|
|
1235
1225
|
children: [
|
|
1236
1226
|
child,
|
|
1237
|
-
/* @__PURE__ */ (0, import_jsx_runtime6.
|
|
1227
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1238
1228
|
"span",
|
|
1239
1229
|
{
|
|
1240
1230
|
ref: bubbleRef,
|
|
@@ -1244,10 +1234,13 @@ function Tooltip({ label, content, placement = "top", shortcut, className, child
|
|
|
1244
1234
|
onMouseEnter: cancelClose,
|
|
1245
1235
|
onMouseLeave: scheduleHide,
|
|
1246
1236
|
...bubbleAttr,
|
|
1247
|
-
children: [
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1237
|
+
children: keys ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
|
|
1238
|
+
text != null && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { id: `${id}-text`, children: text }),
|
|
1239
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "uix-tooltip__shortcut", children: [
|
|
1240
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { id: `${id}-keys`, className: "uix-visually-hidden", children: spokenKeys(keys, platform) }),
|
|
1241
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(KbdCombo, { keys: [...keys], platform, "aria-hidden": "true" })
|
|
1242
|
+
] })
|
|
1243
|
+
] }) : text
|
|
1251
1244
|
}
|
|
1252
1245
|
)
|
|
1253
1246
|
]
|
package/dist/filter-model.d.ts
CHANGED
|
@@ -9,6 +9,12 @@ interface FilterChoice {
|
|
|
9
9
|
}
|
|
10
10
|
type TextFilterOperator = 'contains' | 'not-contains' | 'is' | 'is-not' | 'starts-with';
|
|
11
11
|
type NumberFilterOperator = 'eq' | 'neq' | 'lt' | 'lte' | 'gt' | 'gte' | 'between';
|
|
12
|
+
/**
|
|
13
|
+
* A date condition (HAR-1832). `on`, `before` and `after` take one day in `from`; `between` takes
|
|
14
|
+
* `from` and `to`, both inclusive. "Before" excludes the day and "after" starts the next day:
|
|
15
|
+
* `dateFilterBounds` turns each into the half-open range a query needs.
|
|
16
|
+
*/
|
|
17
|
+
type DateFilterOperator = 'on' | 'before' | 'after' | 'between';
|
|
12
18
|
type FilterValue = {
|
|
13
19
|
kind: 'enum';
|
|
14
20
|
values: string[];
|
|
@@ -25,12 +31,24 @@ type FilterValue = {
|
|
|
25
31
|
kind: 'date-range';
|
|
26
32
|
from?: string;
|
|
27
33
|
to?: string;
|
|
34
|
+
}
|
|
35
|
+
/** A date with a condition (HAR-1832); `YYYY-MM-DD`. A saved `between` stays `between`, even of one day. */
|
|
36
|
+
| {
|
|
37
|
+
kind: 'date';
|
|
38
|
+
operator: DateFilterOperator;
|
|
39
|
+
from?: string;
|
|
40
|
+
to?: string;
|
|
28
41
|
} | {
|
|
29
42
|
kind: 'boolean';
|
|
30
43
|
value?: boolean;
|
|
31
44
|
} | {
|
|
32
45
|
kind: 'reference';
|
|
33
46
|
values: FilterChoice[];
|
|
47
|
+
}
|
|
48
|
+
/** Free-text values typed in as tags (HAR-1832), e.g. several hostnames; matched by the product. */
|
|
49
|
+
| {
|
|
50
|
+
kind: 'tags';
|
|
51
|
+
values: string[];
|
|
34
52
|
};
|
|
35
53
|
type FilterValueKind = FilterValue['kind'];
|
|
36
54
|
/**
|
|
@@ -52,8 +70,15 @@ interface FilterField {
|
|
|
52
70
|
options?: readonly FilterChoice[];
|
|
53
71
|
/** `reference`: finds records to pick; rejected promises show the error state. */
|
|
54
72
|
onSearch?: (query: string) => Promise<readonly FilterChoice[]>;
|
|
73
|
+
/**
|
|
74
|
+
* `reference`: labels for chosen ids that arrived without one (a saved view restores ids).
|
|
75
|
+
* The editor asks once for every id whose label is missing or equal to the id.
|
|
76
|
+
*/
|
|
77
|
+
resolveLabels?: (ids: readonly string[]) => Promise<readonly FilterChoice[]>;
|
|
55
78
|
/** `text` / `number`: the operators offered, in order. Default: all of the kind. */
|
|
56
79
|
operators?: readonly (TextFilterOperator | NumberFilterOperator)[];
|
|
80
|
+
/** `date`: the conditions offered, in order. Default: on, before, after, between. */
|
|
81
|
+
dateOperators?: readonly DateFilterOperator[];
|
|
57
82
|
/** `number`: a unit shown after the value, e.g. "days". */
|
|
58
83
|
unit?: string;
|
|
59
84
|
/** `boolean`: words for true and false, e.g. Escalated / Not escalated. Default from labels. */
|
|
@@ -80,14 +105,45 @@ interface FilterSummaryLabels {
|
|
|
80
105
|
more: string;
|
|
81
106
|
/** An `enum` value equal to a preset: `{label}` (the preset), `{field}`. Default `{label}`. */
|
|
82
107
|
preset?: string;
|
|
108
|
+
/** `date` conditions (HAR-1832), `{date}`. Default "{date}", "before {date}", "after {date}". */
|
|
109
|
+
on?: string;
|
|
110
|
+
before?: string;
|
|
111
|
+
after?: string;
|
|
83
112
|
}
|
|
84
113
|
declare const DEFAULT_FILTER_SUMMARY_LABELS: FilterSummaryLabels;
|
|
85
114
|
declare const TEXT_FILTER_OPERATORS: readonly TextFilterOperator[];
|
|
86
115
|
declare const NUMBER_FILTER_OPERATORS: readonly NumberFilterOperator[];
|
|
116
|
+
declare const DATE_FILTER_OPERATORS: readonly DateFilterOperator[];
|
|
87
117
|
/** The empty value for a field kind (what Clear resets to). */
|
|
88
118
|
declare function emptyFilterValue(kind: FilterValueKind): FilterValue;
|
|
89
119
|
/** True when the value filters nothing (so the chip goes away). */
|
|
90
120
|
declare function isFilterEmpty(value: FilterValue | undefined): boolean;
|
|
121
|
+
/**
|
|
122
|
+
* A range whose ends were typed the wrong way round (from 10 Oct to 1 Oct, 7 to 2) put in order,
|
|
123
|
+
* so it means what the person meant instead of matching nothing (PR #168 review). Number
|
|
124
|
+
* `between`, date `between` and `date-range`; every other value comes back as it is. Internal:
|
|
125
|
+
* FilterEditor applies through it, `dateFilterBounds` and `summarizeFilter` read through it.
|
|
126
|
+
*/
|
|
127
|
+
declare function orderFilterRange<V extends FilterValue | undefined>(value: V): V;
|
|
128
|
+
/**
|
|
129
|
+
* The half-open range a `date` filter means, for a query (HAR-1832): `{ from }` is the first day
|
|
130
|
+
* included, `{ until }` the first day NOT included, either may be missing. "On 5 Oct" is
|
|
131
|
+
* `[5 Oct, 6 Oct)`, "before 5 Oct" is `[…, 5 Oct)`, "after 5 Oct" is `[6 Oct, …)`, "between 1 and
|
|
132
|
+
* 5 Oct" is `[1 Oct, 6 Oct)`. A `date-range` value reads as `between` with optional ends. Empty
|
|
133
|
+
* values give `{}`.
|
|
134
|
+
*/
|
|
135
|
+
declare function dateFilterBounds(input: FilterValue | undefined): {
|
|
136
|
+
from?: string;
|
|
137
|
+
until?: string;
|
|
138
|
+
};
|
|
139
|
+
/** True for a chosen reference whose label is missing or only repeats its id (restored from an id). */
|
|
140
|
+
declare const isUnresolvedChoice: (choice: FilterChoice) => boolean;
|
|
141
|
+
/**
|
|
142
|
+
* Fills in the labels of a `reference` value restored from ids (HAR-1832), with
|
|
143
|
+
* `field.resolveLabels`. Other values, and values whose labels are all known, come back as they
|
|
144
|
+
* are. Ids the resolver does not return keep the label they had.
|
|
145
|
+
*/
|
|
146
|
+
declare function resolveFilterLabels(field: FilterField, value: FilterValue | undefined): Promise<FilterValue | undefined>;
|
|
91
147
|
/**
|
|
92
148
|
* The first of `field.presets` whose values equal the value's as a set, or undefined. Only an
|
|
93
149
|
* `enum` value can match a preset.
|
|
@@ -104,9 +160,9 @@ interface SummarizeFilterOptions {
|
|
|
104
160
|
}
|
|
105
161
|
/**
|
|
106
162
|
* One line for the filter chip, e.g. "State: Open, Pending", "Priority ≥ 2", "Created:
|
|
107
|
-
* 01.10.2026 – 05.10.2026", "Escalated: Yes". An enum value equal to one of `field.presets`
|
|
163
|
+
* 01.10.2026 – 05.10.2026", "Created: before 05.10.2026", "Host: db-01, db-02 +1", "Escalated: Yes". An enum value equal to one of `field.presets`
|
|
108
164
|
* gives that preset's label (the `preset` label). Empty values give an empty string.
|
|
109
165
|
*/
|
|
110
|
-
declare function summarizeFilter(field: FilterField,
|
|
166
|
+
declare function summarizeFilter(field: FilterField, input: FilterValue | undefined, options?: SummarizeFilterOptions): string;
|
|
111
167
|
|
|
112
|
-
export { DEFAULT_FILTER_SUMMARY_LABELS, type FilterChoice, type FilterField, type FilterPreset, type FilterSummaryLabels, type FilterValue, type FilterValueKind, NUMBER_FILTER_OPERATORS, type NumberFilterOperator, type SummarizeFilterOptions, TEXT_FILTER_OPERATORS, type TextFilterOperator, emptyFilterValue, isFilterEmpty, matchFilterPreset, summarizeFilter };
|
|
168
|
+
export { DATE_FILTER_OPERATORS, DEFAULT_FILTER_SUMMARY_LABELS, type DateFilterOperator, type FilterChoice, type FilterField, type FilterPreset, type FilterSummaryLabels, type FilterValue, type FilterValueKind, NUMBER_FILTER_OPERATORS, type NumberFilterOperator, type SummarizeFilterOptions, TEXT_FILTER_OPERATORS, type TextFilterOperator, dateFilterBounds, emptyFilterValue, isFilterEmpty, isUnresolvedChoice, matchFilterPreset, orderFilterRange, resolveFilterLabels, summarizeFilter };
|
package/dist/filter-model.js
CHANGED
|
@@ -19,10 +19,14 @@ const DEFAULT_FILTER_SUMMARY_LABELS = {
|
|
|
19
19
|
from: "from {from}",
|
|
20
20
|
until: "until {to}",
|
|
21
21
|
more: "{first} +{count}",
|
|
22
|
-
preset: "{label}"
|
|
22
|
+
preset: "{label}",
|
|
23
|
+
on: "{date}",
|
|
24
|
+
before: "before {date}",
|
|
25
|
+
after: "after {date}"
|
|
23
26
|
};
|
|
24
27
|
const TEXT_FILTER_OPERATORS = ["contains", "not-contains", "is", "is-not", "starts-with"];
|
|
25
28
|
const NUMBER_FILTER_OPERATORS = ["eq", "neq", "lt", "lte", "gt", "gte", "between"];
|
|
29
|
+
const DATE_FILTER_OPERATORS = ["on", "before", "after", "between"];
|
|
26
30
|
function emptyFilterValue(kind) {
|
|
27
31
|
switch (kind) {
|
|
28
32
|
case "enum":
|
|
@@ -33,10 +37,14 @@ function emptyFilterValue(kind) {
|
|
|
33
37
|
return { kind, operator: "eq" };
|
|
34
38
|
case "date-range":
|
|
35
39
|
return { kind };
|
|
40
|
+
case "date":
|
|
41
|
+
return { kind, operator: "on" };
|
|
36
42
|
case "boolean":
|
|
37
43
|
return { kind };
|
|
38
44
|
case "reference":
|
|
39
45
|
return { kind, values: [] };
|
|
46
|
+
case "tags":
|
|
47
|
+
return { kind, values: [] };
|
|
40
48
|
}
|
|
41
49
|
}
|
|
42
50
|
function isFilterEmpty(value) {
|
|
@@ -44,6 +52,7 @@ function isFilterEmpty(value) {
|
|
|
44
52
|
switch (value.kind) {
|
|
45
53
|
case "enum":
|
|
46
54
|
case "reference":
|
|
55
|
+
case "tags":
|
|
47
56
|
return value.values.length === 0;
|
|
48
57
|
case "text":
|
|
49
58
|
return value.text.trim() === "";
|
|
@@ -51,10 +60,53 @@ function isFilterEmpty(value) {
|
|
|
51
60
|
return value.value === void 0 || value.operator === "between" && value.to === void 0;
|
|
52
61
|
case "date-range":
|
|
53
62
|
return !value.from && !value.to;
|
|
63
|
+
case "date":
|
|
64
|
+
return !value.from || value.operator === "between" && !value.to;
|
|
54
65
|
case "boolean":
|
|
55
66
|
return value.value === void 0;
|
|
56
67
|
}
|
|
57
68
|
}
|
|
69
|
+
function addDays(date, days) {
|
|
70
|
+
const [y, m, d] = date.split("-").map(Number);
|
|
71
|
+
const next = new Date(Date.UTC(y, m - 1, d + days));
|
|
72
|
+
return next.toISOString().slice(0, 10);
|
|
73
|
+
}
|
|
74
|
+
function orderFilterRange(value) {
|
|
75
|
+
if (!value) return value;
|
|
76
|
+
if (value.kind === "number" && value.operator === "between" && value.value !== void 0 && value.to !== void 0 && value.value > value.to) {
|
|
77
|
+
return { ...value, value: value.to, to: value.value };
|
|
78
|
+
}
|
|
79
|
+
if ((value.kind === "date-range" || value.kind === "date" && value.operator === "between") && value.from && value.to && value.from > value.to) {
|
|
80
|
+
return { ...value, from: value.to, to: value.from };
|
|
81
|
+
}
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
function dateFilterBounds(input) {
|
|
85
|
+
const value = orderFilterRange(input);
|
|
86
|
+
if (value?.kind === "date-range") {
|
|
87
|
+
return { ...value.from ? { from: value.from } : {}, ...value.to ? { until: addDays(value.to, 1) } : {} };
|
|
88
|
+
}
|
|
89
|
+
if (value?.kind !== "date" || isFilterEmpty(value)) return {};
|
|
90
|
+
const day = value.from;
|
|
91
|
+
switch (value.operator) {
|
|
92
|
+
case "on":
|
|
93
|
+
return { from: day, until: addDays(day, 1) };
|
|
94
|
+
case "before":
|
|
95
|
+
return { until: day };
|
|
96
|
+
case "after":
|
|
97
|
+
return { from: addDays(day, 1) };
|
|
98
|
+
case "between":
|
|
99
|
+
return { from: day, until: addDays(value.to, 1) };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const isUnresolvedChoice = (choice) => !choice.label || choice.label === choice.value;
|
|
103
|
+
async function resolveFilterLabels(field, value) {
|
|
104
|
+
if (value?.kind !== "reference" || !field.resolveLabels) return value;
|
|
105
|
+
const missing = value.values.filter(isUnresolvedChoice).map((v) => v.value);
|
|
106
|
+
if (missing.length === 0) return value;
|
|
107
|
+
const resolved = new Map((await field.resolveLabels(missing)).map((c) => [c.value, c.label]));
|
|
108
|
+
return { kind: "reference", values: value.values.map((v) => resolved.has(v.value) ? { value: v.value, label: resolved.get(v.value) } : v) };
|
|
109
|
+
}
|
|
58
110
|
function matchFilterPreset(field, value) {
|
|
59
111
|
if (value?.kind !== "enum" || !field.presets?.length) return void 0;
|
|
60
112
|
const chosen = new Set(value.values);
|
|
@@ -64,7 +116,8 @@ function matchFilterPreset(field, value) {
|
|
|
64
116
|
});
|
|
65
117
|
}
|
|
66
118
|
const fill = (template, values) => template.replace(/\{(\w+)\}/g, (match, key) => key in values ? String(values[key]) : match);
|
|
67
|
-
function summarizeFilter(field,
|
|
119
|
+
function summarizeFilter(field, input, options = {}) {
|
|
120
|
+
const value = orderFilterRange(input);
|
|
68
121
|
if (!value || isFilterEmpty(value)) return "";
|
|
69
122
|
const labels = { ...DEFAULT_FILTER_SUMMARY_LABELS, ...options.labels, operators: { ...DEFAULT_FILTER_SUMMARY_LABELS.operators, ...options.labels?.operators } };
|
|
70
123
|
const date = options.formatDate ?? ((d) => d);
|
|
@@ -82,22 +135,34 @@ function summarizeFilter(field, value, options = {}) {
|
|
|
82
135
|
}
|
|
83
136
|
case "reference":
|
|
84
137
|
return `${field.label}: ${list(value.values.map((v) => v.label))}`;
|
|
138
|
+
case "tags":
|
|
139
|
+
return `${field.label}: ${list(value.values)}`;
|
|
85
140
|
case "text":
|
|
86
141
|
return `${field.label} ${labels.operators[value.operator]} \u201C${value.text.trim()}\u201D`;
|
|
87
142
|
case "number":
|
|
88
143
|
return value.operator === "between" ? `${field.label}: ${fill(labels.range, { from: num(value.value), to: num(value.to) })}` : `${field.label} ${labels.operators[value.operator]} ${num(value.value)}`;
|
|
89
144
|
case "date-range":
|
|
90
145
|
return `${field.label}: ${value.from && value.to ? fill(labels.range, { from: date(value.from), to: date(value.to) }) : value.from ? fill(labels.from, { from: date(value.from) }) : fill(labels.until, { to: date(value.to) })}`;
|
|
146
|
+
case "date": {
|
|
147
|
+
if (value.operator === "between") return `${field.label}: ${fill(labels.range, { from: date(value.from), to: date(value.to) })}`;
|
|
148
|
+
const template = labels[value.operator] ?? DEFAULT_FILTER_SUMMARY_LABELS[value.operator];
|
|
149
|
+
return `${field.label}: ${fill(template, { date: date(value.from) })}`;
|
|
150
|
+
}
|
|
91
151
|
case "boolean":
|
|
92
152
|
return `${field.label}: ${value.value ? field.trueLabel ?? labels.yes : field.falseLabel ?? labels.no}`;
|
|
93
153
|
}
|
|
94
154
|
}
|
|
95
155
|
export {
|
|
156
|
+
DATE_FILTER_OPERATORS,
|
|
96
157
|
DEFAULT_FILTER_SUMMARY_LABELS,
|
|
97
158
|
NUMBER_FILTER_OPERATORS,
|
|
98
159
|
TEXT_FILTER_OPERATORS,
|
|
160
|
+
dateFilterBounds,
|
|
99
161
|
emptyFilterValue,
|
|
100
162
|
isFilterEmpty,
|
|
163
|
+
isUnresolvedChoice,
|
|
101
164
|
matchFilterPreset,
|
|
165
|
+
orderFilterRange,
|
|
166
|
+
resolveFilterLabels,
|
|
102
167
|
summarizeFilter
|
|
103
168
|
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
|
|
3
|
+
interface ChangeHighlightOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Said through the page's polite live region when the value changes, e.g. "Status changed to
|
|
6
|
+
* Resolved by A. Novak". The highlight is the signal and its place is the location; this is
|
|
7
|
+
* the description for people who cannot see either (motion standard §2.5).
|
|
8
|
+
*/
|
|
9
|
+
announce?: string;
|
|
10
|
+
/** Turn the highlight off, e.g. while the person is editing the value themselves. */
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Flash `.uix-changed` on an element when `value` changes after mount — never on the first
|
|
15
|
+
* render (HAR-2585). A second change restarts the highlight. Compare by `Object.is`: pass a
|
|
16
|
+
* primitive (a status, a count, an `updatedAt`), not a fresh object.
|
|
17
|
+
*
|
|
18
|
+
* ```tsx
|
|
19
|
+
* const ref = useChangeHighlight<HTMLTableCellElement>(row.status, { announce: `Status: ${row.status}` });
|
|
20
|
+
* <Td ref={ref}>{row.status}</Td>
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* The hook sets `data-uix-changed` on the element (React never renders that attribute, so a
|
|
24
|
+
* re-render cannot clear it) and removes it when the animation ends.
|
|
25
|
+
*/
|
|
26
|
+
declare function useChangeHighlight<T extends HTMLElement = HTMLElement>(value: unknown, { announce, disabled }?: ChangeHighlightOptions): RefObject<T | null>;
|
|
27
|
+
/**
|
|
28
|
+
* Flash `.uix-changed` on `el` now (restarting a highlight already running). For changes that
|
|
29
|
+
* do not flow through a React value, e.g. a row a live update replaced.
|
|
30
|
+
*/
|
|
31
|
+
declare function flashChanged(el: HTMLElement): void;
|
|
32
|
+
|
|
33
|
+
export { type ChangeHighlightOptions, flashChanged, useChangeHighlight };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import { announcePolite, ensureAnnouncer } from "../announce.js";
|
|
4
|
+
function useChangeHighlight(value, { announce, disabled } = {}) {
|
|
5
|
+
const ref = useRef(null);
|
|
6
|
+
const previous = useRef(value);
|
|
7
|
+
const announces = announce !== void 0;
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
if (announces) ensureAnnouncer(ref.current);
|
|
10
|
+
}, [announces]);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (Object.is(previous.current, value)) return;
|
|
13
|
+
previous.current = value;
|
|
14
|
+
const el = ref.current;
|
|
15
|
+
if (disabled || !el) return;
|
|
16
|
+
flashChanged(el);
|
|
17
|
+
if (announce) announcePolite(announce, el);
|
|
18
|
+
}, [value, announce, disabled]);
|
|
19
|
+
return ref;
|
|
20
|
+
}
|
|
21
|
+
const handlers = /* @__PURE__ */ new WeakMap();
|
|
22
|
+
function flashChanged(el) {
|
|
23
|
+
if (el.hasAttribute("data-uix-changed")) {
|
|
24
|
+
el.removeAttribute("data-uix-changed");
|
|
25
|
+
void el.offsetWidth;
|
|
26
|
+
}
|
|
27
|
+
el.setAttribute("data-uix-changed", "");
|
|
28
|
+
const previous = handlers.get(el);
|
|
29
|
+
if (previous) el.removeEventListener("animationend", previous);
|
|
30
|
+
const done = (event) => {
|
|
31
|
+
const target = event.target;
|
|
32
|
+
if (!event.animationName.startsWith("uix-changed")) return;
|
|
33
|
+
if (target !== el && target?.parentNode !== el) return;
|
|
34
|
+
el.removeAttribute("data-uix-changed");
|
|
35
|
+
el.removeEventListener("animationend", done);
|
|
36
|
+
handlers.delete(el);
|
|
37
|
+
};
|
|
38
|
+
handlers.set(el, done);
|
|
39
|
+
el.addEventListener("animationend", done);
|
|
40
|
+
}
|
|
41
|
+
export {
|
|
42
|
+
flashChanged,
|
|
43
|
+
useChangeHighlight
|
|
44
|
+
};
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
* `keepOpen` is asked when the browser closed the dialog on its own (Escape or a close request
|
|
3
3
|
* nobody cancelled, a `method="dialog"` form). Returning `true` reopens it instead of calling
|
|
4
4
|
* `onClose`: a form dialog with unsaved input or a save in flight (HAR-2029).
|
|
5
|
+
*
|
|
6
|
+
* The dialog opens and closes in a layout effect (HAR-1831). A dialog its caller unmounts while
|
|
7
|
+
* open (`{open && <ConfirmDialog …/>}`, or a page that navigates away) is closed in the commit,
|
|
8
|
+
* while it is still in the document, so the browser returns focus to the control that opened it;
|
|
9
|
+
* a passive cleanup ran after the element was removed, and focus fell to `<body>`.
|
|
5
10
|
*/
|
|
6
11
|
declare function useDialog(open: boolean, onClose?: () => void, keepOpen?: () => boolean): React.RefObject<HTMLDialogElement>;
|
|
7
12
|
|
package/dist/hooks/useDialog.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { useRef, useEffect } from "react";
|
|
2
|
+
import { useRef, useEffect, useLayoutEffect } from "react";
|
|
3
|
+
const useIsomorphicLayoutEffect = typeof document !== "undefined" ? useLayoutEffect : useEffect;
|
|
3
4
|
let lockCount = 0;
|
|
4
5
|
let saved = null;
|
|
5
6
|
function lockBodyScroll() {
|
|
@@ -36,7 +37,7 @@ function useDialog(open, onClose, keepOpen) {
|
|
|
36
37
|
onCloseRef.current = onClose;
|
|
37
38
|
const keepOpenRef = useRef(keepOpen);
|
|
38
39
|
keepOpenRef.current = keepOpen;
|
|
39
|
-
|
|
40
|
+
useIsomorphicLayoutEffect(() => {
|
|
40
41
|
const el = ref.current;
|
|
41
42
|
if (!el || !open) {
|
|
42
43
|
if (el && el.open) el.close();
|