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/flyout.js
CHANGED
|
@@ -1,51 +1,54 @@
|
|
|
1
|
-
|
|
2
|
-
* Flyout trigger — trigger adapter over the shared action-surface runtime.
|
|
3
|
-
*
|
|
4
|
-
* App menus keep roving focus and menuitem activation. Nav panels reuse the
|
|
5
|
-
* same surface lifecycle without menu semantics.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import enhance, { registerEnhancement } from "./enhance.js";
|
|
1
|
+
import { registerEnhancement } from "./enhance.js";
|
|
9
2
|
import { focusFirstDescendant } from "./focus.js";
|
|
10
|
-
import { focusFirstMenuItem, focusLastMenuItem
|
|
11
|
-
import { closeSurface, isSurfaceOpen, openSurface,
|
|
3
|
+
import { connectMenu, focusFirstMenuItem, focusLastMenuItem } from "./menu.js";
|
|
4
|
+
import { closeSurface, isSurfaceOpen, openSurface, retainSurface } from "./surface.js";
|
|
12
5
|
|
|
13
|
-
|
|
6
|
+
const panelRefs = new WeakMap();
|
|
14
7
|
const triggerMap = new WeakMap();
|
|
15
|
-
const FLYOUT_SELECTOR = '[data-enhance~="flyout"]';
|
|
16
|
-
const FLYOUT_TRIGGER_SELECTOR = "[aria-controls][aria-expanded]";
|
|
17
8
|
|
|
18
|
-
function
|
|
19
|
-
|
|
9
|
+
function normalizeBreakpoint(raw) {
|
|
10
|
+
const BREAKPOINTS = { sm: 640, md: 768, lg: 1024 };
|
|
11
|
+
const value = Number.parseInt(BREAKPOINTS[raw] || raw, 10);
|
|
12
|
+
return Number.isFinite(value) ? value : undefined;
|
|
20
13
|
}
|
|
21
14
|
|
|
22
|
-
function
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
|
|
15
|
+
function readFlyoutOptions(panel) {
|
|
16
|
+
const opts = { autoClose: "true" };
|
|
17
|
+
const ds = panel.dataset;
|
|
18
|
+
if (ds.flyoutPlacement) opts.placement = ds.flyoutPlacement;
|
|
19
|
+
if (ds.flyoutDistance != null) {
|
|
20
|
+
const d = Number.parseFloat(ds.flyoutDistance);
|
|
21
|
+
if (Number.isFinite(d)) opts.distance = d;
|
|
22
|
+
}
|
|
23
|
+
if (ds.flyoutMobile) opts.mobile = ds.flyoutMobile;
|
|
24
|
+
if (ds.flyoutBreakpoint) {
|
|
25
|
+
const bp = normalizeBreakpoint(ds.flyoutBreakpoint);
|
|
26
|
+
if (bp != null) opts.breakpoint = bp;
|
|
27
|
+
}
|
|
28
|
+
if (ds.flyoutAutoClose) opts.autoClose = ds.flyoutAutoClose;
|
|
29
|
+
return opts;
|
|
26
30
|
}
|
|
27
31
|
|
|
28
|
-
function
|
|
29
|
-
|
|
32
|
+
function panelFor(trigger) {
|
|
33
|
+
const id = trigger.getAttribute("aria-controls");
|
|
34
|
+
return id ? trigger.ownerDocument.getElementById(id) : null;
|
|
30
35
|
}
|
|
31
36
|
|
|
32
|
-
function
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const flyout = flyoutFor(trigger);
|
|
36
|
-
if (!flyout) return null;
|
|
37
|
+
function isMenuFlyout(panel) {
|
|
38
|
+
return panel.matches("menu") || panel.classList.contains("menu");
|
|
39
|
+
}
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
trigger
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
function openFlyout(panel, trigger) {
|
|
42
|
+
return openSurface(panel, {
|
|
43
|
+
trigger,
|
|
44
|
+
source: trigger,
|
|
45
|
+
...readFlyoutOptions(panel),
|
|
46
|
+
});
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
function openAndFocusPanel(flyout, trigger) {
|
|
47
|
-
if (!isSurfaceOpen(flyout)
|
|
48
|
-
focusFirstDescendant(flyout);
|
|
50
|
+
if (!isSurfaceOpen(flyout) && !openFlyout(flyout, trigger)) return false;
|
|
51
|
+
return focusFirstDescendant(flyout);
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
function onTriggerClick(e) {
|
|
@@ -53,35 +56,34 @@ function onTriggerClick(e) {
|
|
|
53
56
|
const state = triggerMap.get(trigger);
|
|
54
57
|
if (!state) return;
|
|
55
58
|
e.stopPropagation();
|
|
56
|
-
const
|
|
57
|
-
if (!
|
|
58
|
-
if (isSurfaceOpen(
|
|
59
|
-
else openFlyout(
|
|
59
|
+
const panel = resolvePanel(trigger, state);
|
|
60
|
+
if (!panel) return;
|
|
61
|
+
if (isSurfaceOpen(panel)) closeSurface(panel);
|
|
62
|
+
else openFlyout(panel, trigger);
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
function onTriggerKeydown(e) {
|
|
63
66
|
const trigger = e.currentTarget;
|
|
64
67
|
const state = triggerMap.get(trigger);
|
|
65
68
|
if (!state) return;
|
|
66
|
-
const
|
|
67
|
-
if (!
|
|
68
|
-
const
|
|
69
|
-
const isActionList = items.length > 0;
|
|
69
|
+
const panel = resolvePanel(trigger, state);
|
|
70
|
+
if (!panel) return;
|
|
71
|
+
const isActionList = isMenuFlyout(panel);
|
|
70
72
|
|
|
71
73
|
if (!isActionList) {
|
|
72
74
|
switch (e.key) {
|
|
73
75
|
case "ArrowDown":
|
|
74
76
|
e.preventDefault();
|
|
75
|
-
openAndFocusPanel(
|
|
77
|
+
openAndFocusPanel(panel, trigger);
|
|
76
78
|
break;
|
|
77
79
|
case "Enter":
|
|
78
80
|
case " ":
|
|
79
81
|
e.preventDefault();
|
|
80
|
-
if (isSurfaceOpen(
|
|
81
|
-
else openAndFocusPanel(
|
|
82
|
+
if (isSurfaceOpen(panel)) closeSurface(panel);
|
|
83
|
+
else openAndFocusPanel(panel, trigger);
|
|
82
84
|
break;
|
|
83
85
|
case "Tab":
|
|
84
|
-
if (isSurfaceOpen(
|
|
86
|
+
if (isSurfaceOpen(panel) && !e.shiftKey && focusFirstDescendant(panel)) {
|
|
85
87
|
e.preventDefault();
|
|
86
88
|
}
|
|
87
89
|
break;
|
|
@@ -92,89 +94,104 @@ function onTriggerKeydown(e) {
|
|
|
92
94
|
switch (e.key) {
|
|
93
95
|
case "ArrowDown":
|
|
94
96
|
e.preventDefault();
|
|
95
|
-
if (!isSurfaceOpen(
|
|
96
|
-
focusFirstMenuItem(
|
|
97
|
+
if (!isSurfaceOpen(panel) && !openFlyout(panel, trigger)) break;
|
|
98
|
+
focusFirstMenuItem(panel);
|
|
97
99
|
break;
|
|
98
100
|
case "ArrowUp":
|
|
99
101
|
e.preventDefault();
|
|
100
|
-
if (!isSurfaceOpen(
|
|
101
|
-
focusLastMenuItem(
|
|
102
|
+
if (!isSurfaceOpen(panel) && !openFlyout(panel, trigger)) break;
|
|
103
|
+
focusLastMenuItem(panel);
|
|
102
104
|
break;
|
|
103
105
|
case "Home":
|
|
104
106
|
e.preventDefault();
|
|
105
|
-
if (!isSurfaceOpen(
|
|
106
|
-
focusFirstMenuItem(
|
|
107
|
+
if (!isSurfaceOpen(panel) && !openFlyout(panel, trigger)) break;
|
|
108
|
+
focusFirstMenuItem(panel);
|
|
107
109
|
break;
|
|
108
110
|
case "End":
|
|
109
111
|
e.preventDefault();
|
|
110
|
-
if (!isSurfaceOpen(
|
|
111
|
-
focusLastMenuItem(
|
|
112
|
+
if (!isSurfaceOpen(panel) && !openFlyout(panel, trigger)) break;
|
|
113
|
+
focusLastMenuItem(panel);
|
|
112
114
|
break;
|
|
113
115
|
case "Enter":
|
|
114
116
|
case " ":
|
|
115
117
|
e.preventDefault();
|
|
116
|
-
if (!isSurfaceOpen(
|
|
117
|
-
openFlyout(
|
|
118
|
-
focusFirstMenuItem(flyout);
|
|
118
|
+
if (!isSurfaceOpen(panel)) {
|
|
119
|
+
if (openFlyout(panel, trigger)) focusFirstMenuItem(panel);
|
|
119
120
|
} else {
|
|
120
|
-
closeSurface(
|
|
121
|
+
closeSurface(panel);
|
|
121
122
|
}
|
|
122
123
|
break;
|
|
123
124
|
case "Tab":
|
|
124
|
-
if (isSurfaceOpen(
|
|
125
|
+
if (isSurfaceOpen(panel)) closeSurface(panel);
|
|
125
126
|
break;
|
|
126
127
|
}
|
|
127
128
|
}
|
|
128
129
|
|
|
129
130
|
function connectTrigger(trigger) {
|
|
130
131
|
if (triggerMap.has(trigger)) return;
|
|
131
|
-
const flyout = flyoutFor(trigger);
|
|
132
|
-
if (!flyout) return;
|
|
133
132
|
|
|
134
133
|
const controller = new AbortController();
|
|
135
|
-
|
|
134
|
+
const state = { panel: null, controller, releaseMenu: null };
|
|
135
|
+
triggerMap.set(trigger, state);
|
|
136
|
+
|
|
137
|
+
resolvePanel(trigger, state);
|
|
136
138
|
|
|
137
|
-
prepareSurface(flyout);
|
|
138
139
|
if (!trigger.hasAttribute("aria-expanded")) {
|
|
139
140
|
trigger.setAttribute("aria-expanded", "false");
|
|
140
141
|
}
|
|
141
|
-
if (!trigger.hasAttribute("aria-haspopup") && isMenuFlyout(flyout)) {
|
|
142
|
-
trigger.setAttribute("aria-haspopup", "menu");
|
|
143
|
-
}
|
|
144
142
|
|
|
145
143
|
trigger.addEventListener("click", onTriggerClick, { signal: controller.signal });
|
|
146
144
|
trigger.addEventListener("keydown", onTriggerKeydown, { signal: controller.signal });
|
|
147
145
|
}
|
|
148
146
|
|
|
147
|
+
function resolvePanel(trigger, state) {
|
|
148
|
+
if (state.panel?.isConnected) return state.panel;
|
|
149
|
+
|
|
150
|
+
const panel = panelFor(trigger);
|
|
151
|
+
if (!panel) return null;
|
|
152
|
+
|
|
153
|
+
let panelEntry = panelRefs.get(panel);
|
|
154
|
+
if (!panelEntry) {
|
|
155
|
+
const release = retainSurface(panel);
|
|
156
|
+
panelEntry = { triggers: new Set(), release };
|
|
157
|
+
panelRefs.set(panel, panelEntry);
|
|
158
|
+
}
|
|
159
|
+
panelEntry.triggers.add(trigger);
|
|
160
|
+
state.panel = panel;
|
|
161
|
+
|
|
162
|
+
if (isMenuFlyout(panel)) {
|
|
163
|
+
state.releaseMenu = connectMenu(panel, {
|
|
164
|
+
close: (menu) => closeSurface(menu),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
if (!trigger.hasAttribute("aria-haspopup") && isMenuFlyout(panel)) {
|
|
168
|
+
trigger.setAttribute("aria-haspopup", "menu");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return panel;
|
|
172
|
+
}
|
|
173
|
+
|
|
149
174
|
function disconnectTrigger(trigger) {
|
|
150
175
|
const state = triggerMap.get(trigger);
|
|
151
176
|
if (!state) return;
|
|
177
|
+
state.releaseMenu?.();
|
|
152
178
|
state.controller.abort();
|
|
153
|
-
if (state.
|
|
154
|
-
closeSurface(state.
|
|
179
|
+
if (state.panel?.isConnected && isSurfaceOpen(state.panel)) {
|
|
180
|
+
closeSurface(state.panel, { restoreFocus: false });
|
|
155
181
|
}
|
|
156
182
|
triggerMap.delete(trigger);
|
|
157
|
-
}
|
|
158
183
|
|
|
159
|
-
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
184
|
+
const panelEntry = state.panel ? panelRefs.get(state.panel) : undefined;
|
|
185
|
+
if (panelEntry) {
|
|
186
|
+
panelEntry.triggers.delete(trigger);
|
|
187
|
+
if (panelEntry.triggers.size === 0) {
|
|
188
|
+
panelEntry.release();
|
|
189
|
+
panelRefs.delete(state.panel);
|
|
190
|
+
}
|
|
166
191
|
}
|
|
167
192
|
}
|
|
168
193
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
return () => disconnectTrigger(trigger);
|
|
173
|
-
},
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
registerEnhancement("flyout", (flyout) => {
|
|
177
|
-
connectFlyout(flyout);
|
|
178
|
-
// Surface owns its own teardown; the marker enhancer in surface.js
|
|
179
|
-
// calls disconnectSurface(el, { restore: false }) on sweep.
|
|
194
|
+
registerEnhancement("flyout", (trigger) => {
|
|
195
|
+
connectTrigger(trigger);
|
|
196
|
+
return () => disconnectTrigger(trigger);
|
|
180
197
|
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { itemForKey } from "./keys.js";
|
|
2
|
+
|
|
3
|
+
const ORIENTATIONS = new Set(["horizontal", "vertical"]);
|
|
4
|
+
|
|
5
|
+
function restoreTabIndex(item, value) {
|
|
6
|
+
if (value == null) item.removeAttribute("tabindex");
|
|
7
|
+
else item.setAttribute("tabindex", value);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function connectFocusGroup(root, opts = {}) {
|
|
11
|
+
if (!root?.addEventListener || !root?.contains) {
|
|
12
|
+
throw new TypeError("connectFocusGroup() requires a root element.");
|
|
13
|
+
}
|
|
14
|
+
if (typeof opts.getItems !== "function") {
|
|
15
|
+
throw new TypeError("connectFocusGroup() requires getItems().");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const orientation = opts.orientation || "horizontal";
|
|
19
|
+
if (!ORIENTATIONS.has(orientation)) {
|
|
20
|
+
throw new TypeError(`Invalid focus-group orientation: ${orientation}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const controller = new AbortController();
|
|
24
|
+
const externalSignal = opts.signal || null;
|
|
25
|
+
const originalTabIndex = new Map();
|
|
26
|
+
let remembered = null;
|
|
27
|
+
let disconnected = false;
|
|
28
|
+
|
|
29
|
+
function getItems() {
|
|
30
|
+
const seen = new Set();
|
|
31
|
+
return [...(opts.getItems() || [])].filter((item) => {
|
|
32
|
+
if (typeof item?.focus !== "function" || !root.contains(item) || seen.has(item)) return false;
|
|
33
|
+
seen.add(item);
|
|
34
|
+
return true;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function restore(item) {
|
|
39
|
+
if (!originalTabIndex.has(item)) return;
|
|
40
|
+
restoreTabIndex(item, originalTabIndex.get(item));
|
|
41
|
+
originalTabIndex.delete(item);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function sync(preferred = remembered) {
|
|
45
|
+
if (disconnected) return [];
|
|
46
|
+
|
|
47
|
+
const items = getItems();
|
|
48
|
+
const itemSet = new Set(items);
|
|
49
|
+
for (const item of originalTabIndex.keys()) {
|
|
50
|
+
if (!itemSet.has(item)) restore(item);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const active = root.ownerDocument.activeElement;
|
|
54
|
+
const target =
|
|
55
|
+
(itemSet.has(preferred) && preferred) ||
|
|
56
|
+
(itemSet.has(active) && active) ||
|
|
57
|
+
items.find((item) => item.tabIndex === 0) ||
|
|
58
|
+
items[0] ||
|
|
59
|
+
null;
|
|
60
|
+
|
|
61
|
+
for (const item of items) {
|
|
62
|
+
if (!originalTabIndex.has(item)) {
|
|
63
|
+
originalTabIndex.set(item, item.getAttribute("tabindex"));
|
|
64
|
+
}
|
|
65
|
+
const tabIndex = item === target ? 0 : -1;
|
|
66
|
+
item.tabIndex = tabIndex;
|
|
67
|
+
}
|
|
68
|
+
remembered = target;
|
|
69
|
+
return items;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function onFocusIn(event) {
|
|
73
|
+
const items = getItems();
|
|
74
|
+
if (items.includes(event.target)) sync(event.target);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function onKeydown(event) {
|
|
78
|
+
const items = sync();
|
|
79
|
+
const current = root.ownerDocument.activeElement;
|
|
80
|
+
if (!items.includes(current)) return;
|
|
81
|
+
|
|
82
|
+
const style = root.ownerDocument.defaultView?.getComputedStyle?.(root);
|
|
83
|
+
const direction = style?.direction === "rtl" ? "rtl" : "ltr";
|
|
84
|
+
const target = itemForKey(items, current, event.key, {
|
|
85
|
+
orientation,
|
|
86
|
+
wrap: opts.wrap === true,
|
|
87
|
+
direction,
|
|
88
|
+
});
|
|
89
|
+
if (!target) return;
|
|
90
|
+
|
|
91
|
+
event.preventDefault();
|
|
92
|
+
sync(target);
|
|
93
|
+
target.focus();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
root.addEventListener("focusin", onFocusIn, { signal: controller.signal });
|
|
97
|
+
root.addEventListener("keydown", onKeydown, { signal: controller.signal });
|
|
98
|
+
|
|
99
|
+
sync();
|
|
100
|
+
|
|
101
|
+
function disconnect() {
|
|
102
|
+
if (disconnected) return;
|
|
103
|
+
disconnected = true;
|
|
104
|
+
externalSignal?.removeEventListener("abort", disconnect);
|
|
105
|
+
controller.abort();
|
|
106
|
+
for (const item of originalTabIndex.keys()) restore(item);
|
|
107
|
+
remembered = null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (externalSignal) {
|
|
111
|
+
if (externalSignal.aborted) disconnect();
|
|
112
|
+
else externalSignal.addEventListener("abort", disconnect, { once: true });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { sync, disconnect };
|
|
116
|
+
}
|
package/src/js/focus.js
CHANGED
|
@@ -12,7 +12,12 @@ const FOCUSABLE_SELECTOR = [
|
|
|
12
12
|
export function isElementVisible(el) {
|
|
13
13
|
if (!el || el.hidden) return false;
|
|
14
14
|
if (typeof el.checkVisibility === "function") return el.checkVisibility();
|
|
15
|
-
|
|
15
|
+
if (el.getClientRects().length === 0) return false;
|
|
16
|
+
// checkVisibility() covers visibility:hidden; without it, read the
|
|
17
|
+
// computed direction of the cascade so a hidden element is not focusable.
|
|
18
|
+
const style = el.ownerDocument?.defaultView?.getComputedStyle?.(el);
|
|
19
|
+
if (!style) return true;
|
|
20
|
+
return style.visibility !== "hidden" && style.visibility !== "collapse";
|
|
16
21
|
}
|
|
17
22
|
|
|
18
23
|
function isFocusable(el) {
|
package/src/js/index.js
CHANGED
|
@@ -14,8 +14,7 @@
|
|
|
14
14
|
import "./flyout.js";
|
|
15
15
|
import "./context-menu.js";
|
|
16
16
|
import "./dialog.js";
|
|
17
|
-
|
|
18
|
-
import "./dialog-fallback.js";
|
|
17
|
+
import "./dismiss.js";
|
|
19
18
|
import "./tab.js";
|
|
20
19
|
import "./tooltip.js";
|
|
21
20
|
import "./scrollspy.js";
|
|
@@ -24,3 +23,6 @@ import "./mask.js";
|
|
|
24
23
|
import "./password.js";
|
|
25
24
|
import "./validation.js";
|
|
26
25
|
import "./status.js";
|
|
26
|
+
import { watchEnhancementManifests } from "./enhancement-loader.js";
|
|
27
|
+
|
|
28
|
+
watchEnhancementManifests();
|
package/src/js/input.js
CHANGED
|
@@ -7,7 +7,7 @@ export function selectionStart(el) {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export function setCaret(el, position) {
|
|
10
|
-
if (
|
|
10
|
+
if (el.ownerDocument.activeElement !== el) return;
|
|
11
11
|
if (typeof el.setSelectionRange !== "function") return;
|
|
12
12
|
try {
|
|
13
13
|
el.setSelectionRange(position, position);
|
package/src/js/keys.js
CHANGED
|
@@ -28,3 +28,30 @@ export function nextItem(items, current, delta, opts = {}) {
|
|
|
28
28
|
const wrapped = ((next % items.length) + items.length) % items.length;
|
|
29
29
|
return items[wrapped];
|
|
30
30
|
}
|
|
31
|
+
|
|
32
|
+
export function itemForKey(items, current, key, opts = {}) {
|
|
33
|
+
const orientation = opts.orientation || "horizontal";
|
|
34
|
+
const wrap = opts.wrap === true;
|
|
35
|
+
const direction = opts.direction || "ltr";
|
|
36
|
+
|
|
37
|
+
switch (key) {
|
|
38
|
+
case "Home":
|
|
39
|
+
return firstItem(items);
|
|
40
|
+
case "End":
|
|
41
|
+
return lastItem(items);
|
|
42
|
+
case "ArrowRight":
|
|
43
|
+
if (orientation !== "horizontal") return null;
|
|
44
|
+
return nextItem(items, current, direction === "rtl" ? -1 : 1, { wrap });
|
|
45
|
+
case "ArrowLeft":
|
|
46
|
+
if (orientation !== "horizontal") return null;
|
|
47
|
+
return nextItem(items, current, direction === "rtl" ? 1 : -1, { wrap });
|
|
48
|
+
case "ArrowDown":
|
|
49
|
+
if (orientation !== "vertical") return null;
|
|
50
|
+
return nextItem(items, current, 1, { wrap });
|
|
51
|
+
case "ArrowUp":
|
|
52
|
+
if (orientation !== "vertical") return null;
|
|
53
|
+
return nextItem(items, current, -1, { wrap });
|
|
54
|
+
default:
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/js/mask.js
CHANGED
|
@@ -55,21 +55,22 @@ function rawMaskChars(value, mask) {
|
|
|
55
55
|
return parseMaskValue(value, mask).raw;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
function appendFollowingLiterals(
|
|
58
|
+
function appendFollowingLiterals(pattern, start) {
|
|
59
59
|
let out = "";
|
|
60
|
-
for (let i = start; i <
|
|
61
|
-
out +=
|
|
60
|
+
for (let i = start; i < pattern.length && !isToken(pattern[i]); i++) {
|
|
61
|
+
out += pattern[i];
|
|
62
62
|
}
|
|
63
63
|
return out;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
function formatRaw(raw, mask, inputType = "") {
|
|
67
67
|
const autoLiteral = inputType.startsWith("insert");
|
|
68
|
+
const pattern = [...mask];
|
|
68
69
|
let rawIndex = 0;
|
|
69
70
|
let out = "";
|
|
70
71
|
|
|
71
|
-
for (let i = 0; i <
|
|
72
|
-
const token =
|
|
72
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
73
|
+
const token = pattern[i];
|
|
73
74
|
|
|
74
75
|
if (!isToken(token)) {
|
|
75
76
|
if (rawIndex < raw.length) out += token;
|
|
@@ -89,7 +90,7 @@ function formatRaw(raw, mask, inputType = "") {
|
|
|
89
90
|
if (!matched) break;
|
|
90
91
|
|
|
91
92
|
if (autoLiteral && rawIndex >= raw.length) {
|
|
92
|
-
out += appendFollowingLiterals(
|
|
93
|
+
out += appendFollowingLiterals(pattern, i + 1);
|
|
93
94
|
break;
|
|
94
95
|
}
|
|
95
96
|
}
|
package/src/js/menu.js
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
import { isElementVisible } from "./focus.js";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
// `:scope li` must stay a *descendant* combinator for that second form; the
|
|
8
|
-
// grouped flyouts in docs/ui.md lose roving focus with `:scope > li`.
|
|
9
|
-
// Scoping is what keeps a neighbouring surface's items out: this selector only
|
|
10
|
-
// ever runs against one surface, and hasMenuItem() resolves the clicked item
|
|
11
|
-
// through this list rather than through a global closest().
|
|
12
|
-
const MENU_ITEM_SELECTOR = `:scope > :is(${MENU_ITEM}), :scope li > :is(${MENU_ITEM})`;
|
|
2
|
+
import { connectFocusGroup } from "./focus-group.js";
|
|
3
|
+
import { firstItem, itemForKey, lastItem } from "./keys.js";
|
|
4
|
+
|
|
5
|
+
const MENU_ITEM_SELECTOR = ":scope > li > .menu-item";
|
|
6
|
+
const MENU_ITEM_ROLES = new Set(["menuitem", "menuitemcheckbox", "menuitemradio"]);
|
|
13
7
|
|
|
14
8
|
function isUsableMenuItem(item) {
|
|
15
9
|
return (
|
|
@@ -25,7 +19,7 @@ export function getMenuItems(menu) {
|
|
|
25
19
|
}
|
|
26
20
|
|
|
27
21
|
function isAriaMenuItem(item) {
|
|
28
|
-
return item.getAttribute("role")
|
|
22
|
+
return MENU_ITEM_ROLES.has(item.getAttribute("role"));
|
|
29
23
|
}
|
|
30
24
|
|
|
31
25
|
function activateCurrentItem(menu) {
|
|
@@ -40,9 +34,13 @@ export function hasMenuItems(menu) {
|
|
|
40
34
|
return getMenuItems(menu).length > 0;
|
|
41
35
|
}
|
|
42
36
|
|
|
37
|
+
function getMenuItem(menu, target) {
|
|
38
|
+
const item = target?.closest?.(".menu-item");
|
|
39
|
+
return item?.parentElement?.parentElement === menu ? item : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
43
42
|
export function hasMenuItem(menu, target) {
|
|
44
|
-
|
|
45
|
-
return !!item && getMenuItems(menu).includes(item);
|
|
43
|
+
return !!getMenuItem(menu, target);
|
|
46
44
|
}
|
|
47
45
|
|
|
48
46
|
export function focusFirstMenuItem(menu) {
|
|
@@ -53,28 +51,22 @@ export function focusLastMenuItem(menu) {
|
|
|
53
51
|
lastItem(getMenuItems(menu))?.focus();
|
|
54
52
|
}
|
|
55
53
|
|
|
56
|
-
export function onMenuKeydown(e, { close }) {
|
|
54
|
+
export function onMenuKeydown(e, { close, navigate = true }) {
|
|
57
55
|
const menu = e.currentTarget;
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
if (navigate) {
|
|
57
|
+
const items = getMenuItems(menu);
|
|
58
|
+
const target = itemForKey(items, menu.ownerDocument.activeElement, e.key, {
|
|
59
|
+
orientation: "vertical",
|
|
60
|
+
wrap: true,
|
|
61
|
+
});
|
|
62
|
+
if (target) {
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
target.focus();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
60
68
|
|
|
61
69
|
switch (e.key) {
|
|
62
|
-
case "ArrowDown":
|
|
63
|
-
e.preventDefault();
|
|
64
|
-
nextItem(items, menu.ownerDocument.activeElement, 1, { wrap: true })?.focus();
|
|
65
|
-
break;
|
|
66
|
-
case "ArrowUp":
|
|
67
|
-
e.preventDefault();
|
|
68
|
-
nextItem(items, menu.ownerDocument.activeElement, -1, { wrap: true })?.focus();
|
|
69
|
-
break;
|
|
70
|
-
case "Home":
|
|
71
|
-
e.preventDefault();
|
|
72
|
-
firstItem(items)?.focus();
|
|
73
|
-
break;
|
|
74
|
-
case "End":
|
|
75
|
-
e.preventDefault();
|
|
76
|
-
lastItem(items)?.focus();
|
|
77
|
-
break;
|
|
78
70
|
case "Tab":
|
|
79
71
|
close(menu);
|
|
80
72
|
break;
|
|
@@ -82,8 +74,69 @@ export function onMenuKeydown(e, { close }) {
|
|
|
82
74
|
case " ":
|
|
83
75
|
if (activateCurrentItem(menu)) {
|
|
84
76
|
e.preventDefault();
|
|
85
|
-
close(menu);
|
|
86
77
|
}
|
|
87
78
|
break;
|
|
88
79
|
}
|
|
89
80
|
}
|
|
81
|
+
|
|
82
|
+
// Menu wiring is a shared, ref-counted resource: a menu may be retained by
|
|
83
|
+
// several triggers (or by flyout and context-menu at once), so the listeners
|
|
84
|
+
// and their AbortController belong to the menu, not to any single caller.
|
|
85
|
+
// Each retain returns a release(); the wiring is torn down on the last one.
|
|
86
|
+
const menuConnections = new WeakMap();
|
|
87
|
+
|
|
88
|
+
export function connectMenu(menu, { close }) {
|
|
89
|
+
let entry = menuConnections.get(menu);
|
|
90
|
+
|
|
91
|
+
if (!entry) {
|
|
92
|
+
const controller = new AbortController();
|
|
93
|
+
const focusGroup = menu.matches('[role="menu"]')
|
|
94
|
+
? connectFocusGroup(menu, {
|
|
95
|
+
getItems: () => getMenuItems(menu).filter(isAriaMenuItem),
|
|
96
|
+
orientation: "vertical",
|
|
97
|
+
wrap: true,
|
|
98
|
+
signal: controller.signal,
|
|
99
|
+
})
|
|
100
|
+
: null;
|
|
101
|
+
menu.addEventListener(
|
|
102
|
+
"keydown",
|
|
103
|
+
(event) =>
|
|
104
|
+
onMenuKeydown(event, {
|
|
105
|
+
close: () => close(menu),
|
|
106
|
+
navigate: !focusGroup,
|
|
107
|
+
}),
|
|
108
|
+
{ signal: controller.signal },
|
|
109
|
+
);
|
|
110
|
+
menu.addEventListener(
|
|
111
|
+
"click",
|
|
112
|
+
(event) => {
|
|
113
|
+
const item = getMenuItem(menu, event.target);
|
|
114
|
+
if (!item) return;
|
|
115
|
+
|
|
116
|
+
if (item.matches(":disabled, [aria-disabled='true']")) {
|
|
117
|
+
event.preventDefault();
|
|
118
|
+
// A disabled command must not reach the surface-level inside-click
|
|
119
|
+
// policy and dismiss the menu as if it had been activated.
|
|
120
|
+
event.stopPropagation();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
{ signal: controller.signal },
|
|
125
|
+
);
|
|
126
|
+
entry = { count: 0, controller, focusGroup };
|
|
127
|
+
menuConnections.set(menu, entry);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
entry.count++;
|
|
131
|
+
let released = false;
|
|
132
|
+
return () => {
|
|
133
|
+
if (released) return;
|
|
134
|
+
released = true;
|
|
135
|
+
entry.count--;
|
|
136
|
+
if (entry.count <= 0) {
|
|
137
|
+
entry.focusGroup?.disconnect();
|
|
138
|
+
entry.controller.abort();
|
|
139
|
+
menuConnections.delete(menu);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|