actual-css 0.6.0 → 0.7.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 +48 -1
- package/dist/actual.full.css +109 -14
- package/dist/actual.full.js +1 -1
- package/dist/actual.full.min.css +1 -1
- package/docs/design-notes/browser-support.md +39 -14
- package/docs/design-notes/css-anchor.md +1 -1
- package/docs/design-notes/platform-alignment.md +51 -0
- package/docs/design-notes/popover-manual-poc.md +248 -0
- package/docs/design-notes/surface-reparenting.md +122 -0
- package/docs/design-notes/widget-primitives.md +2 -2
- package/docs/pages/enhancements/flyout.md +92 -2
- package/docs/pages/enhancements/status-bar.md +20 -0
- package/docs/pages/enhancements/tooltip.md +12 -0
- package/docs/pages/foundations/philosophy.md +15 -1
- package/docs/pages/foundations/theming.md +8 -0
- package/docs/pages/guides/progressive-enhancement.md +29 -27
- package/package.json +5 -3
- package/src/css/components/flyout.css +49 -3
- package/src/css/components/join.css +7 -0
- package/src/css/components/menu.css +16 -10
- package/src/css/components/tooltip.css +37 -1
- package/src/js/status.js +38 -2
- package/src/js/surface.js +28 -12
- package/src/js/tooltip.js +2 -2
- package/src/js/floating.js +0 -432
|
@@ -17,6 +17,18 @@ with the `hidden` attribute.
|
|
|
17
17
|
- The arrow inherits the tooltip background — custom gradients carry through automatically.
|
|
18
18
|
- Avoid relying on the native `title` attribute as the primary implementation.
|
|
19
19
|
|
|
20
|
+
A `.tooltip[popover]` may use a native or third-party-owned popover lifecycle.
|
|
21
|
+
Actual removes the native inset, margin, and overflow that conflict with
|
|
22
|
+
positioning and with the projecting arrow, while leaving bare `[popover]`
|
|
23
|
+
elements untouched. The lifecycle owner must still write the fixed viewport
|
|
24
|
+
coordinates and arrow position; Actual only supplies the presentation.
|
|
25
|
+
|
|
26
|
+
That reset is gated on `@supports selector(:popover-open)`. Native Popover is
|
|
27
|
+
optional and is not part of Actual's Minimal contract: browsers within the
|
|
28
|
+
Minimal range may not support it, and there the attribute carries no behavior,
|
|
29
|
+
so nothing shows or hides the tip. Use `data-tooltip` and the built-in runtime
|
|
30
|
+
when the lifecycle must work across the full Minimal range.
|
|
31
|
+
|
|
20
32
|
```html demo
|
|
21
33
|
<button class="btn ghost"
|
|
22
34
|
type="button"
|
|
@@ -75,6 +75,19 @@ As inherited contexts, one class tightens or loosens the whole subtree:
|
|
|
75
75
|
</div>
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
+
Application chrome can use the same scope; no separate `.compact` mode is
|
|
79
|
+
needed:
|
|
80
|
+
|
|
81
|
+
```html demo
|
|
82
|
+
<header class="navbar sm">
|
|
83
|
+
<a class="navbar-brand" href="/">Workspace</a>
|
|
84
|
+
<nav class="cluster" aria-label="Workspace actions">
|
|
85
|
+
<button class="btn ghost">Search</button>
|
|
86
|
+
<button class="btn primary">Create</button>
|
|
87
|
+
</nav>
|
|
88
|
+
</header>
|
|
89
|
+
```
|
|
90
|
+
|
|
78
91
|
The same class on a component is local density, not a second meaning:
|
|
79
92
|
|
|
80
93
|
```html demo
|
|
@@ -86,7 +99,8 @@ This is not "`.sm` sometimes means density and sometimes size": `.sm` always
|
|
|
86
99
|
establishes the same density tokens — only the scope changes. The density
|
|
87
100
|
tokens are `--gap`, `--density-space`, `--control-size`, and
|
|
88
101
|
`--density-compact-size`. Components consume the dimensions that make sense
|
|
89
|
-
for them and opt out of the rest.
|
|
102
|
+
for them and opt out of the rest. The absolute `--space-*` scale is not rebound
|
|
103
|
+
by density scopes.
|
|
90
104
|
|
|
91
105
|
Components with bespoke sizing (`.avatar`, `.spinner`) define local `.sm`/`.lg`
|
|
92
106
|
instead of consuming the shared tokens.
|
|
@@ -70,6 +70,14 @@ Native controls that no component class restyles — a bare checkbox, radio, ran
|
|
|
70
70
|
|
|
71
71
|
→ Forms · Overview · Foundations · Tokens (theme contract)
|
|
72
72
|
|
|
73
|
+
### Store a theme name without applying it
|
|
74
|
+
|
|
75
|
+
`data-theme` is an active theme boundary on every element that carries it, not
|
|
76
|
+
a generic storage attribute. Use a different name such as `data-theme-value`,
|
|
77
|
+
or a form control's `value`, when the element should only hold a theme name.
|
|
78
|
+
Apply `data-theme` deliberately when that element and its subtree should paint
|
|
79
|
+
with the selected palette — a theme swatch is a useful example.
|
|
80
|
+
|
|
73
81
|
### Loading / busy overlay on a container
|
|
74
82
|
|
|
75
83
|
Use `aria-busy="true"` with a direct last-child `.spinner`; tint the overlay with `--busy-overlay-bg`.
|
|
@@ -124,19 +124,18 @@ The JS is designed to run on its own, without Actual's stylesheet.
|
|
|
124
124
|
## The runtime as a primitive kit
|
|
125
125
|
|
|
126
126
|
For building custom widgets (select, tags, date picker) on Actual's primitives,
|
|
127
|
-
see the widget-primitives design note. Every primitive subpath is
|
|
128
|
-
`package.json#exports`:
|
|
129
|
-
|
|
130
|
-
| Subpath
|
|
131
|
-
|
|
|
132
|
-
| `actual-css/js/enhance`
|
|
133
|
-
| `actual-css/js/escape`
|
|
134
|
-
| `actual-css/js/events`
|
|
135
|
-
| `actual-css/js/
|
|
136
|
-
| `actual-css/js/
|
|
137
|
-
| `actual-css/js/
|
|
138
|
-
| `actual-css/js/
|
|
139
|
-
| `actual-css/js/surface` | Surface lifecycle (`openSurface`, `closeSurface`, …) |
|
|
127
|
+
see the widget-primitives design note. Every Actual runtime primitive subpath is
|
|
128
|
+
published in `package.json#exports`:
|
|
129
|
+
|
|
130
|
+
| Subpath | Purpose |
|
|
131
|
+
| ----------------------- | ---------------------------------------------------------------- |
|
|
132
|
+
| `actual-css/js/enhance` | DOM lifecycle engine: `enhance()`, `registerEnhancement`, tokens |
|
|
133
|
+
| `actual-css/js/escape` | Per-document LIFO Escape dismissal stack |
|
|
134
|
+
| `actual-css/js/events` | The `actual:*` event name constants |
|
|
135
|
+
| `actual-css/js/focus` | Focusable-item lookup and helpers |
|
|
136
|
+
| `actual-css/js/keys` | Roving-focus item navigation helpers |
|
|
137
|
+
| `actual-css/js/menu` | Menu keyboard/click wiring, item vocabulary |
|
|
138
|
+
| `actual-css/js/surface` | Surface lifecycle (`openSurface`, `closeSurface`, …) |
|
|
140
139
|
|
|
141
140
|
The exports map is explicit — it *is* the statement of what is public.
|
|
142
141
|
|
|
@@ -149,33 +148,36 @@ computes and writes inline `left`/`top`, `data-placement`,
|
|
|
149
148
|
caller owns the floating element's presentation entirely.
|
|
150
149
|
|
|
151
150
|
```js
|
|
152
|
-
import { autoUpdate, reposition, repositionAt } from "
|
|
151
|
+
import { autoUpdate, reposition, repositionAt } from "@lekoala/floating";
|
|
153
152
|
|
|
154
|
-
const stop = autoUpdate(float, () => {
|
|
153
|
+
const stop = autoUpdate(trigger, float, () => {
|
|
155
154
|
reposition(trigger, float, { placement: "bottom-start", distance: 4 });
|
|
156
155
|
});
|
|
157
156
|
```
|
|
158
157
|
|
|
159
158
|
Options passed to `reposition()`:
|
|
160
159
|
|
|
161
|
-
| Option | Default | Contract
|
|
162
|
-
| -------------- | ---------------- |
|
|
163
|
-
| `placement` | `"bottom-start"` | Preferred side and alignment
|
|
164
|
-
| `distance` | `0` | Gap from the reference element
|
|
165
|
-
| `flip` | `true` | Flip when the preferred side is out of view
|
|
166
|
-
| `shift` | `true` | Shift along the
|
|
167
|
-
| `shiftPadding` | `4` | Minimum space kept from the boundary
|
|
168
|
-
| `scope` | viewport | Boundary element for overflow decisions
|
|
160
|
+
| Option | Default | Contract |
|
|
161
|
+
| -------------- | ---------------- | -------------------------------------------- |
|
|
162
|
+
| `placement` | `"bottom-start"` | Preferred side and alignment |
|
|
163
|
+
| `distance` | `0` | Gap from the reference element |
|
|
164
|
+
| `flip` | `true` | Flip when the preferred side is out of view |
|
|
165
|
+
| `shift` | `true` | Shift along the cross axis to stay in bounds |
|
|
166
|
+
| `shiftPadding` | `4` | Minimum space kept from the boundary |
|
|
167
|
+
| `scope` | viewport | Boundary element for overflow decisions |
|
|
169
168
|
|
|
170
169
|
`placement` takes a `-start` or `-end` alignment suffix.
|
|
171
170
|
|
|
172
|
-
`
|
|
173
|
-
|
|
171
|
+
`@lekoala/floating` is the standalone positioning dependency used by the
|
|
172
|
+
runtime. `autoUpdate(reference, float, callback)` batches scroll, resize, and
|
|
173
|
+
element-resize callbacks per document and returns a stop handle; it observes
|
|
174
|
+
both the reference and floating element while the surface is open. Pass `null`
|
|
175
|
+
as the reference for point-positioned surfaces. It does not keep a closed or
|
|
174
176
|
hidden element positioned. `repositionAt(x, y, float, opts)` positions relative
|
|
175
177
|
to a fixed point — used by context menus.
|
|
176
178
|
|
|
177
|
-
A third-party widget importing only
|
|
178
|
-
able to position a listbox under an input — that is the test.
|
|
179
|
+
A third-party widget importing only `@lekoala/floating` with no Actual stylesheet
|
|
180
|
+
must be able to position a listbox under an input — that is the test.
|
|
179
181
|
|
|
180
182
|
## Surface contract
|
|
181
183
|
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "actual-css",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Plain CSS component framework with semantic classes, universal variants, small tokens, theme hooks, and progressive enhancements.",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"packageManager": "bun@1.4.
|
|
6
|
+
"packageManager": "bun@1.4.1",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"author": "Thomas <thomas@lekoala.be>",
|
|
9
9
|
"repository": {
|
|
@@ -58,7 +58,6 @@
|
|
|
58
58
|
"./js/parse-config": "./src/js/parse-config.js",
|
|
59
59
|
"./js/events": "./src/js/events.js",
|
|
60
60
|
"./js/filter": "./src/js/filter.js",
|
|
61
|
-
"./js/floating": "./src/js/floating.js",
|
|
62
61
|
"./js/flyout": "./src/js/flyout.js",
|
|
63
62
|
"./js/focus": "./src/js/focus.js",
|
|
64
63
|
"./js/focus-group": "./src/js/focus-group.js",
|
|
@@ -143,5 +142,8 @@
|
|
|
143
142
|
"devDependencies": {
|
|
144
143
|
"@biomejs/biome": "^2.5",
|
|
145
144
|
"happy-dom": "^20.11"
|
|
145
|
+
},
|
|
146
|
+
"dependencies": {
|
|
147
|
+
"@lekoala/floating": "^0.1.0"
|
|
146
148
|
}
|
|
147
149
|
}
|
|
@@ -79,12 +79,50 @@
|
|
|
79
79
|
transition: opacity var(--duration-fast) ease;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
/* Native popovers enter the top layer with UA geometry that conflicts with
|
|
83
|
+
anchored positioning. The reset is gated on real Popover support because an
|
|
84
|
+
attribute selector matches whether or not the engine implements Popover:
|
|
85
|
+
without it the UA neither hides nor promotes the element, so an ungated
|
|
86
|
+
reset would strand a permanently visible panel at inset: auto. Popover is
|
|
87
|
+
not part of Actual's Minimal contract — engines inside the Minimal range
|
|
88
|
+
may lack it — so the gate is about the capability, not the floor. Kept
|
|
89
|
+
scoped to Actual surfaces so bare [popover] elements retain their platform
|
|
90
|
+
presentation. */
|
|
91
|
+
@supports selector(:popover-open) {
|
|
92
|
+
.flyout[popover] {
|
|
93
|
+
position: fixed;
|
|
94
|
+
inset: auto;
|
|
95
|
+
margin: 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* A presentation class may own its *visible* display type, but must not
|
|
99
|
+
override the platform's hidden state. The UA closes a popover with
|
|
100
|
+
display: none, and an author declaration beats the UA origin whatever the
|
|
101
|
+
specificity — so .flyout's own display: grid would leave a closed popover
|
|
102
|
+
painted at its static position. Restoring both properties here also gives
|
|
103
|
+
the native path the exit target the runtime path already has in
|
|
104
|
+
.flyout[hidden]: without an opacity to animate, the overlay/display
|
|
105
|
+
allow-discrete transition below would hold a fully opaque panel in the top
|
|
106
|
+
layer and then snap it away. */
|
|
107
|
+
.flyout[popover]:not(:popover-open) {
|
|
108
|
+
display: none;
|
|
109
|
+
opacity: 0;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* compat-ok: :popover-open rides in a forgiving :is(), where an engine without
|
|
114
|
+
Popover discards just that member and .is-open alone keeps driving the open
|
|
115
|
+
state. This is a property of the forgiving list, not of the pseudo-class: in
|
|
116
|
+
a plain selector list an unsupported member would invalidate the whole rule.
|
|
117
|
+
Keep it inside :is()/:where() or gate it. */
|
|
118
|
+
.flyout:is(.is-open, :popover-open):not(.is-sheet) {
|
|
83
119
|
inset: auto;
|
|
84
120
|
}
|
|
85
121
|
|
|
122
|
+
/* compat-ok: forgiving :is() again, and @starting-style is itself unsupported
|
|
123
|
+
across part of the Minimal range, so the rule simply does not exist there. */
|
|
86
124
|
@starting-style {
|
|
87
|
-
.flyout.is-open {
|
|
125
|
+
.flyout:is(.is-open, :popover-open) {
|
|
88
126
|
opacity: 0;
|
|
89
127
|
}
|
|
90
128
|
}
|
|
@@ -123,16 +161,23 @@
|
|
|
123
161
|
translate: 0 var(--space-30);
|
|
124
162
|
}
|
|
125
163
|
|
|
164
|
+
/* compat-ok: forgiving :is() inside @starting-style, as above. */
|
|
126
165
|
@starting-style {
|
|
127
|
-
.flyout.is-sheet.is-open {
|
|
166
|
+
.flyout.is-sheet:is(.is-open, :popover-open) {
|
|
128
167
|
translate: 0 var(--space-30);
|
|
129
168
|
}
|
|
130
169
|
}
|
|
131
170
|
|
|
171
|
+
/* display keeps the exit transition alive for the [hidden] path; overlay does
|
|
172
|
+
the same for the native popover path, where hidePopover() would otherwise
|
|
173
|
+
drop the panel out of the top layer on the first frame and let a parent
|
|
174
|
+
overflow or a dialog clip the fade. An engine that cannot transition either
|
|
175
|
+
property ignores it and the exit is instant. */
|
|
132
176
|
@supports (transition-behavior: allow-discrete) {
|
|
133
177
|
.flyout {
|
|
134
178
|
transition:
|
|
135
179
|
opacity var(--duration-fast) ease,
|
|
180
|
+
overlay var(--duration-fast) allow-discrete,
|
|
136
181
|
display var(--duration-fast) allow-discrete;
|
|
137
182
|
}
|
|
138
183
|
|
|
@@ -140,6 +185,7 @@
|
|
|
140
185
|
transition:
|
|
141
186
|
opacity var(--duration-fast) ease,
|
|
142
187
|
translate var(--duration-fast) ease,
|
|
188
|
+
overlay var(--duration-fast) allow-discrete,
|
|
143
189
|
display var(--duration-fast) allow-discrete;
|
|
144
190
|
}
|
|
145
191
|
}
|
|
@@ -63,6 +63,13 @@
|
|
|
63
63
|
border-end-start-radius: var(--join-es);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/* A square icon button has an explicit block size, which otherwise prevents
|
|
67
|
+
align-items: stretch from matching a taller labelled sibling. The button's
|
|
68
|
+
existing min-block-size keeps the standalone square-size invariant intact. */
|
|
69
|
+
.join > .btn.icon-only {
|
|
70
|
+
block-size: auto;
|
|
71
|
+
}
|
|
72
|
+
|
|
66
73
|
.join > :first-child {
|
|
67
74
|
--join-ss: var(--join-radius);
|
|
68
75
|
--join-es: var(--join-radius);
|
|
@@ -75,30 +75,36 @@
|
|
|
75
75
|
/* Checkable menu items are one command with one accessible state. Their
|
|
76
76
|
indicator is generated so authors do not add a second interactive input. */
|
|
77
77
|
.menu-item:is([role="menuitemcheckbox"], [role="menuitemradio"])::before {
|
|
78
|
-
|
|
78
|
+
content: "";
|
|
79
|
+
display: block;
|
|
79
80
|
flex: 0 0 var(--menu-item-icon-size);
|
|
80
81
|
inline-size: var(--menu-item-icon-size);
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
.menu-item[role="menuitemcheckbox"]::before {
|
|
85
|
-
content: "";
|
|
82
|
+
block-size: var(--menu-item-icon-size);
|
|
86
83
|
}
|
|
87
84
|
|
|
88
85
|
.menu-item[role="menuitemcheckbox"][aria-checked="true"]::before {
|
|
89
|
-
|
|
86
|
+
background: currentColor;
|
|
87
|
+
-webkit-mask: var(--icon-check) center / contain no-repeat;
|
|
88
|
+
mask: var(--icon-check) center / contain no-repeat;
|
|
90
89
|
}
|
|
91
90
|
|
|
92
91
|
.menu-item[role="menuitemcheckbox"][aria-checked="mixed"]::before {
|
|
93
|
-
|
|
92
|
+
background:
|
|
93
|
+
linear-gradient(currentColor 0 0) center / 60% max(var(--border-width), 0.125em) no-repeat;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
.menu-item[role="menuitemradio"]::before {
|
|
97
|
-
|
|
97
|
+
background: radial-gradient(circle, transparent 0 46%, currentColor 48% 58%, transparent 60%);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
.menu-item[role="menuitemradio"][aria-checked="true"]::before {
|
|
101
|
-
|
|
101
|
+
background: radial-gradient(
|
|
102
|
+
circle,
|
|
103
|
+
currentColor 0 25%,
|
|
104
|
+
transparent 27% 46%,
|
|
105
|
+
currentColor 48% 58%,
|
|
106
|
+
transparent 60%
|
|
107
|
+
);
|
|
102
108
|
}
|
|
103
109
|
|
|
104
110
|
.menu-item:focus-visible {
|
|
@@ -39,21 +39,57 @@
|
|
|
39
39
|
transition: opacity var(--duration-fast) ease;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/* Neutralize only the native geometry that conflicts with a positioned
|
|
43
|
+
tooltip. In particular, the UA overflow would turn the arrow into scroll
|
|
44
|
+
overflow instead of letting it project beyond the bubble. Gated on real
|
|
45
|
+
Popover support: the attribute selector matches whether or not the engine
|
|
46
|
+
implements Popover, and without it nothing hides the element, so an ungated
|
|
47
|
+
reset would leave a stray bubble parked at inset: auto. Popover sits outside
|
|
48
|
+
Actual's Minimal contract, so the gate tracks the capability rather than the
|
|
49
|
+
floor. */
|
|
50
|
+
@supports selector(:popover-open) {
|
|
51
|
+
.tooltip[popover] {
|
|
52
|
+
inset: auto;
|
|
53
|
+
margin: 0;
|
|
54
|
+
overflow: visible;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* .tooltip declares no display of its own, so the UA already hides a closed
|
|
58
|
+
popover here. The rule is written anyway: both components advertised as
|
|
59
|
+
Popover-compatible should cede the hidden state the same way, and the
|
|
60
|
+
opacity target is what actually makes the native exit fade — see the
|
|
61
|
+
overlay note below. */
|
|
62
|
+
.tooltip[popover]:not(:popover-open) {
|
|
63
|
+
display: none;
|
|
64
|
+
opacity: 0;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
42
68
|
.tooltip[hidden] {
|
|
43
69
|
display: none;
|
|
44
70
|
opacity: 0;
|
|
45
71
|
}
|
|
46
72
|
|
|
73
|
+
/* Entry opacity for both lifecycles: a runtime-driven tip is visible when it
|
|
74
|
+
is neither a popover nor [hidden], a native one when it is :popover-open.
|
|
75
|
+
compat-ok: [popover] appears here only under negation, which every engine
|
|
76
|
+
evaluates correctly whether or not it implements Popover, :popover-open
|
|
77
|
+
rides in a forgiving :is(), and @starting-style is itself unsupported across
|
|
78
|
+
part of the Minimal range — so the rule does not exist there at all. */
|
|
47
79
|
@starting-style {
|
|
48
|
-
.tooltip:not([hidden]) {
|
|
80
|
+
.tooltip:is(:not([popover]):not([hidden]), :popover-open) {
|
|
49
81
|
opacity: 0;
|
|
50
82
|
}
|
|
51
83
|
}
|
|
52
84
|
|
|
85
|
+
/* overlay is the native popover counterpart of display: it holds the tip in
|
|
86
|
+
the top layer until the fade finishes, instead of letting hidePopover()
|
|
87
|
+
demote it on the first frame. Ignored where unsupported. */
|
|
53
88
|
@supports (transition-behavior: allow-discrete) {
|
|
54
89
|
.tooltip {
|
|
55
90
|
transition:
|
|
56
91
|
opacity var(--duration-fast) ease,
|
|
92
|
+
overlay var(--duration-fast) allow-discrete,
|
|
57
93
|
display var(--duration-fast) allow-discrete;
|
|
58
94
|
}
|
|
59
95
|
}
|
package/src/js/status.js
CHANGED
|
@@ -31,6 +31,12 @@
|
|
|
31
31
|
* - Any other code, without importing this module: dispatch `actual:status`
|
|
32
32
|
* directly — `detail: { message, intent, duration }`, or `{}` to clear.
|
|
33
33
|
*
|
|
34
|
+
* A message originating inside an open dialog temporarily mounts the singleton
|
|
35
|
+
* bar in that dialog. Content outside a modal dialog is inert and cannot sit
|
|
36
|
+
* above its top layer, so this keeps both the visual feedback and live region
|
|
37
|
+
* in the active subtree. Programmatic callers pass the originating element as
|
|
38
|
+
* `options.source`; event and command entry points propagate it automatically.
|
|
39
|
+
*
|
|
34
40
|
* The element contract is `[data-status][role="status"]`. Both are required:
|
|
35
41
|
* "status" is a generic domain word, so `role="status"` keeps the
|
|
36
42
|
* document-wide lookup from matching an unrelated app element that happens to
|
|
@@ -59,12 +65,39 @@ let statusTimer;
|
|
|
59
65
|
// never emptied by the previous message's cleanup.
|
|
60
66
|
let statusGeneration = 0;
|
|
61
67
|
let viewportTracker = null;
|
|
68
|
+
const statusHomes = new WeakMap();
|
|
62
69
|
|
|
63
70
|
function statusTarget() {
|
|
64
71
|
if (typeof document === "undefined") return null;
|
|
65
72
|
return document.querySelector(STATUS_SELECTOR);
|
|
66
73
|
}
|
|
67
74
|
|
|
75
|
+
function restoreStatusTarget(target) {
|
|
76
|
+
const home = statusHomes.get(target);
|
|
77
|
+
if (!home) return;
|
|
78
|
+
|
|
79
|
+
const parent = home.parent?.isConnected ? home.parent : target.ownerDocument?.body;
|
|
80
|
+
if (parent) {
|
|
81
|
+
const reference = home.nextSibling?.parentNode === parent ? home.nextSibling : null;
|
|
82
|
+
parent.insertBefore(target, reference);
|
|
83
|
+
}
|
|
84
|
+
statusHomes.delete(target);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function mountStatusTarget(target, source) {
|
|
88
|
+
const dialog = source?.closest?.("dialog[open]");
|
|
89
|
+
if (!dialog || dialog.ownerDocument !== target.ownerDocument) {
|
|
90
|
+
restoreStatusTarget(target);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (dialog.contains(target)) return;
|
|
94
|
+
|
|
95
|
+
if (!statusHomes.has(target)) {
|
|
96
|
+
statusHomes.set(target, { parent: target.parentNode, nextSibling: target.nextSibling });
|
|
97
|
+
}
|
|
98
|
+
dialog.append(target);
|
|
99
|
+
}
|
|
100
|
+
|
|
68
101
|
// The bar is fixed to the layout viewport, so the mobile software keyboard
|
|
69
102
|
// covers it. visualViewport reports the occluded band; the CSS adds it to the
|
|
70
103
|
// bottom inset. Tracking runs only while a message is on screen.
|
|
@@ -130,6 +163,8 @@ export function status(message, options = {}) {
|
|
|
130
163
|
clearTimeout(statusTimer);
|
|
131
164
|
statusGeneration++;
|
|
132
165
|
|
|
166
|
+
mountStatusTarget(target, options.source);
|
|
167
|
+
|
|
133
168
|
clearIntentClasses(target);
|
|
134
169
|
const intent = intentClasses(options.intent);
|
|
135
170
|
if (intent.length) {
|
|
@@ -168,6 +203,7 @@ status.clear = function clear() {
|
|
|
168
203
|
target.textContent = "";
|
|
169
204
|
clearIntentClasses(target);
|
|
170
205
|
releaseViewport();
|
|
206
|
+
restoreStatusTarget(target);
|
|
171
207
|
});
|
|
172
208
|
};
|
|
173
209
|
|
|
@@ -198,7 +234,7 @@ function connectStatusTrigger(trigger, target) {
|
|
|
198
234
|
if (typeof document !== "undefined") {
|
|
199
235
|
document.addEventListener(EVENTS.invalid, (event) => {
|
|
200
236
|
const message = event.detail?.message;
|
|
201
|
-
if (message) status(message, { intent: "danger" });
|
|
237
|
+
if (message) status(message, { intent: "danger", source: event.target });
|
|
202
238
|
});
|
|
203
239
|
|
|
204
240
|
// Public contract: any code can trigger the status bar this way, without
|
|
@@ -209,7 +245,7 @@ if (typeof document !== "undefined") {
|
|
|
209
245
|
// paths end in the same exit, so a detail that lost its message never
|
|
210
246
|
// strands a stale one on screen.
|
|
211
247
|
if (message != null) {
|
|
212
|
-
status(message, { intent, duration });
|
|
248
|
+
status(message, { intent, duration, source: event.target });
|
|
213
249
|
} else {
|
|
214
250
|
status.clear();
|
|
215
251
|
}
|
package/src/js/surface.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { autoUpdate, reposition, repositionAt } from "@lekoala/floating";
|
|
1
2
|
import enhance from "./enhance.js";
|
|
2
3
|
import { registerEscapeDismissal } from "./escape.js";
|
|
3
4
|
import { EVENTS } from "./events.js";
|
|
4
|
-
import { autoUpdate, reposition, repositionAt } from "./floating.js";
|
|
5
5
|
|
|
6
6
|
import { CLASSES } from "./selectors.js";
|
|
7
7
|
import { waitForTransitions } from "./transition.js";
|
|
@@ -210,6 +210,10 @@ function startSurfaceResources(menu, state) {
|
|
|
210
210
|
const armKey = (event) => {
|
|
211
211
|
if (!event.defaultPrevented && SCROLL_KEYS.has(event.key)) arm(event);
|
|
212
212
|
};
|
|
213
|
+
const dismissOnScroll = (event) => {
|
|
214
|
+
if (state.scrollIntentAt == null || menu.contains(event.target)) return;
|
|
215
|
+
closeSurface(menu);
|
|
216
|
+
};
|
|
213
217
|
const captureOptions = { signal: controller.signal, capture: true };
|
|
214
218
|
const passiveCaptureOptions = { ...captureOptions, passive: true };
|
|
215
219
|
const doc = menu.ownerDocument;
|
|
@@ -217,20 +221,13 @@ function startSurfaceResources(menu, state) {
|
|
|
217
221
|
doc.addEventListener("touchmove", arm, passiveCaptureOptions);
|
|
218
222
|
doc.addEventListener("wheel", arm, passiveCaptureOptions);
|
|
219
223
|
doc.addEventListener("keydown", armKey, { signal: controller.signal });
|
|
224
|
+
doc.addEventListener("scroll", dismissOnScroll, passiveCaptureOptions);
|
|
220
225
|
state.stopScrollIntent = () => controller.abort();
|
|
221
226
|
}
|
|
222
|
-
|
|
227
|
+
const anchor = state.point ? null : state.trigger || state.source;
|
|
228
|
+
state.stopTracking = autoUpdate(anchor, menu, ({ type }) => {
|
|
223
229
|
if (menu.hidden) return;
|
|
224
|
-
if (
|
|
225
|
-
type === "scroll" &&
|
|
226
|
-
state.dismissOnScroll &&
|
|
227
|
-
state.scrollIntentAt != null &&
|
|
228
|
-
timeStamp >= state.scrollIntentAt &&
|
|
229
|
-
[...targets].some((target) => target !== menu && !menu.contains(target))
|
|
230
|
-
) {
|
|
231
|
-
closeSurface(menu);
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
230
|
+
if (type === "scroll" && state.dismissOnScroll) return;
|
|
234
231
|
applyPresentation(menu, state);
|
|
235
232
|
if (!positionSurface(menu)) closeSurface(menu);
|
|
236
233
|
});
|
|
@@ -249,11 +246,30 @@ export function prepareSurface(menu) {
|
|
|
249
246
|
reaperFor(menu)?.refresh(menu);
|
|
250
247
|
}
|
|
251
248
|
|
|
249
|
+
/*
|
|
250
|
+
* One lifecycle owner per surface. surface.js opens by clearing [hidden] and
|
|
251
|
+
* adding .is-open; it never calls showPopover(). So a [popover] panel handed to
|
|
252
|
+
* the runtime has two owners and stays shut: the platform keeps a popover it
|
|
253
|
+
* was never asked to show closed, whatever the runtime writes.
|
|
254
|
+
*
|
|
255
|
+
* This covers every popover value. "manual" is a candidate future transport for
|
|
256
|
+
* this module, which does not make it a legitimate mix today. Delete this guard
|
|
257
|
+
* in the commit that makes surface.js call showPopover().
|
|
258
|
+
*/
|
|
259
|
+
function warnPopoverConflict(panel) {
|
|
260
|
+
if (!panel.hasAttribute("popover")) return;
|
|
261
|
+
console.warn(
|
|
262
|
+
"Actual: the surface lifecycle cannot own a [popover] element — choose one lifecycle owner (remove the popover attribute, or drop the Actual enhancement).",
|
|
263
|
+
panel,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
252
267
|
export function retainSurface(panel) {
|
|
253
268
|
if (!panel) return () => {};
|
|
254
269
|
|
|
255
270
|
let entry = surfaceRetainers.get(panel);
|
|
256
271
|
if (!entry) {
|
|
272
|
+
warnPopoverConflict(panel);
|
|
257
273
|
prepareSurface(panel);
|
|
258
274
|
entry = { count: 0 };
|
|
259
275
|
surfaceRetainers.set(panel, entry);
|
package/src/js/tooltip.js
CHANGED
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
* Hide: blur, pointer leave, Escape
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
+
import { autoUpdate, reposition } from "@lekoala/floating";
|
|
30
31
|
import enhance from "./enhance.js";
|
|
31
32
|
import { registerEscapeDismissal } from "./escape.js";
|
|
32
|
-
import { autoUpdate, reposition } from "./floating.js";
|
|
33
33
|
import { CLASSES } from "./selectors.js";
|
|
34
34
|
|
|
35
35
|
const SHOW_DELAY_MS = 150;
|
|
@@ -163,7 +163,7 @@ function updateTrackedTip(tip, state) {
|
|
|
163
163
|
function startTracking(tip, state) {
|
|
164
164
|
if (state.stopTracking) return;
|
|
165
165
|
|
|
166
|
-
state.stopTracking = autoUpdate(tip, () => updateTrackedTip(tip, state));
|
|
166
|
+
state.stopTracking = autoUpdate(state.activeRef, tip, () => updateTrackedTip(tip, state));
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
function stopTracking(state) {
|