actual-css 0.2.0 → 0.3.1
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/CHANGELOG.md +49 -1
- package/README.md +24 -9
- package/dist/actual-themes.min.css +1 -0
- package/dist/actual.css +1532 -1114
- package/dist/actual.js +1 -3
- package/dist/actual.min.css +1 -1
- package/dist/optional.css +1048 -0
- package/dist/optional.min.css +1 -0
- package/docs/design-notes/browser-support.md +94 -14
- package/docs/design-notes/cascade-layer.md +7 -6
- package/docs/design-notes/enhancement-contract.md +11 -14
- package/docs/design-notes/forced-colors.md +105 -0
- package/docs/design-notes/naming.md +2 -1
- package/docs/design-notes/platform-alignment.md +492 -0
- package/docs/design-notes/references.md +8 -0
- package/docs/design-notes/widget-primitives.md +37 -1
- package/docs/navigation.json +135 -0
- package/docs/pages/components/accordion.md +80 -0
- package/docs/pages/components/alert.md +203 -0
- package/docs/pages/components/aura.md +35 -0
- package/docs/pages/components/avatar.md +95 -0
- package/docs/pages/components/badge.md +102 -0
- package/docs/pages/components/breadcrumb.md +19 -0
- package/docs/pages/components/button.md +210 -0
- package/docs/pages/components/card.md +195 -0
- package/docs/pages/components/chat.md +50 -0
- package/docs/pages/components/dialog.md +367 -0
- package/docs/pages/components/drawer.md +113 -0
- package/docs/pages/components/fab.md +118 -0
- package/docs/pages/components/key.md +33 -0
- package/docs/pages/components/meter.md +23 -0
- package/docs/pages/components/navbar.md +92 -0
- package/docs/pages/components/pagination.md +55 -0
- package/docs/pages/components/progress.md +29 -0
- package/docs/pages/components/skeleton.md +38 -0
- package/docs/pages/components/spinner.md +93 -0
- package/docs/pages/components/table.md +102 -0
- package/docs/pages/enhancements/flyout.md +391 -0
- package/docs/pages/enhancements/overview.md +36 -0
- package/docs/pages/enhancements/scrollspy.md +71 -0
- package/docs/pages/enhancements/status-bar.md +72 -0
- package/docs/pages/enhancements/tabs.md +80 -0
- package/docs/pages/enhancements/tooltip.md +130 -0
- package/docs/pages/examples/overview.md +43 -0
- package/docs/pages/forms/choice-cards.md +102 -0
- package/docs/pages/forms/controls.md +32 -0
- package/docs/pages/forms/detached-actions.md +59 -0
- package/docs/pages/forms/floating-field.md +88 -0
- package/docs/pages/forms/form-actions.md +56 -0
- package/docs/pages/forms/input-policy.md +91 -0
- package/docs/pages/forms/joined-controls.md +37 -0
- package/docs/pages/forms/labels.md +42 -0
- package/docs/pages/forms/otp.md +50 -0
- package/docs/pages/forms/overview.md +188 -0
- package/docs/pages/forms/password.md +40 -0
- package/docs/pages/forms/range.md +17 -0
- package/docs/pages/forms/states-and-sizes.md +321 -0
- package/docs/pages/forms/sticky-actions.md +187 -0
- package/docs/pages/forms/switch.md +31 -0
- package/docs/pages/forms/validation.md +241 -0
- package/docs/{mental-model.md → pages/foundations/philosophy.md} +33 -18
- package/docs/pages/foundations/theming.md +99 -0
- package/docs/pages/foundations/tokens.md +421 -0
- package/docs/{typography.md → pages/foundations/typography.md} +27 -9
- package/docs/pages/guides/bootstrap.md +438 -0
- package/docs/pages/guides/introduction.md +157 -0
- package/docs/pages/guides/optional-components.md +33 -0
- package/docs/pages/guides/progressive-enhancement.md +686 -0
- package/docs/pages/guides/tailwind.md +455 -0
- package/docs/pages/layout/app-shell.md +25 -0
- package/docs/pages/layout/center.md +29 -0
- package/docs/pages/layout/cluster.md +59 -0
- package/docs/pages/layout/frame.md +30 -0
- package/docs/pages/layout/grid.md +142 -0
- package/docs/pages/layout/list.md +41 -0
- package/docs/pages/layout/media.md +28 -0
- package/docs/pages/layout/overview.md +37 -0
- package/docs/pages/layout/scroll-snap.md +54 -0
- package/docs/pages/layout/sidebar-layout.md +27 -0
- package/docs/pages/layout/stack.md +55 -0
- package/docs/pages/layout/switcher.md +36 -0
- package/docs/pages/layout/topbar.md +23 -0
- package/docs/pages/patterns/actions.md +52 -0
- package/docs/pages/patterns/code-block.md +36 -0
- package/docs/pages/patterns/exclusive-accordion.md +16 -0
- package/docs/pages/patterns/hierarchical-nav.md +26 -0
- package/docs/pages/patterns/icon-input.md +25 -0
- package/docs/pages/patterns/intent-nesting.md +3 -0
- package/docs/pages/patterns/navigation-list.md +235 -0
- package/docs/pages/patterns/overview.md +58 -0
- package/docs/pages/patterns/structured-lists.md +47 -0
- package/docs/pages/utilities/accessibility.md +34 -0
- package/docs/pages/utilities/flex.md +105 -0
- package/docs/pages/utilities/links.md +18 -0
- package/docs/pages/utilities/list.md +32 -0
- package/docs/pages/utilities/overflow.md +43 -0
- package/docs/pages/utilities/overview.md +18 -0
- package/docs/pages/utilities/shape.md +25 -0
- package/docs/pages/utilities/sizing-wrapping.md +115 -0
- package/docs/pages/utilities/spacing.md +79 -0
- package/docs/pages/utilities/text.md +43 -0
- package/llms.txt +4 -4
- package/package.json +21 -12
- package/src/css/actual.css +16 -2
- package/src/css/base.css +3 -5
- package/src/css/components/accordion.css +18 -30
- package/src/css/components/actions.css +4 -1
- package/src/css/components/alert.css +139 -102
- package/src/css/components/avatar.css +16 -10
- package/src/css/components/badge.css +69 -42
- package/src/css/components/breadcrumb.css +7 -15
- package/src/css/components/busy.css +6 -0
- package/src/css/components/button.css +84 -34
- package/src/css/components/card.css +26 -38
- package/src/css/components/drawer.css +30 -13
- package/src/css/components/flyout.css +39 -109
- package/src/css/components/index.css +1 -0
- package/src/css/components/join.css +14 -2
- package/src/css/components/key.css +2 -0
- package/src/css/components/menu.css +125 -0
- package/src/css/components/meter.css +0 -23
- package/src/css/components/modal.css +162 -7
- package/src/css/components/navbar.css +30 -23
- package/src/css/components/overline.css +8 -9
- package/src/css/components/pagination.css +1 -1
- package/src/css/components/progress.css +8 -26
- package/src/css/components/skeleton.css +7 -0
- package/src/css/components/spinner.css +6 -7
- package/src/css/components/status-bar.css +15 -24
- package/src/css/components/surface.css +2 -8
- package/src/css/components/tab.css +19 -48
- package/src/css/components/table.css +7 -5
- package/src/css/components/tooltip.css +14 -7
- package/src/css/focus.css +14 -38
- package/src/css/forms/choice-card.css +13 -14
- package/src/css/forms/choice.css +21 -27
- package/src/css/forms/control.css +18 -17
- package/src/css/forms/custom-select.css +9 -11
- package/src/css/forms/form-actions.css +14 -3
- package/src/css/forms/form.css +7 -4
- package/src/css/forms/index.css +4 -4
- package/src/css/forms/input-icon.css +30 -12
- package/src/css/forms/native.css +50 -11
- package/src/css/forms/range.css +17 -9
- package/src/css/forms/select.css +8 -8
- package/src/css/forms/switch.css +14 -12
- package/src/css/forms/validation.css +32 -5
- package/src/css/grid.css +32 -18
- package/src/css/intents.css +2 -2
- package/src/css/layout.css +45 -2
- package/src/css/optional/aura.css +93 -0
- package/src/css/optional/chat.css +105 -0
- package/src/css/optional/fab.css +104 -0
- package/src/css/optional/floating-field.css +87 -0
- package/src/css/optional/index.css +5 -0
- package/src/css/optional/layout-extra.css +7 -4
- package/src/css/optional/otp.css +115 -0
- package/src/css/optional/scroll-snap.css +6 -0
- package/src/css/optional/scroller.css +13 -7
- package/src/css/optional/typography-fluid.css +5 -0
- package/src/css/optional/utilities-extra.css +17 -2
- package/src/css/prose.css +18 -8
- package/src/css/reset.css +0 -10
- package/src/css/theme.css +21 -5
- package/src/css/themes/cyberpunk.css +1 -1
- package/src/css/themes/dim.css +3 -3
- package/src/css/themes/edge.css +8 -10
- package/src/css/themes/index.css +8 -4
- package/src/css/themes/{dark.css → indigo.css} +5 -4
- package/src/css/themes/neon.css +5 -5
- package/src/css/themes/square.css +0 -3
- package/src/css/tokens.css +74 -20
- package/src/css/utilities.css +8 -10
- package/src/css/variants.css +51 -66
- package/src/js/command.js +1 -3
- package/src/js/context-menu.js +67 -35
- package/src/js/dialog.js +68 -79
- package/src/js/dismiss.js +15 -0
- package/src/js/enhance.js +54 -23
- package/src/js/enhancement-loader.js +301 -0
- package/src/js/events.js +3 -0
- package/src/js/filter.js +3 -3
- package/src/js/floating.js +101 -131
- package/src/js/flyout.js +104 -87
- package/src/js/focus-group.js +116 -0
- package/src/js/focus.js +6 -1
- package/src/js/index.js +4 -2
- package/src/js/input.js +1 -1
- package/src/js/keys.js +27 -0
- package/src/js/mask.js +7 -6
- package/src/js/menu.js +87 -34
- package/src/js/parse-config.js +47 -0
- package/src/js/password.js +9 -1
- package/src/js/scrollspy.js +12 -4
- package/src/js/selectors.js +0 -7
- package/src/js/status.js +3 -1
- package/src/js/surface.js +187 -136
- package/src/js/tab.js +36 -53
- package/src/js/tooltip.js +70 -35
- package/src/js/validation.js +34 -8
- package/dist/actual.js.map +0 -31
- package/docs/README.md +0 -28
- package/docs/components.md +0 -860
- package/docs/design-notes/previous-work.md +0 -5
- package/docs/forms.md +0 -1265
- package/docs/javascript.md +0 -496
- package/docs/layout.md +0 -480
- package/docs/patterns.md +0 -504
- package/docs/reserved.md +0 -7
- package/docs/tokens.md +0 -455
- package/docs/ui.md +0 -929
- package/docs/utilities.md +0 -384
- package/src/css/dialog-fallback.css +0 -56
- package/src/js/dialog-fallback.js +0 -163
package/src/js/tab.js
CHANGED
|
@@ -16,12 +16,17 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { registerEnhancement } from "./enhance.js";
|
|
19
|
-
import {
|
|
19
|
+
import { itemForKey } from "./keys.js";
|
|
20
20
|
|
|
21
21
|
const TABLIST_SELECTOR = '[data-enhance~="tabs"]';
|
|
22
22
|
|
|
23
|
+
// A tablist owns only the tabs directly beneath it, not those of a nested
|
|
24
|
+
// tablist. Filtering by closest owner keeps the outer list from operating on
|
|
25
|
+
// (and re-reacting to events from) tabs that belong to an inner list.
|
|
23
26
|
function tabsOf(list) {
|
|
24
|
-
return [...list.querySelectorAll('[role="tab"]')]
|
|
27
|
+
return [...list.querySelectorAll('[role="tab"]')].filter(
|
|
28
|
+
(tab) => tab.closest(TABLIST_SELECTOR) === list,
|
|
29
|
+
);
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
function panelsOf(tabs) {
|
|
@@ -35,7 +40,7 @@ function panelsOf(tabs) {
|
|
|
35
40
|
|
|
36
41
|
function panelFor(tab) {
|
|
37
42
|
const panelId = tab.getAttribute("aria-controls");
|
|
38
|
-
return panelId ?
|
|
43
|
+
return panelId ? tab.ownerDocument.getElementById(panelId) : null;
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
function isTabDisabled(tab) {
|
|
@@ -89,68 +94,46 @@ function initialize(list) {
|
|
|
89
94
|
}
|
|
90
95
|
|
|
91
96
|
function onKeydown(e) {
|
|
97
|
+
const list = e.currentTarget;
|
|
92
98
|
const tab = e.target.closest('[role="tab"]');
|
|
93
|
-
if (!tab) return;
|
|
94
|
-
const list = tab.closest(TABLIST_SELECTOR);
|
|
95
|
-
if (!list) return;
|
|
99
|
+
if (!tab || tab.closest(TABLIST_SELECTOR) !== list) return;
|
|
96
100
|
|
|
97
101
|
const tabs = activatableTabs(list);
|
|
98
102
|
if (!tabs.length || !tabs.includes(tab)) return;
|
|
99
103
|
const isVertical = list.getAttribute("aria-orientation") === "vertical";
|
|
100
|
-
let next;
|
|
101
104
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
e.preventDefault();
|
|
106
|
-
next = nextItem(tabs, tab, 1, { wrap: true });
|
|
107
|
-
if (next) activateAndFocus(next);
|
|
108
|
-
break;
|
|
109
|
-
case "ArrowLeft":
|
|
110
|
-
if (isVertical) break;
|
|
111
|
-
e.preventDefault();
|
|
112
|
-
next = nextItem(tabs, tab, -1, { wrap: true });
|
|
113
|
-
if (next) activateAndFocus(next);
|
|
114
|
-
break;
|
|
115
|
-
case "ArrowDown":
|
|
116
|
-
if (!isVertical) {
|
|
117
|
-
e.preventDefault();
|
|
118
|
-
const panel = panelFor(tab);
|
|
119
|
-
if (panel) {
|
|
120
|
-
makePanelFocusable(panel);
|
|
121
|
-
panel.focus();
|
|
122
|
-
}
|
|
123
|
-
break;
|
|
124
|
-
}
|
|
125
|
-
e.preventDefault();
|
|
126
|
-
next = nextItem(tabs, tab, 1, { wrap: true });
|
|
127
|
-
if (next) activateAndFocus(next);
|
|
128
|
-
break;
|
|
129
|
-
case "ArrowUp":
|
|
130
|
-
if (!isVertical) break;
|
|
131
|
-
e.preventDefault();
|
|
132
|
-
next = nextItem(tabs, tab, -1, { wrap: true });
|
|
133
|
-
if (next) activateAndFocus(next);
|
|
134
|
-
break;
|
|
135
|
-
case "Home":
|
|
136
|
-
e.preventDefault();
|
|
137
|
-
activateAndFocus(firstItem(tabs));
|
|
138
|
-
break;
|
|
139
|
-
case "End":
|
|
140
|
-
e.preventDefault();
|
|
141
|
-
activateAndFocus(lastItem(tabs));
|
|
142
|
-
break;
|
|
143
|
-
case "Enter":
|
|
144
|
-
case " ":
|
|
105
|
+
if (e.key === "ArrowDown" && !isVertical) {
|
|
106
|
+
const panel = panelFor(tab);
|
|
107
|
+
if (panel) {
|
|
145
108
|
e.preventDefault();
|
|
146
|
-
|
|
147
|
-
|
|
109
|
+
makePanelFocusable(panel);
|
|
110
|
+
panel.focus();
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
116
|
+
e.preventDefault();
|
|
117
|
+
activate(tab);
|
|
118
|
+
return;
|
|
148
119
|
}
|
|
120
|
+
|
|
121
|
+
const direction = list.ownerDocument.defaultView?.getComputedStyle(list).direction || "ltr";
|
|
122
|
+
const target = itemForKey(tabs, tab, e.key, {
|
|
123
|
+
orientation: isVertical ? "vertical" : "horizontal",
|
|
124
|
+
wrap: true,
|
|
125
|
+
direction,
|
|
126
|
+
});
|
|
127
|
+
if (!target) return;
|
|
128
|
+
|
|
129
|
+
e.preventDefault();
|
|
130
|
+
activateAndFocus(target);
|
|
149
131
|
}
|
|
150
132
|
|
|
151
133
|
function onClick(e) {
|
|
134
|
+
const list = e.currentTarget;
|
|
152
135
|
const tab = e.target.closest('[role="tab"]');
|
|
153
|
-
if (!tab) return;
|
|
136
|
+
if (!tab || tab.closest(TABLIST_SELECTOR) !== list) return;
|
|
154
137
|
if (isTabDisabled(tab)) {
|
|
155
138
|
e.preventDefault();
|
|
156
139
|
return;
|
package/src/js/tooltip.js
CHANGED
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
* treated as a tooltip trigger.
|
|
15
15
|
*
|
|
16
16
|
* Sharing: an explicit tooltip may be referenced by several triggers. The
|
|
17
|
-
* tooltip element is wired once
|
|
18
|
-
*
|
|
19
|
-
* when the last trigger leaves the DOM.
|
|
17
|
+
* tooltip element is wired once; each trigger only registers
|
|
18
|
+
* itself. Positional tracking runs only while the tip is visible,
|
|
19
|
+
* and all wiring is torn down when the last trigger leaves the DOM.
|
|
20
20
|
*
|
|
21
21
|
* Lazy: tooltip elements are created (shorthand) or wired (explicit) on
|
|
22
22
|
* first mouseover / focusin. No page-load scan, no DOM overhead
|
|
@@ -28,8 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
import enhance from "./enhance.js";
|
|
31
|
-
import {
|
|
32
|
-
import { reposition, track } from "./floating.js";
|
|
31
|
+
import { autoUpdate, reposition } from "./floating.js";
|
|
33
32
|
import { CLASSES } from "./selectors.js";
|
|
34
33
|
|
|
35
34
|
const SHOW_DELAY_MS = 150;
|
|
@@ -40,19 +39,20 @@ const HIDE_DELAY_MS = 100;
|
|
|
40
39
|
* @property {Set<Element>} refs Triggers that currently reference this tip.
|
|
41
40
|
* @property {Element | null} activeRef Trigger that last showed this tip.
|
|
42
41
|
* @property {AbortController} controller Tip-level event listener cleanup.
|
|
43
|
-
* @property {() => void}
|
|
42
|
+
* @property {(() => void) | null} stopTracking Floating-position cleanup while visible.
|
|
44
43
|
* @property {boolean} generated True when the tip was created from data-tooltip text.
|
|
45
44
|
* @property {{ parent: Node, next: ChildNode | null } | null} mount Original DOM position for explicit tips moved into a dialog/body root.
|
|
46
45
|
* @property {ReturnType<typeof setTimeout> | null} timer Pending delayed show.
|
|
47
46
|
* @property {ReturnType<typeof setTimeout> | null} hideTimer Pending delayed hide.
|
|
48
|
-
* @property {boolean}
|
|
47
|
+
* @property {boolean} hovered Pointer is over the active trigger.
|
|
48
|
+
* @property {boolean} focused The active trigger has keyboard focus.
|
|
49
49
|
* @property {boolean} overTip Pointer is over the tooltip.
|
|
50
50
|
*/
|
|
51
51
|
|
|
52
52
|
let uid = 0;
|
|
53
53
|
const triggerStates = new WeakMap(); // trigger -> { tip, cleanup }
|
|
54
54
|
/** @type {WeakMap<Element, TooltipState>} */
|
|
55
|
-
const tipStates = new WeakMap(); // tip -> { refs, activeRef, controller,
|
|
55
|
+
const tipStates = new WeakMap(); // tip -> { refs, activeRef, controller, stopTracking, generated, mount, timer }
|
|
56
56
|
|
|
57
57
|
function mountTip(tip, trigger) {
|
|
58
58
|
const root = trigger.closest("dialog") || trigger.ownerDocument.body;
|
|
@@ -132,7 +132,7 @@ function nextTooltipId(doc) {
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
function repositionTip(ref, tip) {
|
|
135
|
-
reposition(ref, tip, {
|
|
135
|
+
return reposition(ref, tip, {
|
|
136
136
|
placement: placementFor(ref),
|
|
137
137
|
distance: 6,
|
|
138
138
|
flip: true,
|
|
@@ -140,6 +140,22 @@ function repositionTip(ref, tip) {
|
|
|
140
140
|
});
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
function startTracking(tip, state) {
|
|
144
|
+
if (state.stopTracking) return;
|
|
145
|
+
|
|
146
|
+
state.stopTracking = autoUpdate(tip, () => {
|
|
147
|
+
if (isAlwaysVisible(state.activeRef)) tip.hidden = false;
|
|
148
|
+
if (state.activeRef && !tip.hidden && !repositionTip(state.activeRef, tip)) {
|
|
149
|
+
hideTip(tip, true);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function stopTracking(state) {
|
|
155
|
+
state?.stopTracking?.();
|
|
156
|
+
if (state) state.stopTracking = null;
|
|
157
|
+
}
|
|
158
|
+
|
|
143
159
|
function hideTip(tip, force = false) {
|
|
144
160
|
const state = tipStates.get(tip);
|
|
145
161
|
if (!force && isAlwaysVisible(state?.activeRef)) return;
|
|
@@ -152,6 +168,7 @@ function hideTip(tip, force = false) {
|
|
|
152
168
|
state.hideTimer = null;
|
|
153
169
|
}
|
|
154
170
|
tip.hidden = true;
|
|
171
|
+
stopTracking(state);
|
|
155
172
|
}
|
|
156
173
|
|
|
157
174
|
function clearHide(tip) {
|
|
@@ -165,11 +182,11 @@ function scheduleHide(tip) {
|
|
|
165
182
|
const state = tipStates.get(tip);
|
|
166
183
|
if (!state) return;
|
|
167
184
|
if (isClickTrigger(state.activeRef) || isAlwaysVisible(state.activeRef)) return;
|
|
168
|
-
if (state.
|
|
185
|
+
if (state.hovered || state.focused || state.overTip) return;
|
|
169
186
|
if (state.hideTimer) clearTimeout(state.hideTimer);
|
|
170
187
|
state.hideTimer = setTimeout(() => {
|
|
171
188
|
state.hideTimer = null;
|
|
172
|
-
if (!state.
|
|
189
|
+
if (!state.hovered && !state.focused && !state.overTip) hideTip(tip);
|
|
173
190
|
}, HIDE_DELAY_MS);
|
|
174
191
|
}
|
|
175
192
|
|
|
@@ -185,25 +202,16 @@ function wireTip(tip, options = {}) {
|
|
|
185
202
|
refs: new Set(),
|
|
186
203
|
activeRef: null,
|
|
187
204
|
controller,
|
|
188
|
-
|
|
205
|
+
stopTracking: null,
|
|
189
206
|
generated: options.generated === true,
|
|
190
207
|
mount: options.mount || null,
|
|
191
208
|
timer: null,
|
|
192
209
|
hideTimer: null,
|
|
193
|
-
|
|
210
|
+
hovered: false,
|
|
211
|
+
focused: false,
|
|
194
212
|
overTip: false,
|
|
195
213
|
};
|
|
196
214
|
|
|
197
|
-
const onHide = () => hideTip(tip);
|
|
198
|
-
const onOutOfView = () => hideTip(tip, true);
|
|
199
|
-
const onReposition = () => {
|
|
200
|
-
if (isAlwaysVisible(state.activeRef)) tip.hidden = false;
|
|
201
|
-
if (!tip.hidden && state.activeRef) repositionTip(state.activeRef, tip);
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
tip.addEventListener(EVENTS.reposition, onReposition, { signal: controller.signal });
|
|
205
|
-
tip.addEventListener(EVENTS.hide, onHide, { signal: controller.signal });
|
|
206
|
-
tip.addEventListener(EVENTS.outOfView, onOutOfView, { signal: controller.signal });
|
|
207
215
|
tip.addEventListener(
|
|
208
216
|
"mouseenter",
|
|
209
217
|
() => {
|
|
@@ -221,6 +229,17 @@ function wireTip(tip, options = {}) {
|
|
|
221
229
|
{ signal: controller.signal },
|
|
222
230
|
);
|
|
223
231
|
|
|
232
|
+
tip.ownerDocument.addEventListener(
|
|
233
|
+
"keydown",
|
|
234
|
+
(event) => {
|
|
235
|
+
if (event.key !== "Escape" || event.ctrlKey || event.altKey || event.shiftKey) return;
|
|
236
|
+
if (tip.hidden) return;
|
|
237
|
+
event.preventDefault();
|
|
238
|
+
hideTip(tip);
|
|
239
|
+
},
|
|
240
|
+
{ signal: controller.signal },
|
|
241
|
+
);
|
|
242
|
+
|
|
224
243
|
tipStates.set(tip, state);
|
|
225
244
|
return state;
|
|
226
245
|
}
|
|
@@ -256,10 +275,11 @@ function ensureTip(trigger) {
|
|
|
256
275
|
if (!tip) {
|
|
257
276
|
tip = explicitTipFor(trigger);
|
|
258
277
|
if (!tip) return null;
|
|
259
|
-
tip.
|
|
260
|
-
|
|
261
|
-
|
|
278
|
+
// Wire and mount the shared tip only on first retain. A later trigger
|
|
279
|
+
// must not re-hide a tooltip that is currently visible for another.
|
|
262
280
|
if (!tipStates.has(tip)) {
|
|
281
|
+
tip.hidden = true;
|
|
282
|
+
tip.style.position = "fixed";
|
|
263
283
|
mount = mountTip(tip, trigger);
|
|
264
284
|
}
|
|
265
285
|
}
|
|
@@ -268,12 +288,22 @@ function ensureTip(trigger) {
|
|
|
268
288
|
state.refs.add(trigger);
|
|
269
289
|
|
|
270
290
|
const triggerController = new AbortController();
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
291
|
+
trigger.addEventListener(
|
|
292
|
+
"mouseleave",
|
|
293
|
+
() => {
|
|
294
|
+
state.hovered = false;
|
|
295
|
+
scheduleHide(tip);
|
|
296
|
+
},
|
|
297
|
+
{ signal: triggerController.signal },
|
|
298
|
+
);
|
|
299
|
+
trigger.addEventListener(
|
|
300
|
+
"blur",
|
|
301
|
+
() => {
|
|
302
|
+
state.focused = false;
|
|
303
|
+
scheduleHide(tip);
|
|
304
|
+
},
|
|
305
|
+
{ signal: triggerController.signal },
|
|
306
|
+
);
|
|
277
307
|
|
|
278
308
|
const cleanup = () => {
|
|
279
309
|
triggerController.abort();
|
|
@@ -290,7 +320,7 @@ function ensureTip(trigger) {
|
|
|
290
320
|
|
|
291
321
|
hideTip(tip, true);
|
|
292
322
|
state.controller.abort();
|
|
293
|
-
state
|
|
323
|
+
stopTracking(state);
|
|
294
324
|
tipStates.delete(tip);
|
|
295
325
|
if (state.generated) tip.remove();
|
|
296
326
|
else restoreTip(tip, state);
|
|
@@ -310,13 +340,13 @@ function show(tip, ref, immediate = false) {
|
|
|
310
340
|
if (!state) return;
|
|
311
341
|
|
|
312
342
|
state.activeRef = ref;
|
|
313
|
-
state.overRef = true;
|
|
314
343
|
clearHide(tip);
|
|
315
344
|
if (state.timer) clearTimeout(state.timer);
|
|
316
345
|
const reveal = () => {
|
|
317
346
|
state.timer = null;
|
|
318
347
|
tip.hidden = false;
|
|
319
348
|
repositionTip(ref, tip);
|
|
349
|
+
startTracking(tip, state);
|
|
320
350
|
};
|
|
321
351
|
|
|
322
352
|
if (immediate) reveal();
|
|
@@ -339,7 +369,12 @@ function handleTriggerIntent(e) {
|
|
|
339
369
|
return;
|
|
340
370
|
|
|
341
371
|
const tip = ensureTip(trigger);
|
|
342
|
-
if (tip)
|
|
372
|
+
if (!tip) return;
|
|
373
|
+
|
|
374
|
+
const state = tipStates.get(tip);
|
|
375
|
+
if (e.type === "focusin") state.focused = true;
|
|
376
|
+
else state.hovered = true;
|
|
377
|
+
show(tip, trigger);
|
|
343
378
|
}
|
|
344
379
|
|
|
345
380
|
function handleTriggerClick(e) {
|
package/src/js/validation.js
CHANGED
|
@@ -74,8 +74,11 @@ const rules = {
|
|
|
74
74
|
return false;
|
|
75
75
|
}
|
|
76
76
|
},
|
|
77
|
+
// A user-entered decimal: optional sign, digits with optional fraction,
|
|
78
|
+
// optional exponent. Rejects JS-isms that Number() would accept (0x10,
|
|
79
|
+
// Infinity, scientific shorthand, whitespace).
|
|
77
80
|
number(v) {
|
|
78
|
-
return
|
|
81
|
+
return /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(v);
|
|
79
82
|
},
|
|
80
83
|
digits(v) {
|
|
81
84
|
return /^\d+$/.test(v);
|
|
@@ -106,18 +109,28 @@ function fieldContainer(el) {
|
|
|
106
109
|
return el.closest?.(`.${FIELD_CLASS}`) || null;
|
|
107
110
|
}
|
|
108
111
|
|
|
109
|
-
// .field.danger
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
112
|
+
// .field.danger added by validation is tracked in a WeakSet so that only
|
|
113
|
+
// validation-owned state is removed. A .danger applied manually by app code
|
|
114
|
+
// for an unrelated reason is left alone: validation claims ownership only
|
|
115
|
+
// when it introduces the class itself.
|
|
116
|
+
const dangerOwned = new WeakSet();
|
|
117
|
+
|
|
118
|
+
function markDangerOwned(field) {
|
|
119
|
+
if (!field.classList.contains(DANGER_CLASS)) {
|
|
120
|
+
dangerOwned.add(field);
|
|
121
|
+
}
|
|
122
|
+
field.classList.add(DANGER_CLASS);
|
|
123
|
+
}
|
|
124
|
+
|
|
113
125
|
function syncFieldDangerState(el) {
|
|
114
126
|
const field = fieldContainer(el);
|
|
115
127
|
if (!field) return;
|
|
116
128
|
|
|
117
129
|
if (field.querySelector('[aria-invalid="true"], [data-validation-errors]')) {
|
|
118
|
-
field
|
|
119
|
-
} else {
|
|
130
|
+
markDangerOwned(field);
|
|
131
|
+
} else if (dangerOwned.has(field)) {
|
|
120
132
|
field.classList.remove(DANGER_CLASS);
|
|
133
|
+
dangerOwned.delete(field);
|
|
121
134
|
}
|
|
122
135
|
}
|
|
123
136
|
|
|
@@ -181,7 +194,10 @@ function errorEl(el) {
|
|
|
181
194
|
|
|
182
195
|
function markInvalid(el) {
|
|
183
196
|
el.setAttribute("aria-invalid", "true");
|
|
184
|
-
fieldContainer(el)
|
|
197
|
+
const field = fieldContainer(el);
|
|
198
|
+
if (field) {
|
|
199
|
+
markDangerOwned(field);
|
|
200
|
+
}
|
|
185
201
|
}
|
|
186
202
|
|
|
187
203
|
function markValid(el) {
|
|
@@ -322,6 +338,10 @@ function connectForm(form) {
|
|
|
322
338
|
{ signal: controller.signal },
|
|
323
339
|
);
|
|
324
340
|
|
|
341
|
+
// Disconnect contract: release resources only. Validation-owned classes,
|
|
342
|
+
// aria-invalid, custom validity, and rendered errors are intentionally left
|
|
343
|
+
// in place, so a move/remove/reinsert of the form does not erase business
|
|
344
|
+
// state. A full reset goes through the reset event, not teardown.
|
|
325
345
|
const cleanup = () => {
|
|
326
346
|
controller.abort();
|
|
327
347
|
releaseManagedNoValidate(form);
|
|
@@ -338,6 +358,12 @@ export class FormValidator {
|
|
|
338
358
|
}
|
|
339
359
|
|
|
340
360
|
static registerRule(name, callback) {
|
|
361
|
+
if (typeof name !== "string" || name.trim() === "") {
|
|
362
|
+
throw new TypeError("registerRule() requires a non-empty rule name.");
|
|
363
|
+
}
|
|
364
|
+
if (typeof callback !== "function") {
|
|
365
|
+
throw new TypeError(`registerRule("${name}") requires a function callback.`);
|
|
366
|
+
}
|
|
341
367
|
rules[name] = callback;
|
|
342
368
|
}
|
|
343
369
|
|