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
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* parse-config.js — Configuration parsing for data-enhance-config.
|
|
3
|
+
*
|
|
4
|
+
* Strict JSON is parsed as-is, never rewritten. When strict JSON fails, a
|
|
5
|
+
* small, deliberate relax step accepts the ergonomic subset that is common in
|
|
6
|
+
* HTML attributes: unquoted keys and single-quoted strings. The relaxed
|
|
7
|
+
* subset is intentionally tiny and is NOT a second grammar — anything it
|
|
8
|
+
* cannot transform cleanly is rejected by the final JSON.parse. The root
|
|
9
|
+
* value must be an object; bare string values (other than true/false/null)
|
|
10
|
+
* stay invalid, and trailing commas are rejected.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
function relax(source) {
|
|
14
|
+
return source.replace(
|
|
15
|
+
/([a-zA-Z_$][\w$-]*)\s*:|'((?:\\'|[^'])*)'/g,
|
|
16
|
+
(_match, key, stringContent) => {
|
|
17
|
+
if (key) return `"${key}":`;
|
|
18
|
+
const unescaped = stringContent.replace(/\\'/g, "'");
|
|
19
|
+
const escaped = unescaped.replace(/"/g, '\\"');
|
|
20
|
+
return `"${escaped}"`;
|
|
21
|
+
},
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function parseConfig(source) {
|
|
26
|
+
if (source == null || typeof source !== "string") return {};
|
|
27
|
+
const str = source.trim();
|
|
28
|
+
if (str === "") return {};
|
|
29
|
+
|
|
30
|
+
let text = str;
|
|
31
|
+
let value;
|
|
32
|
+
try {
|
|
33
|
+
value = JSON.parse(text);
|
|
34
|
+
} catch {
|
|
35
|
+
const relaxed = relax(text);
|
|
36
|
+
text = relaxed.startsWith("{") ? relaxed : `{${relaxed}}`;
|
|
37
|
+
value = JSON.parse(text);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
41
|
+
throw new TypeError("Enhancement config must be an object");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default parseConfig;
|
package/src/js/password.js
CHANGED
|
@@ -93,6 +93,14 @@ if (typeof document !== "undefined") {
|
|
|
93
93
|
document.addEventListener("submit", handleSubmit);
|
|
94
94
|
|
|
95
95
|
window.addEventListener("pagehide", () => {
|
|
96
|
-
for (const input of revealed)
|
|
96
|
+
for (const input of revealed) {
|
|
97
|
+
// Opportunistic housekeeping: an input removed from the DOM before
|
|
98
|
+
// navigation no longer needs reverting and can leave the set.
|
|
99
|
+
if (!input.isConnected) {
|
|
100
|
+
revealed.delete(input);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
setRevealed(input, false);
|
|
104
|
+
}
|
|
97
105
|
});
|
|
98
106
|
}
|
package/src/js/scrollspy.js
CHANGED
|
@@ -75,12 +75,15 @@ function measureRoot(nav, root) {
|
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
// scrollingElement is the real scroll container in standards mode; fall
|
|
79
|
+
// back to documentElement (quirks mode, or older engines without it).
|
|
80
|
+
const doc = nav.ownerDocument;
|
|
81
|
+
const el = doc.scrollingElement || doc.documentElement;
|
|
79
82
|
return {
|
|
80
83
|
top: 0,
|
|
81
|
-
height:
|
|
82
|
-
scrollTop:
|
|
83
|
-
scrollHeight:
|
|
84
|
+
height: el.clientHeight,
|
|
85
|
+
scrollTop: el.scrollTop,
|
|
86
|
+
scrollHeight: el.scrollHeight,
|
|
84
87
|
};
|
|
85
88
|
}
|
|
86
89
|
|
|
@@ -151,6 +154,11 @@ function setupNav(nav) {
|
|
|
151
154
|
measure();
|
|
152
155
|
}
|
|
153
156
|
|
|
157
|
+
// The scroll root (custom container or the document viewport) is resolved
|
|
158
|
+
// once at connect and is stable for the lifetime of the connection; measure
|
|
159
|
+
// still re-reads the current root element so a replaced *element* under a
|
|
160
|
+
// stable container is handled, but swapping the container itself is out of
|
|
161
|
+
// contract (re-inject the nav or call refreshScrollspy after such a move).
|
|
154
162
|
const scrollTarget = rootFor(nav) ?? nav.ownerDocument.defaultView;
|
|
155
163
|
scrollTarget?.addEventListener("scroll", () => schedule(), {
|
|
156
164
|
passive: true,
|
package/src/js/selectors.js
CHANGED
|
@@ -32,13 +32,6 @@ export const CLASSES = {
|
|
|
32
32
|
backdrop: "surface-backdrop",
|
|
33
33
|
// dialog.js — transient class for the "can't dismiss" shake animation.
|
|
34
34
|
static: "is-static",
|
|
35
|
-
// dialog-fallback.js — marker written on a dialog controlled by the legacy
|
|
36
|
-
// shim (browsers without native <dialog>); dialog-fallback.css keys its
|
|
37
|
-
// emulated dialog presentation on it.
|
|
38
|
-
dialogFallback: "dialog-fallback",
|
|
39
|
-
// dialog-fallback.js — state class on a shimmed dialog while it is open as
|
|
40
|
-
// a modal; carries the fixed positioning and simulated backdrop.
|
|
41
|
-
fallbackModal: "is-fallback-modal",
|
|
42
35
|
// dialog.js — written on <html> while any modal dialog is open.
|
|
43
36
|
modalOpen: "has-modal-open",
|
|
44
37
|
// dialog.js — written on <html> when the viewport had a classic scrollbar
|
package/src/js/status.js
CHANGED
|
@@ -125,7 +125,9 @@ if (typeof document !== "undefined") {
|
|
|
125
125
|
// importing this module. A message shows it; omitting one clears it.
|
|
126
126
|
document.addEventListener(EVENTS.status, (event) => {
|
|
127
127
|
const { message, intent, duration } = event.detail ?? {};
|
|
128
|
-
|
|
128
|
+
// `message != null` keeps "" (an intentional empty message) on the show
|
|
129
|
+
// path, consistent with status() itself; an absent message clears.
|
|
130
|
+
if (message != null) {
|
|
129
131
|
status(message, { intent, duration });
|
|
130
132
|
} else {
|
|
131
133
|
status.clear();
|
package/src/js/surface.js
CHANGED
|
@@ -1,33 +1,35 @@
|
|
|
1
1
|
import enhance from "./enhance.js";
|
|
2
2
|
import { EVENTS } from "./events.js";
|
|
3
|
-
import { reposition, repositionAt
|
|
4
|
-
|
|
3
|
+
import { autoUpdate, reposition, repositionAt } from "./floating.js";
|
|
4
|
+
|
|
5
5
|
import { CLASSES } from "./selectors.js";
|
|
6
6
|
|
|
7
|
-
const DEFAULT_BREAKPOINT = 768;
|
|
8
|
-
const BREAKPOINTS = {
|
|
9
|
-
sm: 640,
|
|
10
|
-
md: 768,
|
|
11
|
-
lg: 1024,
|
|
12
|
-
};
|
|
13
7
|
const openSurfaces = new Set();
|
|
14
8
|
const surfaceMap = new WeakMap();
|
|
9
|
+
const surfaceRetainers = new WeakMap();
|
|
15
10
|
const mountedSurfaces = new WeakMap();
|
|
16
11
|
const clickBoundDocuments = new WeakSet();
|
|
17
12
|
|
|
18
|
-
// data-actual-surface is written by the runtime, never by an author, and never
|
|
19
|
-
// selected on by CSS. It is namespaced because "data-surface" is a name an
|
|
20
|
-
// application may already own. surface.js reaps its own surfaces so consumers
|
|
21
|
-
// do not have to maintain panel-side lifecycle hooks.
|
|
22
13
|
const SURFACE_MARKER = "data-actual-surface";
|
|
14
|
+
const AUTO_CLOSE_VALUES = new Set(["true", "inside", "outside", "false"]);
|
|
15
|
+
const SCROLL_KEYS = new Set([
|
|
16
|
+
"ArrowDown",
|
|
17
|
+
"ArrowLeft",
|
|
18
|
+
"ArrowRight",
|
|
19
|
+
"ArrowUp",
|
|
20
|
+
"End",
|
|
21
|
+
"Home",
|
|
22
|
+
"PageDown",
|
|
23
|
+
"PageUp",
|
|
24
|
+
" ",
|
|
25
|
+
]);
|
|
23
26
|
const reapers = new WeakMap();
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// retained once a document is gone.
|
|
28
|
+
function normalizeAutoClose(value) {
|
|
29
|
+
const normalized = String(value ?? "outside");
|
|
30
|
+
return AUTO_CLOSE_VALUES.has(normalized) ? normalized : "true";
|
|
31
|
+
}
|
|
32
|
+
|
|
31
33
|
function reaperFor(menu) {
|
|
32
34
|
const root = menu.ownerDocument?.documentElement;
|
|
33
35
|
if (!root) return null;
|
|
@@ -82,45 +84,20 @@ function restoreSurface(menu) {
|
|
|
82
84
|
mountedSurfaces.delete(menu);
|
|
83
85
|
}
|
|
84
86
|
|
|
85
|
-
function
|
|
86
|
-
const
|
|
87
|
-
const raw = Number.parseInt(BREAKPOINTS[value] || value, 10);
|
|
88
|
-
return Number.isFinite(raw) ? raw : DEFAULT_BREAKPOINT;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function getMobileMode(menu, mode) {
|
|
92
|
-
return mode || menu.dataset.flyoutMobile || "auto";
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function getPlacement(menu, placement) {
|
|
96
|
-
return placement || menu.dataset.flyoutPlacement || "bottom-start";
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function getDistance(menu, distance) {
|
|
100
|
-
if (distance != null) return distance;
|
|
101
|
-
const value = Number.parseFloat(menu.dataset.flyoutDistance);
|
|
102
|
-
return Number.isFinite(value) ? value : 4;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function getAutoCloseMode(menu, value) {
|
|
106
|
-
const mode = String(value ?? menu.dataset.flyoutAutoClose ?? "true").toLowerCase();
|
|
107
|
-
if (mode === "inside" || mode === "outside" || mode === "false") return mode;
|
|
108
|
-
return "true";
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function shouldUseSheet(menu, mode) {
|
|
112
|
-
const mobile = getMobileMode(menu, mode);
|
|
87
|
+
function shouldUseSheet(menu, state) {
|
|
88
|
+
const mobile = state.mobile || "auto";
|
|
113
89
|
if (mobile === "none" || mobile === "anchored") return false;
|
|
114
90
|
if (mobile === "sheet") return true;
|
|
115
91
|
|
|
116
|
-
|
|
92
|
+
const view = menu.ownerDocument?.defaultView;
|
|
93
|
+
if (!view || typeof view.matchMedia !== "function") {
|
|
117
94
|
return false;
|
|
118
95
|
}
|
|
119
96
|
|
|
120
|
-
const breakpoint =
|
|
97
|
+
const breakpoint = state.breakpoint ?? 768;
|
|
121
98
|
return (
|
|
122
|
-
|
|
123
|
-
|
|
99
|
+
view.matchMedia("(pointer: coarse)").matches &&
|
|
100
|
+
view.matchMedia(`(max-width: ${breakpoint}px)`).matches
|
|
124
101
|
);
|
|
125
102
|
}
|
|
126
103
|
|
|
@@ -150,16 +127,13 @@ function ensureBackdrop(menu, state) {
|
|
|
150
127
|
const backdrop = menu.ownerDocument.createElement("div");
|
|
151
128
|
backdrop.className = CLASSES.backdrop;
|
|
152
129
|
backdrop.hidden = false;
|
|
153
|
-
backdrop.addEventListener("click", () => {
|
|
154
|
-
closeSurface(menu);
|
|
155
|
-
});
|
|
156
130
|
|
|
157
131
|
menu.before(backdrop);
|
|
158
132
|
state.backdrop = backdrop;
|
|
159
133
|
}
|
|
160
134
|
|
|
161
135
|
function applyPresentation(menu, state) {
|
|
162
|
-
state.isSheet = shouldUseSheet(menu, state
|
|
136
|
+
state.isSheet = shouldUseSheet(menu, state);
|
|
163
137
|
if (state.isSheet) {
|
|
164
138
|
menu.style.removeProperty("left");
|
|
165
139
|
menu.style.removeProperty("top");
|
|
@@ -170,13 +144,13 @@ function applyPresentation(menu, state) {
|
|
|
170
144
|
|
|
171
145
|
function positionSurface(menu) {
|
|
172
146
|
const state = surfaceMap.get(menu);
|
|
173
|
-
if (!state || state.isSheet) return;
|
|
147
|
+
if (!state || state.isSheet) return true;
|
|
174
148
|
|
|
175
149
|
if (state.trigger) {
|
|
176
150
|
const triggerWidth = state.trigger.getBoundingClientRect().width;
|
|
177
|
-
menu.style.setProperty("--
|
|
151
|
+
menu.style.setProperty("--surface-anchor-width", `${triggerWidth}px`);
|
|
178
152
|
} else {
|
|
179
|
-
menu.style.removeProperty("--
|
|
153
|
+
menu.style.removeProperty("--surface-anchor-width");
|
|
180
154
|
}
|
|
181
155
|
|
|
182
156
|
const opts = {
|
|
@@ -189,94 +163,87 @@ function positionSurface(menu) {
|
|
|
189
163
|
};
|
|
190
164
|
|
|
191
165
|
if (state.point) {
|
|
192
|
-
repositionAt(state.point.x, state.point.y, menu, opts);
|
|
193
|
-
return;
|
|
166
|
+
return repositionAt(state.point.x, state.point.y, menu, opts);
|
|
194
167
|
}
|
|
195
168
|
|
|
196
169
|
const anchor = state.trigger || state.source;
|
|
197
|
-
if (anchor) reposition(anchor, menu, opts);
|
|
170
|
+
if (anchor) return reposition(anchor, menu, opts);
|
|
171
|
+
return true;
|
|
198
172
|
}
|
|
199
173
|
|
|
200
174
|
function ensureSurfaceWired(menu) {
|
|
201
175
|
if (surfaceMap.has(menu)) return surfaceMap.get(menu);
|
|
202
176
|
|
|
203
177
|
ensureDocumentClick(menu);
|
|
204
|
-
const controller = new AbortController();
|
|
205
178
|
const state = {
|
|
206
|
-
|
|
207
|
-
|
|
179
|
+
stopTracking: null,
|
|
180
|
+
stopScrollIntent: null,
|
|
181
|
+
unregisterEscape: null,
|
|
208
182
|
backdrop: null,
|
|
209
183
|
trigger: null,
|
|
210
184
|
source: null,
|
|
211
185
|
point: null,
|
|
212
186
|
mobile: "auto",
|
|
187
|
+
breakpoint: 768,
|
|
213
188
|
placement: "bottom-start",
|
|
214
189
|
distance: 4,
|
|
215
190
|
flip: true,
|
|
216
191
|
shift: true,
|
|
217
192
|
shiftPadding: 4,
|
|
218
|
-
autoClose: "
|
|
193
|
+
autoClose: "outside",
|
|
194
|
+
dismissOnScroll: false,
|
|
195
|
+
scrollIntentAt: null,
|
|
219
196
|
isSheet: false,
|
|
220
197
|
closeId: 0,
|
|
221
198
|
};
|
|
222
199
|
|
|
223
|
-
menu.addEventListener(
|
|
224
|
-
EVENTS.reposition,
|
|
225
|
-
(e) => {
|
|
226
|
-
if (menu.hidden) return;
|
|
227
|
-
if (e.detail?.type === "scroll" && surfaceMap.get(menu)?.point) {
|
|
228
|
-
closeSurface(menu);
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
const state = surfaceMap.get(menu);
|
|
232
|
-
if (!state) return;
|
|
233
|
-
applyPresentation(menu, state);
|
|
234
|
-
positionSurface(menu);
|
|
235
|
-
},
|
|
236
|
-
{ signal: controller.signal },
|
|
237
|
-
);
|
|
238
|
-
|
|
239
|
-
menu.addEventListener(
|
|
240
|
-
EVENTS.hide,
|
|
241
|
-
(e) => {
|
|
242
|
-
if (!menu.hidden) closeSurface(menu, { restoreFocus: e.detail?.type === "escape" });
|
|
243
|
-
},
|
|
244
|
-
{ signal: controller.signal },
|
|
245
|
-
);
|
|
246
|
-
|
|
247
|
-
menu.addEventListener(
|
|
248
|
-
EVENTS.outOfView,
|
|
249
|
-
() => {
|
|
250
|
-
if (!menu.hidden) closeSurface(menu);
|
|
251
|
-
},
|
|
252
|
-
{ signal: controller.signal },
|
|
253
|
-
);
|
|
254
|
-
|
|
255
|
-
menu.addEventListener(
|
|
256
|
-
"keydown",
|
|
257
|
-
(e) => onMenuKeydown(e, { close: (target) => closeSurface(target) }),
|
|
258
|
-
{ signal: controller.signal },
|
|
259
|
-
);
|
|
260
|
-
|
|
261
|
-
menu.addEventListener(
|
|
262
|
-
"click",
|
|
263
|
-
(e) => {
|
|
264
|
-
const state = surfaceMap.get(menu);
|
|
265
|
-
const disabledItem = e.target.closest?.(":disabled, [aria-disabled='true']");
|
|
266
|
-
if (disabledItem && menu.contains(disabledItem)) {
|
|
267
|
-
e.preventDefault();
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
if (!state || state.autoClose === "outside" || state.autoClose === "false") return;
|
|
271
|
-
if (hasMenuItem(menu, e.target)) closeSurface(menu);
|
|
272
|
-
},
|
|
273
|
-
{ signal: controller.signal },
|
|
274
|
-
);
|
|
275
|
-
|
|
276
200
|
surfaceMap.set(menu, state);
|
|
277
201
|
return state;
|
|
278
202
|
}
|
|
279
203
|
|
|
204
|
+
// Escape and positional tracking belong to the *open* surface, not to the
|
|
205
|
+
// wiring. They start when the surface opens and stop when it closes, so a
|
|
206
|
+
// closed surface never participates in the dismissable stack or in
|
|
207
|
+
// scroll/resize work. Reopening re-registers in the correct order.
|
|
208
|
+
function startSurfaceResources(menu, state) {
|
|
209
|
+
state.unregisterEscape = registerEscapeDismissal(menu, (opts) => closeSurface(menu, opts));
|
|
210
|
+
if (state.dismissOnScroll) {
|
|
211
|
+
const controller = new AbortController();
|
|
212
|
+
const arm = (event) => {
|
|
213
|
+
state.scrollIntentAt = event.timeStamp;
|
|
214
|
+
};
|
|
215
|
+
const armExternalPointer = (event) => {
|
|
216
|
+
if (!menu.contains(event.target)) arm(event);
|
|
217
|
+
};
|
|
218
|
+
const armKey = (event) => {
|
|
219
|
+
if (!event.defaultPrevented && SCROLL_KEYS.has(event.key)) arm(event);
|
|
220
|
+
};
|
|
221
|
+
const captureOptions = { signal: controller.signal, capture: true };
|
|
222
|
+
const passiveCaptureOptions = { ...captureOptions, passive: true };
|
|
223
|
+
const doc = menu.ownerDocument;
|
|
224
|
+
doc.addEventListener("pointerdown", armExternalPointer, passiveCaptureOptions);
|
|
225
|
+
doc.addEventListener("touchmove", arm, passiveCaptureOptions);
|
|
226
|
+
doc.addEventListener("wheel", arm, passiveCaptureOptions);
|
|
227
|
+
doc.addEventListener("keydown", armKey, { signal: controller.signal });
|
|
228
|
+
state.stopScrollIntent = () => controller.abort();
|
|
229
|
+
}
|
|
230
|
+
state.stopTracking = autoUpdate(menu, ({ type, targets, timeStamp }) => {
|
|
231
|
+
if (menu.hidden) return;
|
|
232
|
+
if (
|
|
233
|
+
type === "scroll" &&
|
|
234
|
+
state.dismissOnScroll &&
|
|
235
|
+
state.scrollIntentAt != null &&
|
|
236
|
+
timeStamp >= state.scrollIntentAt &&
|
|
237
|
+
[...targets].some((target) => target !== menu && !menu.contains(target))
|
|
238
|
+
) {
|
|
239
|
+
closeSurface(menu);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
applyPresentation(menu, state);
|
|
243
|
+
if (!positionSurface(menu)) closeSurface(menu);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
280
247
|
export function isSurfaceOpen(menu) {
|
|
281
248
|
return menu.classList.contains(CLASSES.open) && !menu.hidden;
|
|
282
249
|
}
|
|
@@ -286,12 +253,35 @@ export function prepareSurface(menu) {
|
|
|
286
253
|
menu.style.position = "fixed";
|
|
287
254
|
menu.hidden = true;
|
|
288
255
|
syncExpanded(menu, false);
|
|
289
|
-
// Both steps are idempotent, so they run unguarded: that also keeps the
|
|
290
|
-
// reaper correct if a surface is ever adopted into another document.
|
|
291
256
|
menu.setAttribute(SURFACE_MARKER, "");
|
|
292
257
|
reaperFor(menu)?.refresh(menu);
|
|
293
258
|
}
|
|
294
259
|
|
|
260
|
+
export function retainSurface(panel) {
|
|
261
|
+
if (!panel) return () => {};
|
|
262
|
+
|
|
263
|
+
let entry = surfaceRetainers.get(panel);
|
|
264
|
+
if (!entry) {
|
|
265
|
+
prepareSurface(panel);
|
|
266
|
+
entry = { count: 0 };
|
|
267
|
+
surfaceRetainers.set(panel, entry);
|
|
268
|
+
}
|
|
269
|
+
entry.count++;
|
|
270
|
+
|
|
271
|
+
let released = false;
|
|
272
|
+
return () => {
|
|
273
|
+
if (released) return;
|
|
274
|
+
released = true;
|
|
275
|
+
entry.count--;
|
|
276
|
+
if (entry.count <= 0) {
|
|
277
|
+
// A released retainer may leave a mounted panel whose original parent
|
|
278
|
+
// either still needs it back or has disappeared along with the trigger.
|
|
279
|
+
disconnectSurface(panel);
|
|
280
|
+
surfaceRetainers.delete(panel);
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
295
285
|
export function openSurface(menu, opts = {}) {
|
|
296
286
|
if (!menu?.isConnected) return false;
|
|
297
287
|
if (isSurfaceOpen(menu)) return false;
|
|
@@ -304,7 +294,7 @@ export function openSurface(menu, opts = {}) {
|
|
|
304
294
|
if (!menu.dispatchEvent(beforeOpen)) return false;
|
|
305
295
|
|
|
306
296
|
for (const other of openSurfaces) {
|
|
307
|
-
if (other !== menu) closeSurface(other);
|
|
297
|
+
if (other !== menu && other.ownerDocument === menu.ownerDocument) closeSurface(other);
|
|
308
298
|
}
|
|
309
299
|
|
|
310
300
|
const anchor = opts.trigger || opts.source || null;
|
|
@@ -315,10 +305,13 @@ export function openSurface(menu, opts = {}) {
|
|
|
315
305
|
state.source = opts.source || null;
|
|
316
306
|
state.point =
|
|
317
307
|
Number.isFinite(opts.x) && Number.isFinite(opts.y) ? { x: opts.x, y: opts.y } : null;
|
|
318
|
-
state.mobile =
|
|
319
|
-
state.
|
|
320
|
-
state.
|
|
321
|
-
state.
|
|
308
|
+
state.mobile = opts.mobile || "auto";
|
|
309
|
+
state.breakpoint = opts.breakpoint ?? 768;
|
|
310
|
+
state.autoClose = normalizeAutoClose(opts.autoClose);
|
|
311
|
+
state.dismissOnScroll = opts.dismissOnScroll === true;
|
|
312
|
+
state.scrollIntentAt = null;
|
|
313
|
+
state.placement = opts.placement || "bottom-start";
|
|
314
|
+
state.distance = opts.distance ?? 4;
|
|
322
315
|
state.flip = opts.flip !== false;
|
|
323
316
|
state.shift = opts.shift !== false;
|
|
324
317
|
state.shiftPadding = opts.shiftPadding ?? 4;
|
|
@@ -328,10 +321,16 @@ export function openSurface(menu, opts = {}) {
|
|
|
328
321
|
|
|
329
322
|
menu.classList.add(CLASSES.open);
|
|
330
323
|
menu.hidden = false;
|
|
331
|
-
openSurfaces.add(menu);
|
|
332
324
|
applyPresentation(menu, state);
|
|
333
325
|
syncExpanded(menu, true);
|
|
334
|
-
|
|
326
|
+
|
|
327
|
+
if (!positionSurface(menu)) {
|
|
328
|
+
closeSurface(menu, { restoreFocus: false });
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
openSurfaces.add(menu);
|
|
333
|
+
startSurfaceResources(menu, state);
|
|
335
334
|
return true;
|
|
336
335
|
}
|
|
337
336
|
|
|
@@ -352,6 +351,9 @@ export function closeSurface(menu, opts = {}) {
|
|
|
352
351
|
if (backdrop) backdrop.hidden = true;
|
|
353
352
|
openSurfaces.delete(menu);
|
|
354
353
|
syncExpanded(menu, false);
|
|
354
|
+
state?.unregisterEscape?.();
|
|
355
|
+
state?.stopScrollIntent?.();
|
|
356
|
+
state?.stopTracking?.();
|
|
355
357
|
|
|
356
358
|
if (shouldRestoreFocus && state?.restoreFocusTo?.isConnected) {
|
|
357
359
|
state.restoreFocusTo.focus({ preventScroll: true });
|
|
@@ -374,30 +376,79 @@ export function disconnectSurface(menu, { restore = true } = {}) {
|
|
|
374
376
|
const state = surfaceMap.get(menu);
|
|
375
377
|
if (state) {
|
|
376
378
|
state.backdrop?.remove();
|
|
377
|
-
state.
|
|
378
|
-
state.
|
|
379
|
+
state.unregisterEscape?.();
|
|
380
|
+
state.stopScrollIntent?.();
|
|
381
|
+
state.stopTracking?.();
|
|
379
382
|
surfaceMap.delete(menu);
|
|
380
383
|
}
|
|
381
384
|
restoreSurface(menu);
|
|
382
385
|
}
|
|
383
386
|
|
|
384
387
|
function onDocumentClick(e) {
|
|
388
|
+
const path = e.composedPath();
|
|
389
|
+
|
|
385
390
|
for (const menu of openSurfaces) {
|
|
391
|
+
if (menu.ownerDocument !== e.currentTarget) continue;
|
|
386
392
|
const state = surfaceMap.get(menu);
|
|
387
|
-
if (!state
|
|
388
|
-
|
|
389
|
-
|
|
393
|
+
if (!state) continue;
|
|
394
|
+
|
|
395
|
+
const menuIndex = path.indexOf(menu);
|
|
396
|
+
const isInside = menuIndex >= 0;
|
|
397
|
+
const hasCloseTrigger =
|
|
398
|
+
isInside &&
|
|
399
|
+
path
|
|
400
|
+
.slice(0, menuIndex)
|
|
401
|
+
.some((node) => node instanceof Element && node.hasAttribute("data-flyout-close"));
|
|
402
|
+
|
|
403
|
+
if (hasCloseTrigger) {
|
|
404
|
+
closeSurface(menu);
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const closesInside = state.autoClose === "true" || state.autoClose === "inside";
|
|
409
|
+
const closesOutside = state.autoClose === "true" || state.autoClose === "outside";
|
|
410
|
+
if ((isInside && closesInside) || (!isInside && closesOutside)) closeSurface(menu);
|
|
390
411
|
}
|
|
391
412
|
}
|
|
392
413
|
|
|
393
|
-
// Bound per owning document on first use, not at import time — same reason as
|
|
394
|
-
// reaperFor(): an import-time listener attaches to whichever document existed
|
|
395
|
-
// then, and stays attached to it. Nothing can need this listener before a
|
|
396
|
-
// surface has been wired, so binding here costs nothing and keeps SSR imports
|
|
397
|
-
// inert without a typeof guard.
|
|
398
414
|
function ensureDocumentClick(menu) {
|
|
399
415
|
const doc = menu.ownerDocument;
|
|
400
416
|
if (!doc || clickBoundDocuments.has(doc)) return;
|
|
401
417
|
clickBoundDocuments.add(doc);
|
|
402
418
|
doc.addEventListener("click", onDocumentClick);
|
|
403
419
|
}
|
|
420
|
+
|
|
421
|
+
const dismissableStacks = new WeakMap();
|
|
422
|
+
|
|
423
|
+
export function registerEscapeDismissal(element, dismiss) {
|
|
424
|
+
const doc = element.ownerDocument;
|
|
425
|
+
let stack = dismissableStacks.get(doc);
|
|
426
|
+
if (!stack) {
|
|
427
|
+
stack = [];
|
|
428
|
+
dismissableStacks.set(doc, stack);
|
|
429
|
+
doc.addEventListener("keydown", onDocumentEscape);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const entry = { element, dismiss };
|
|
433
|
+
stack.push(entry);
|
|
434
|
+
|
|
435
|
+
return () => {
|
|
436
|
+
const index = stack.indexOf(entry);
|
|
437
|
+
if (index >= 0) stack.splice(index, 1);
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function onDocumentEscape(event) {
|
|
442
|
+
if (event.key !== "Escape" || event.ctrlKey || event.altKey || event.shiftKey) return;
|
|
443
|
+
|
|
444
|
+
const stack = dismissableStacks.get(event.currentTarget);
|
|
445
|
+
const entry = stack.at(-1);
|
|
446
|
+
if (!entry) return;
|
|
447
|
+
|
|
448
|
+
event.preventDefault();
|
|
449
|
+
entry.dismiss({ restoreFocus: true });
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export function getSurfaceAutoClose(menu) {
|
|
453
|
+
return surfaceMap.get(menu)?.autoClose ?? "outside";
|
|
454
|
+
}
|