actual-css 0.9.0 → 0.9.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 +25 -0
- package/components.json +2 -0
- package/dist/actual.css +10 -3
- package/dist/actual.full.css +238 -71
- package/dist/actual.full.js +1 -1
- package/dist/actual.full.min.css +1 -1
- package/dist/actual.min.css +1 -1
- package/docs/pages/foundations/tokens.md +1 -1
- package/docs/pages/guides/progressive-enhancement.md +12 -8
- package/package.json +159 -159
- package/src/css/components/accordion.css +2 -0
- package/src/css/components/actions.css +2 -0
- package/src/css/components/alert.css +2 -0
- package/src/css/components/app-nav.css +2 -0
- package/src/css/components/avatar.css +2 -0
- package/src/css/components/badge.css +2 -0
- package/src/css/components/breadcrumb.css +2 -0
- package/src/css/components/busy.css +2 -0
- package/src/css/components/button.css +2 -0
- package/src/css/components/card.css +2 -0
- package/src/css/components/chat.css +2 -0
- package/src/css/components/data-list.css +2 -0
- package/src/css/components/drawer.css +35 -22
- package/src/css/components/fab.css +2 -0
- package/src/css/components/flyout.css +2 -0
- package/src/css/components/indicator.css +2 -0
- package/src/css/components/join.css +2 -0
- package/src/css/components/key.css +2 -0
- package/src/css/components/list.css +2 -0
- package/src/css/components/meter.css +2 -0
- package/src/css/components/modal.css +47 -40
- package/src/css/components/navbar.css +2 -0
- package/src/css/components/pagination.css +2 -0
- package/src/css/components/progress.css +2 -0
- package/src/css/components/rating.css +2 -0
- package/src/css/components/scrollspy.css +2 -0
- package/src/css/components/skeleton.css +2 -0
- package/src/css/components/spinner.css +2 -0
- package/src/css/components/status-bar.css +2 -0
- package/src/css/components/steps.css +2 -0
- package/src/css/components/tab.css +2 -0
- package/src/css/components/table.css +2 -0
- package/src/css/components/tooltip.css +2 -0
- package/src/css/core/print.css +3 -0
- package/src/css/core/variants.css +7 -3
- package/src/css/effects/aura.css +2 -0
- package/src/css/forms/choice-card.css +4 -0
- package/src/css/forms/control.css +2 -0
- package/src/css/forms/floating-field.css +5 -1
- package/src/css/forms/form-actions.css +2 -0
- package/src/css/forms/form.css +2 -0
- package/src/css/forms/input-icon.css +2 -0
- package/src/css/forms/native.css +2 -0
- package/src/css/forms/otp.css +10 -0
- package/src/css/forms/range.css +2 -0
- package/src/css/forms/select.css +2 -0
- package/src/css/forms/switch.css +2 -0
- package/src/css/forms/validation.css +2 -0
- package/src/css/layout/app-layout.css +2 -0
- package/src/css/layout/app-shell.css +5 -1
- package/src/css/layout/center.css +2 -0
- package/src/css/layout/cluster.css +2 -0
- package/src/css/layout/column-layout.css +2 -0
- package/src/css/layout/frame.css +2 -0
- package/src/css/layout/grid.css +2 -0
- package/src/css/layout/media.css +2 -1
- package/src/css/layout/scroll-snap.css +2 -0
- package/src/css/layout/scroll-target.css +2 -0
- package/src/css/layout/sidebar-layout.css +2 -0
- package/src/css/layout/stack.css +2 -1
- package/src/css/layout/switcher.css +2 -0
- package/src/css/layout/topbar.css +2 -0
- package/src/css/typography/fluid.css +2 -0
- package/src/css/typography/lead.css +2 -1
- package/src/css/typography/overline.css +2 -0
- package/src/css/typography/prose.css +2 -0
- package/src/css/utilities/base.css +6 -1
- package/src/js/context-menu.js +56 -15
- package/src/js/enhancement-loader.js +5 -1
- package/src/js/escape.js +11 -1
- package/src/js/flyout.js +6 -1
- package/src/js/focus.js +8 -3
- package/src/js/surface.js +28 -0
- package/src/js/tooltip.js +10 -4
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Text controls — input, textarea, select.
|
|
3
3
|
*
|
|
4
|
+
* Docs: docs/pages/forms/overview.md
|
|
5
|
+
*
|
|
4
6
|
* Neutral geometry and interaction states shared across all text-like
|
|
5
7
|
* controls. select shares this base recipe; its chevron/appearance layer
|
|
6
8
|
* lives in select.css. checkbox, radio, range, switch, and file have their
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Floating labels — optional composition over the text controls
|
|
3
3
|
* (.input, .textarea, .select) and .field-label.
|
|
4
4
|
*
|
|
5
|
+
* Docs: docs/pages/forms/floating-field.md
|
|
6
|
+
*
|
|
5
7
|
* The cell owns a control plus its label; the label overlays the control and
|
|
6
8
|
* floats to the top when the control is focused or non-empty. Help and error
|
|
7
9
|
* messages stay in the surrounding .field, not inside the cell, so the
|
|
@@ -43,7 +45,9 @@
|
|
|
43
45
|
|
|
44
46
|
.floating-field > .field-label {
|
|
45
47
|
position: absolute;
|
|
46
|
-
|
|
48
|
+
/* Same start edge as the control's text: --control-pad-x owns that shared
|
|
49
|
+
geometry, so .sm/.lg keep label and value aligned. */
|
|
50
|
+
inset-inline-start: calc(var(--control-pad-x) + var(--border-width));
|
|
47
51
|
color: var(--text-muted);
|
|
48
52
|
font-size: var(--control-font-size);
|
|
49
53
|
font-weight: var(--font-weight-normal);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Form actions — bottom form bar with sticky variant.
|
|
3
3
|
*
|
|
4
|
+
* Docs: docs/pages/forms/form-actions.md
|
|
5
|
+
*
|
|
4
6
|
* A flex cluster of buttons/actions that closes a form. The sticky variant
|
|
5
7
|
* docks to the viewport bottom with a translucent blur where supported; at
|
|
6
8
|
* rest it stays visually neutral so it does not double as a separator.
|
package/src/css/forms/form.css
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Input Icon — decorative icon inside a relative wrapper, leading or trailing.
|
|
3
3
|
*
|
|
4
|
+
* Docs: docs/pages/patterns/icon-input.md
|
|
5
|
+
*
|
|
4
6
|
* Icon-library agnostic: detects position by DOM order. First child that is
|
|
5
7
|
* not .input = leading icon; icon after .input = trailing. The icon follows
|
|
6
8
|
* local typography; density contexts shrink the control geometry around it
|
package/src/css/forms/native.css
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* custom styling, so the framework stays out of the way and just keeps them
|
|
4
4
|
* readable and theme-aware.
|
|
5
5
|
*
|
|
6
|
+
* Docs: docs/pages/forms/controls.md
|
|
7
|
+
*
|
|
6
8
|
* Range has its own custom-styled file (range.css) so the focus ring can
|
|
7
9
|
* target the thumb pseudo-element rather than the rectangular CSS box.
|
|
8
10
|
*/
|
package/src/css/forms/otp.css
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* One-time code input with visual cells and one native input.
|
|
3
3
|
*
|
|
4
|
+
* Docs: docs/pages/forms/otp.md
|
|
5
|
+
*
|
|
4
6
|
* Add one direct span per accepted character. The cells determine the
|
|
5
7
|
* component width; the overlaid input remains the only interactive control.
|
|
6
8
|
*
|
|
@@ -129,4 +131,12 @@
|
|
|
129
131
|
.needs-validation.was-validated .otp > input:invalid ~ span {
|
|
130
132
|
--otp-cell-border: Mark;
|
|
131
133
|
}
|
|
134
|
+
|
|
135
|
+
/* biome-ignore lint/style/noDescendingSpecificity: the :user-invalid route
|
|
136
|
+
mirrors the non-forced-colors block above and must come last — the three
|
|
137
|
+
invalid markers write the same value, so source order is inert. An engine
|
|
138
|
+
without the pseudo drops this rule alone. */
|
|
139
|
+
.needs-validation .otp > input:user-invalid ~ span {
|
|
140
|
+
--otp-cell-border: Mark;
|
|
141
|
+
}
|
|
132
142
|
}
|
package/src/css/forms/range.css
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Range slider — custom-styled so the focus ring always targets the
|
|
3
3
|
* user-manipulated surface (the thumb) rather than the CSS box.
|
|
4
4
|
*
|
|
5
|
+
* Docs: docs/pages/forms/range.md
|
|
6
|
+
*
|
|
5
7
|
* appearance: none gives us full control over track and thumb; without it,
|
|
6
8
|
* pseudo-element rules for ::-webkit-slider-thumb and ::-moz-range-thumb
|
|
7
9
|
* are not reliably applied across browsers.
|
package/src/css/forms/select.css
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Select — chevron and appearance layer on top of the shared text-control
|
|
3
3
|
* recipe.
|
|
4
4
|
*
|
|
5
|
+
* Docs: docs/pages/forms/overview.md
|
|
6
|
+
*
|
|
5
7
|
* Base geometry and interaction states come from control.css (select shares
|
|
6
8
|
* the --control-* tokens). This file only removes the native arrow and draws
|
|
7
9
|
* the CSS chevron, so the picker (appearance: base-select) enhancement in
|
package/src/css/forms/switch.css
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Explicit 12-column composition canvas.
|
|
3
3
|
*
|
|
4
|
+
* Docs: docs/pages/layout/column-layout.md
|
|
5
|
+
*
|
|
4
6
|
* Neither space-driven like .grid nor count-driven like .grid-N: this is a
|
|
5
7
|
* coordinate system. The author places regions with .column-span-N /
|
|
6
8
|
* .column-start-N and owns narrow-container recomposition. There is
|
package/src/css/layout/frame.css
CHANGED
package/src/css/layout/grid.css
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Responsive equal-width grids.
|
|
3
3
|
*
|
|
4
|
+
* Docs: docs/pages/layout/grid.md
|
|
5
|
+
*
|
|
4
6
|
* .grid is space-driven: it fits as many tracks as --grid-min allows.
|
|
5
7
|
* .grid-N is count-driven: it declares a structural density of N and collapses
|
|
6
8
|
* through natural divisors of N.
|
package/src/css/layout/media.css
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/* Media object: fixed-width leading element (avatar, icon, image) + flexible
|
|
2
2
|
trailing content. Replaces the repeated "auto minmax(0, 1fr)" pattern in
|
|
3
3
|
author cards, comments, meta rows, and similar compositions.
|
|
4
|
-
Child contract: exactly two direct children (leading media + content).
|
|
4
|
+
Child contract: exactly two direct children (leading media + content).
|
|
5
|
+
Docs: docs/pages/layout/media.md */
|
|
5
6
|
.media {
|
|
6
7
|
display: grid;
|
|
7
8
|
grid-template-columns: auto minmax(0, 1fr);
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Scroll snap — optional horizontal rails for niche card strips and simple
|
|
3
3
|
* touch-friendly overflow areas.
|
|
4
4
|
*
|
|
5
|
+
* Docs: docs/pages/layout/scroll-snap.md
|
|
6
|
+
*
|
|
5
7
|
* This is not a full carousel. For previous/next controls, pagination,
|
|
6
8
|
* active state, drag behavior, looping, or robust desktop mouse interaction,
|
|
7
9
|
* use a dedicated carousel library.
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* header (.topbar) or underlaps it; scroll-margin reserves space on the
|
|
4
4
|
* target itself so the browser stops early.
|
|
5
5
|
*
|
|
6
|
+
* Docs: docs/pages/layout/scroll-target.md
|
|
7
|
+
*
|
|
6
8
|
* Apply to the elements that are link targets: <section id="...">, headings,
|
|
7
9
|
* etc. The scrollspy enhancer only reads ids — this offset is what actually
|
|
8
10
|
* shapes where the page lands, and the two are independent.
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Main content + secondary region.
|
|
3
3
|
*
|
|
4
|
+
* Docs: docs/pages/layout/sidebar-layout.md
|
|
5
|
+
*
|
|
4
6
|
* The children wrap according to the space allocated to this layout, without
|
|
5
7
|
* an ancestor opt-in or viewport threshold. The large growth ratio lets the
|
|
6
8
|
* main region reclaim the full row when the aside wraps instead of remaining
|
package/src/css/layout/stack.css
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
region re-spaces every nested stack, cluster and grid at any depth. Setting
|
|
10
10
|
the token stays correct on the element that consumes it, and
|
|
11
11
|
.compact/.spacious set it deliberately so a density context reaches nested layouts. See
|
|
12
|
-
docs/pages/layout/stack.md.
|
|
12
|
+
docs/pages/layout/stack.md.
|
|
13
|
+
Docs: docs/pages/layout/stack.md */
|
|
13
14
|
.stack {
|
|
14
15
|
display: flex;
|
|
15
16
|
flex-direction: column;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Peer regions that switch as a group.
|
|
3
3
|
*
|
|
4
|
+
* Docs: docs/pages/layout/switcher.md
|
|
5
|
+
*
|
|
4
6
|
* All children sit side by side when they all fit; otherwise every child
|
|
5
7
|
* stacks. Unlike .grid, this recipe cannot create a partial row such as two
|
|
6
8
|
* items plus one orphan.
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* background only — row content (search, breadcrumb, actions) stays local
|
|
5
5
|
* to the page.
|
|
6
6
|
*
|
|
7
|
+
* Docs: docs/pages/layout/topbar.md
|
|
8
|
+
*
|
|
7
9
|
* Not for marketing/editorial site headers — those are one-off visual
|
|
8
10
|
* identity per page; see docs/pages/patterns/overview.md's Header Navigation
|
|
9
11
|
* section. This is for the structural app-shell topbar, which recurs unchanged
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Optional fluid typography module.
|
|
3
3
|
*
|
|
4
|
+
* Docs: docs/pages/foundations/typography.md
|
|
5
|
+
*
|
|
4
6
|
* Part of the typography family (typography/index.css) and the full bundle; it
|
|
5
7
|
* is not in the minimal core, so a core-only import adds it explicitly when
|
|
6
8
|
* display, title, and lead sizes must scale with the viewport. Typical use
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* Lead paragraph: a slightly larger, more relaxed reading-introduction text.
|
|
2
2
|
Non-fluid and always available — use .text-lead from the optional fluid
|
|
3
|
-
module for marketing/display surfaces.
|
|
3
|
+
module for marketing/display surfaces.
|
|
4
|
+
Docs: docs/pages/foundations/typography.md */
|
|
4
5
|
.lead {
|
|
5
6
|
font-size: 1.125em;
|
|
6
7
|
line-height: var(--line-height-relaxed);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Overline — editorial small-cap label for section openers.
|
|
3
3
|
*
|
|
4
|
+
* Docs: docs/pages/foundations/typography.md
|
|
5
|
+
*
|
|
4
6
|
* Base form is unstyled chrome that takes its surrounding color. Add .pill for
|
|
5
7
|
* the bordered rounded shape, and combine with an intent class (.primary,
|
|
6
8
|
* .success, etc.) to tint the text, border, and background.
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Prose is the opt-in rich text scope for authored content.
|
|
3
3
|
*
|
|
4
|
+
* Docs: docs/pages/foundations/typography.md
|
|
5
|
+
*
|
|
4
6
|
* Global element styles stay mild so app UI is not shaped accidentally. Page
|
|
5
7
|
* layout, forms, cards, navigation, and app tables remain outside this scope.
|
|
6
8
|
*
|
|
@@ -143,7 +143,12 @@
|
|
|
143
143
|
text-decoration: none;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
/* :focus-visible stays in its own rule: in a mixed selector list an engine
|
|
147
|
+
without it would drop the whole rule, taking the :hover styling with it. */
|
|
148
|
+
.link-muted:hover {
|
|
149
|
+
color: var(--text);
|
|
150
|
+
}
|
|
151
|
+
|
|
147
152
|
.link-muted:focus-visible {
|
|
148
153
|
color: var(--text);
|
|
149
154
|
}
|
package/src/js/context-menu.js
CHANGED
|
@@ -48,7 +48,7 @@ function getContextScope(target) {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
function getLongPressDelay(target, menu) {
|
|
51
|
-
const value = target.dataset.contextMenuLongPress ?? menu
|
|
51
|
+
const value = target.dataset.contextMenuLongPress ?? menu?.dataset.contextMenuLongPress;
|
|
52
52
|
if (value == null || value === "false" || value === "none") return null;
|
|
53
53
|
if (value === "") return LONG_PRESS_MS;
|
|
54
54
|
|
|
@@ -147,21 +147,52 @@ function clearLongPress(state) {
|
|
|
147
147
|
state.pointerId = null;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
function
|
|
151
|
-
|
|
150
|
+
function releaseMenuReference(target, state) {
|
|
151
|
+
const menu = state.menu;
|
|
152
|
+
// contextByMenu backs the exported contextFor(): a released menu must not
|
|
153
|
+
// keep reporting a target it no longer belongs to. The context check keeps
|
|
154
|
+
// a menu another target has since claimed.
|
|
155
|
+
if (menu && contextFor(menu)?.context === target) {
|
|
156
|
+
contextByMenu.delete(menu);
|
|
157
|
+
}
|
|
158
|
+
state.releaseMenu?.();
|
|
159
|
+
state.releaseMenu = null;
|
|
160
|
+
state.release?.();
|
|
161
|
+
state.release = null;
|
|
162
|
+
state.menu = null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/*
|
|
166
|
+
* The menu is resolved per interaction, not once at connect: a target can be
|
|
167
|
+
* declared before its <menu> exists (streamed markup, late AJAX), and a
|
|
168
|
+
* same-id replacement must rewire to the live element. An unchanged menu
|
|
169
|
+
* costs a WeakMap read and a connection check; a changed one pays for
|
|
170
|
+
* releasing the old retain/menu wiring and acquiring the new.
|
|
171
|
+
*/
|
|
172
|
+
function resolveContextMenu(target, state) {
|
|
152
173
|
const menu = menuFor(target);
|
|
153
|
-
if (
|
|
174
|
+
if (state.menu === menu && menu?.isConnected) return menu;
|
|
154
175
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
176
|
+
releaseMenuReference(target, state);
|
|
177
|
+
if (!menu) return null;
|
|
178
|
+
|
|
179
|
+
state.release = retainSurface(menu);
|
|
180
|
+
state.menu = menu;
|
|
181
|
+
state.releaseMenu = connectMenu(menu, {
|
|
158
182
|
close: (menu) => closeSurface(menu),
|
|
159
183
|
});
|
|
184
|
+
return menu;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function connectContextTarget(target) {
|
|
188
|
+
if (contextMap.has(target)) return;
|
|
189
|
+
|
|
190
|
+
const controller = new AbortController();
|
|
160
191
|
const state = {
|
|
161
192
|
controller,
|
|
162
|
-
releaseMenu,
|
|
163
|
-
menu,
|
|
164
|
-
release,
|
|
193
|
+
releaseMenu: null,
|
|
194
|
+
menu: null,
|
|
195
|
+
release: null,
|
|
165
196
|
timer: null,
|
|
166
197
|
pointerId: null,
|
|
167
198
|
startX: 0,
|
|
@@ -169,10 +200,15 @@ function connectContextTarget(target) {
|
|
|
169
200
|
suppressClickUntil: 0,
|
|
170
201
|
};
|
|
171
202
|
|
|
203
|
+
resolveContextMenu(target, state);
|
|
204
|
+
|
|
172
205
|
target.addEventListener(
|
|
173
206
|
"contextmenu",
|
|
174
207
|
(e) => {
|
|
175
208
|
if (shouldIgnoreNativeTarget(e.target)) return;
|
|
209
|
+
const menu = resolveContextMenu(target, state);
|
|
210
|
+
// Without a resolvable menu the native context menu stays the fallback.
|
|
211
|
+
if (!menu) return;
|
|
176
212
|
e.preventDefault();
|
|
177
213
|
e.stopPropagation();
|
|
178
214
|
openContextMenu(target, menu, {
|
|
@@ -190,6 +226,8 @@ function connectContextTarget(target) {
|
|
|
190
226
|
(e) => {
|
|
191
227
|
if (e.key !== "ContextMenu" && !(e.shiftKey && e.key === "F10")) return;
|
|
192
228
|
if (shouldIgnoreNativeTarget(e.target)) return;
|
|
229
|
+
const menu = resolveContextMenu(target, state);
|
|
230
|
+
if (!menu) return;
|
|
193
231
|
e.preventDefault();
|
|
194
232
|
openFromKeyboard(target, menu, e.target, target);
|
|
195
233
|
},
|
|
@@ -199,7 +237,7 @@ function connectContextTarget(target) {
|
|
|
199
237
|
target.addEventListener(
|
|
200
238
|
"pointerdown",
|
|
201
239
|
(e) => {
|
|
202
|
-
const delay = getLongPressDelay(target,
|
|
240
|
+
const delay = getLongPressDelay(target, menuFor(target));
|
|
203
241
|
if (e.pointerType !== "touch" || delay == null || shouldIgnoreNativeTarget(e.target)) {
|
|
204
242
|
return;
|
|
205
243
|
}
|
|
@@ -210,6 +248,8 @@ function connectContextTarget(target) {
|
|
|
210
248
|
state.startY = e.clientY;
|
|
211
249
|
state.timer = setTimeout(() => {
|
|
212
250
|
state.timer = null;
|
|
251
|
+
const menu = resolveContextMenu(target, state);
|
|
252
|
+
if (!menu) return;
|
|
213
253
|
state.suppressClickUntil = Date.now() + LONG_PRESS_CLICK_SUPPRESSION_MS;
|
|
214
254
|
openContextMenu(target, menu, {
|
|
215
255
|
x: state.startX,
|
|
@@ -268,6 +308,9 @@ function connectContextTarget(target) {
|
|
|
268
308
|
)
|
|
269
309
|
return;
|
|
270
310
|
|
|
311
|
+
const menu = resolveContextMenu(target, state);
|
|
312
|
+
if (!menu) return;
|
|
313
|
+
|
|
271
314
|
e.preventDefault();
|
|
272
315
|
e.stopPropagation();
|
|
273
316
|
|
|
@@ -294,12 +337,10 @@ function disconnectContextTarget(target) {
|
|
|
294
337
|
if (!state) return;
|
|
295
338
|
clearLongPress(state);
|
|
296
339
|
state.controller.abort();
|
|
297
|
-
state.
|
|
298
|
-
if (contextFor(state.menu)?.context === target) {
|
|
299
|
-
contextByMenu.delete(state.menu);
|
|
340
|
+
if (state.menu && contextFor(state.menu)?.context === target) {
|
|
300
341
|
closeSurface(state.menu, { restoreFocus: false });
|
|
301
342
|
}
|
|
302
|
-
state
|
|
343
|
+
releaseMenuReference(target, state);
|
|
303
344
|
contextMap.delete(target);
|
|
304
345
|
}
|
|
305
346
|
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* enhancement-loader.js — Server-declared enhancement manifest loader.
|
|
3
3
|
*
|
|
4
|
-
* Reads <script type="application/json" data-enhance-modules
|
|
4
|
+
* Reads <script type="application/json" data-enhance-modules> blocks or
|
|
5
5
|
* the Enhance-Modules HTTP header, dynamically imports the declared ES
|
|
6
6
|
* module entrypoints, and registers them with the Actual lifecycle engine.
|
|
7
7
|
*
|
|
8
|
+
* A manifest is trusted application configuration: every declared URL is
|
|
9
|
+
* imported and executed (or loaded as script/style), so manifests must be
|
|
10
|
+
* produced by the server — never built from untrusted input.
|
|
11
|
+
*
|
|
8
12
|
* Manifests declare document-wide modules: dynamically loaded enhancements
|
|
9
13
|
* always register on the default document root. A scope passed to
|
|
10
14
|
* loadEnhancements() only limits where manifest blocks are looked up.
|
package/src/js/escape.js
CHANGED
|
@@ -25,7 +25,17 @@ export function registerEscapeDismissal(element, dismiss) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
function onDocumentEscape(event) {
|
|
28
|
-
|
|
28
|
+
// A closer handler that consumed the key (defaultPrevented) keeps its
|
|
29
|
+
// claim; Escape with any modifier is not a dismissal.
|
|
30
|
+
if (
|
|
31
|
+
event.defaultPrevented ||
|
|
32
|
+
event.key !== "Escape" ||
|
|
33
|
+
event.ctrlKey ||
|
|
34
|
+
event.altKey ||
|
|
35
|
+
event.shiftKey ||
|
|
36
|
+
event.metaKey
|
|
37
|
+
)
|
|
38
|
+
return;
|
|
29
39
|
|
|
30
40
|
const stack = dismissableStacks.get(event.currentTarget);
|
|
31
41
|
const entry = stack.at(-1);
|
package/src/js/flyout.js
CHANGED
|
@@ -43,9 +43,14 @@ function onTriggerClick(e) {
|
|
|
43
43
|
const trigger = e.currentTarget;
|
|
44
44
|
const state = triggerMap.get(trigger);
|
|
45
45
|
if (!state) return;
|
|
46
|
-
e.stopPropagation();
|
|
47
46
|
const panel = resolvePanel(trigger, state);
|
|
48
47
|
if (!panel) return;
|
|
48
|
+
// A link trigger is legal markup; the interaction is claimed only once a
|
|
49
|
+
// panel has actually resolved — an unresolved trigger still navigates and
|
|
50
|
+
// still bubbles. The stop is what keeps the document-level surface handler
|
|
51
|
+
// from closing the panel this same click just opened.
|
|
52
|
+
e.preventDefault();
|
|
53
|
+
e.stopPropagation();
|
|
49
54
|
if (isSurfaceOpen(panel)) closeSurface(panel);
|
|
50
55
|
else openFlyout(panel, trigger);
|
|
51
56
|
}
|
package/src/js/focus.js
CHANGED
|
@@ -17,10 +17,15 @@ const FOCUSABLE_SELECTOR = [
|
|
|
17
17
|
*/
|
|
18
18
|
export function isElementVisible(el) {
|
|
19
19
|
if (!el || el.hidden) return false;
|
|
20
|
-
if (typeof el.checkVisibility === "function")
|
|
20
|
+
if (typeof el.checkVisibility === "function") {
|
|
21
|
+
// Bare checkVisibility() only tests for a layout box — visibility:hidden
|
|
22
|
+
// requires the option. Both names are passed because the engines that
|
|
23
|
+
// shipped first knew it as checkVisibilityCSS.
|
|
24
|
+
return el.checkVisibility({ visibilityProperty: true, checkVisibilityCSS: true });
|
|
25
|
+
}
|
|
21
26
|
if (el.getClientRects().length === 0) return false;
|
|
22
|
-
//
|
|
23
|
-
//
|
|
27
|
+
// The fallback reproduces the option's coverage through the computed style
|
|
28
|
+
// so a hidden element is not focusable.
|
|
24
29
|
const style = el.ownerDocument?.defaultView?.getComputedStyle?.(el);
|
|
25
30
|
if (!style) return true;
|
|
26
31
|
return style.visibility !== "hidden" && style.visibility !== "collapse";
|
package/src/js/surface.js
CHANGED
|
@@ -152,9 +152,36 @@ function ensureSurfaceWired(menu) {
|
|
|
152
152
|
};
|
|
153
153
|
|
|
154
154
|
surfaceMap.set(menu, state);
|
|
155
|
+
menu.addEventListener(EVENTS.dismiss, onSurfaceDismiss);
|
|
155
156
|
return state;
|
|
156
157
|
}
|
|
157
158
|
|
|
159
|
+
/*
|
|
160
|
+
* The generic `--dismiss` command hides its target by writing `hidden` — but
|
|
161
|
+
* on a surface the attribute is not the closed state, the popover transport
|
|
162
|
+
* is. Left alone it would strand the open lifecycle: the Escape entry, the
|
|
163
|
+
* position tracker and the is-open class would survive, and openSurface()
|
|
164
|
+
* would refuse to act on a panel that reads open. The surface therefore
|
|
165
|
+
* consumes the event to finalize its own lifecycle, rather than dismiss.js
|
|
166
|
+
* learning about surfaces. `hidden` comes back off once the transport state
|
|
167
|
+
* is clean; prepareSurface() also strips it on the next open.
|
|
168
|
+
* A dismissal bubbled from a nested target is not the surface's own.
|
|
169
|
+
*/
|
|
170
|
+
function onSurfaceDismiss(event) {
|
|
171
|
+
if (event.target !== event.currentTarget) return;
|
|
172
|
+
const menu = event.currentTarget;
|
|
173
|
+
// dismiss.js writes `hidden` before dispatching, and hiding an ancestor can
|
|
174
|
+
// already have dropped focus out of the surface — so the command's trigger,
|
|
175
|
+
// not activeElement, is the reliable witness of where the activation came
|
|
176
|
+
// from. A trigger inside the surface means focus was inside it and must be
|
|
177
|
+
// restored to the opener; a trigger outside keeps focus where it landed.
|
|
178
|
+
const trigger = event.detail?.trigger;
|
|
179
|
+
closeSurface(menu, {
|
|
180
|
+
restoreFocus: trigger ? menu.contains(trigger) : undefined,
|
|
181
|
+
});
|
|
182
|
+
menu.removeAttribute("hidden");
|
|
183
|
+
}
|
|
184
|
+
|
|
158
185
|
// Escape and positional tracking belong to the *open* surface, not to the
|
|
159
186
|
// wiring. They start when the surface opens and stop when it closes, so a
|
|
160
187
|
// closed surface never participates in the dismissable stack or in
|
|
@@ -341,6 +368,7 @@ export function closeSurface(menu, opts = {}) {
|
|
|
341
368
|
export function disconnectSurface(menu) {
|
|
342
369
|
if (!menu) return;
|
|
343
370
|
closeSurface(menu);
|
|
371
|
+
menu.removeEventListener(EVENTS.dismiss, onSurfaceDismiss);
|
|
344
372
|
const state = surfaceMap.get(menu);
|
|
345
373
|
if (state) {
|
|
346
374
|
state.unregisterEscape?.();
|
package/src/js/tooltip.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* work automatically via bubbling delegated listeners.
|
|
25
25
|
*
|
|
26
26
|
* Show: hover + focus (150ms delay), click toggle, or always visible
|
|
27
|
-
* Hide:
|
|
27
|
+
* Hide: focus leaving the trigger (focusout), pointer leave, Escape
|
|
28
28
|
*
|
|
29
29
|
* Geometry: a trigger scrolled out of the positioning boundary takes its tip
|
|
30
30
|
* down and keeps it tracked, so scrolling back brings the same tip
|
|
@@ -420,8 +420,12 @@ function ensureTip(trigger) {
|
|
|
420
420
|
{ signal: triggerController.signal },
|
|
421
421
|
);
|
|
422
422
|
trigger.addEventListener(
|
|
423
|
-
|
|
424
|
-
|
|
423
|
+
// blur does not bubble: on a composite trigger (focusable children) the
|
|
424
|
+
// loss of focus is only observable through focusout. relatedTarget keeps
|
|
425
|
+
// a focus move between the trigger's own children from ending the tip.
|
|
426
|
+
"focusout",
|
|
427
|
+
(event) => {
|
|
428
|
+
if (event.relatedTarget && trigger.contains(event.relatedTarget)) return;
|
|
425
429
|
state.focused = false;
|
|
426
430
|
scheduleHide(tip);
|
|
427
431
|
},
|
|
@@ -489,8 +493,10 @@ function handleTriggerIntent(e) {
|
|
|
489
493
|
const trigger = e.target.closest?.(SEL);
|
|
490
494
|
if (!trigger) return;
|
|
491
495
|
if (isClickTrigger(trigger) || isAlwaysVisible(trigger)) return;
|
|
496
|
+
// A pointer or focus move between the trigger's own children is not a new
|
|
497
|
+
// intent: it must not restart the show delay or rewrite the intent flags.
|
|
492
498
|
if (
|
|
493
|
-
e.type === "mouseover" &&
|
|
499
|
+
(e.type === "mouseover" || e.type === "focusin") &&
|
|
494
500
|
e.relatedTarget instanceof Node &&
|
|
495
501
|
trigger.contains(e.relatedTarget)
|
|
496
502
|
)
|