actual-css 0.2.0 → 0.3.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/CHANGELOG.md +38 -1
- package/README.md +24 -9
- package/dist/actual-themes.min.css +1 -0
- package/dist/actual.css +1244 -915
- 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 +1 -1
- 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 +205 -0
- package/docs/pages/components/card.md +183 -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 +41 -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 +92 -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 +57 -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 +21 -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 +18 -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 +31 -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 +20 -12
- package/src/css/actual.css +12 -1
- 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 +126 -99
- package/src/css/components/avatar.css +16 -10
- package/src/css/components/badge.css +55 -35
- package/src/css/components/breadcrumb.css +7 -15
- package/src/css/components/busy.css +6 -0
- package/src/css/components/button.css +78 -32
- package/src/css/components/card.css +17 -37
- 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 +17 -18
- package/src/css/components/overline.css +0 -7
- 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 +16 -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 +19 -18
- package/src/css/intents.css +2 -2
- package/src/css/layout.css +29 -1
- 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 +3 -3
- 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 +37 -65
- 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 +185 -136
- package/src/js/tab.js +36 -53
- package/src/js/tooltip.js +53 -31
- 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
|
@@ -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,12 +39,13 @@ 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} stopTracking Floating-position cleanup.
|
|
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
|
|
|
@@ -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,
|
|
@@ -165,11 +165,11 @@ function scheduleHide(tip) {
|
|
|
165
165
|
const state = tipStates.get(tip);
|
|
166
166
|
if (!state) return;
|
|
167
167
|
if (isClickTrigger(state.activeRef) || isAlwaysVisible(state.activeRef)) return;
|
|
168
|
-
if (state.
|
|
168
|
+
if (state.hovered || state.focused || state.overTip) return;
|
|
169
169
|
if (state.hideTimer) clearTimeout(state.hideTimer);
|
|
170
170
|
state.hideTimer = setTimeout(() => {
|
|
171
171
|
state.hideTimer = null;
|
|
172
|
-
if (!state.
|
|
172
|
+
if (!state.hovered && !state.focused && !state.overTip) hideTip(tip);
|
|
173
173
|
}, HIDE_DELAY_MS);
|
|
174
174
|
}
|
|
175
175
|
|
|
@@ -185,25 +185,21 @@ function wireTip(tip, options = {}) {
|
|
|
185
185
|
refs: new Set(),
|
|
186
186
|
activeRef: null,
|
|
187
187
|
controller,
|
|
188
|
-
|
|
188
|
+
stopTracking: autoUpdate(tip, () => {
|
|
189
|
+
if (isAlwaysVisible(state.activeRef)) tip.hidden = false;
|
|
190
|
+
if (!tip.hidden && state.activeRef) {
|
|
191
|
+
if (!repositionTip(state.activeRef, tip)) hideTip(tip, true);
|
|
192
|
+
}
|
|
193
|
+
}),
|
|
189
194
|
generated: options.generated === true,
|
|
190
195
|
mount: options.mount || null,
|
|
191
196
|
timer: null,
|
|
192
197
|
hideTimer: null,
|
|
193
|
-
|
|
198
|
+
hovered: false,
|
|
199
|
+
focused: false,
|
|
194
200
|
overTip: false,
|
|
195
201
|
};
|
|
196
202
|
|
|
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
203
|
tip.addEventListener(
|
|
208
204
|
"mouseenter",
|
|
209
205
|
() => {
|
|
@@ -221,6 +217,17 @@ function wireTip(tip, options = {}) {
|
|
|
221
217
|
{ signal: controller.signal },
|
|
222
218
|
);
|
|
223
219
|
|
|
220
|
+
tip.ownerDocument.addEventListener(
|
|
221
|
+
"keydown",
|
|
222
|
+
(event) => {
|
|
223
|
+
if (event.key !== "Escape" || event.ctrlKey || event.altKey || event.shiftKey) return;
|
|
224
|
+
if (tip.hidden) return;
|
|
225
|
+
event.preventDefault();
|
|
226
|
+
hideTip(tip);
|
|
227
|
+
},
|
|
228
|
+
{ signal: controller.signal },
|
|
229
|
+
);
|
|
230
|
+
|
|
224
231
|
tipStates.set(tip, state);
|
|
225
232
|
return state;
|
|
226
233
|
}
|
|
@@ -256,10 +263,11 @@ function ensureTip(trigger) {
|
|
|
256
263
|
if (!tip) {
|
|
257
264
|
tip = explicitTipFor(trigger);
|
|
258
265
|
if (!tip) return null;
|
|
259
|
-
tip.
|
|
260
|
-
|
|
261
|
-
|
|
266
|
+
// Wire and mount the shared tip only on first retain. A later trigger
|
|
267
|
+
// must not re-hide a tooltip that is currently visible for another.
|
|
262
268
|
if (!tipStates.has(tip)) {
|
|
269
|
+
tip.hidden = true;
|
|
270
|
+
tip.style.position = "fixed";
|
|
263
271
|
mount = mountTip(tip, trigger);
|
|
264
272
|
}
|
|
265
273
|
}
|
|
@@ -268,12 +276,22 @@ function ensureTip(trigger) {
|
|
|
268
276
|
state.refs.add(trigger);
|
|
269
277
|
|
|
270
278
|
const triggerController = new AbortController();
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
279
|
+
trigger.addEventListener(
|
|
280
|
+
"mouseleave",
|
|
281
|
+
() => {
|
|
282
|
+
state.hovered = false;
|
|
283
|
+
scheduleHide(tip);
|
|
284
|
+
},
|
|
285
|
+
{ signal: triggerController.signal },
|
|
286
|
+
);
|
|
287
|
+
trigger.addEventListener(
|
|
288
|
+
"blur",
|
|
289
|
+
() => {
|
|
290
|
+
state.focused = false;
|
|
291
|
+
scheduleHide(tip);
|
|
292
|
+
},
|
|
293
|
+
{ signal: triggerController.signal },
|
|
294
|
+
);
|
|
277
295
|
|
|
278
296
|
const cleanup = () => {
|
|
279
297
|
triggerController.abort();
|
|
@@ -290,7 +308,7 @@ function ensureTip(trigger) {
|
|
|
290
308
|
|
|
291
309
|
hideTip(tip, true);
|
|
292
310
|
state.controller.abort();
|
|
293
|
-
state.
|
|
311
|
+
state.stopTracking();
|
|
294
312
|
tipStates.delete(tip);
|
|
295
313
|
if (state.generated) tip.remove();
|
|
296
314
|
else restoreTip(tip, state);
|
|
@@ -310,7 +328,6 @@ function show(tip, ref, immediate = false) {
|
|
|
310
328
|
if (!state) return;
|
|
311
329
|
|
|
312
330
|
state.activeRef = ref;
|
|
313
|
-
state.overRef = true;
|
|
314
331
|
clearHide(tip);
|
|
315
332
|
if (state.timer) clearTimeout(state.timer);
|
|
316
333
|
const reveal = () => {
|
|
@@ -339,7 +356,12 @@ function handleTriggerIntent(e) {
|
|
|
339
356
|
return;
|
|
340
357
|
|
|
341
358
|
const tip = ensureTip(trigger);
|
|
342
|
-
if (tip)
|
|
359
|
+
if (!tip) return;
|
|
360
|
+
|
|
361
|
+
const state = tipStates.get(tip);
|
|
362
|
+
if (e.type === "focusin") state.focused = true;
|
|
363
|
+
else state.hovered = true;
|
|
364
|
+
show(tip, trigger);
|
|
343
365
|
}
|
|
344
366
|
|
|
345
367
|
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
|
|