actual-css 0.7.0 → 0.9.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 +217 -0
- package/README.md +13 -11
- package/components.json +668 -0
- package/dist/actual.css +166 -37
- package/dist/actual.full.css +1087 -643
- package/dist/actual.full.js +1 -1
- package/dist/actual.full.min.css +1 -1
- package/dist/actual.js +1 -1
- package/dist/actual.min.css +1 -1
- package/docs/design-notes/accessibility-decisions.md +33 -0
- package/docs/design-notes/browser-support.md +62 -30
- package/docs/design-notes/column-layout.md +3 -3
- package/docs/design-notes/css-anchor.md +75 -21
- package/docs/design-notes/enhancement-contract.md +14 -6
- package/docs/design-notes/grid.md +4 -4
- package/docs/design-notes/naming.md +6 -1
- package/docs/design-notes/platform-alignment.md +53 -7
- package/docs/design-notes/popover-manual-poc.md +105 -25
- package/docs/design-notes/surface-reparenting.md +56 -47
- package/docs/design-notes/tooltip-coordinate-space.md +132 -0
- package/docs/design-notes/widget-primitives.md +7 -4
- package/docs/pages/components/accordion.md +41 -2
- package/docs/pages/components/alert.md +85 -30
- package/docs/pages/components/app-navigation.md +6 -0
- package/docs/pages/components/avatar.md +6 -6
- package/docs/pages/components/badge.md +101 -16
- package/docs/pages/components/button.md +16 -8
- package/docs/pages/components/card.md +10 -10
- package/docs/pages/components/dialog.md +21 -6
- package/docs/pages/components/drawer.md +13 -0
- package/docs/pages/components/fab.md +4 -3
- package/docs/pages/components/list.md +42 -4
- package/docs/pages/components/navbar.md +17 -1
- package/docs/pages/components/pagination.md +2 -2
- package/docs/pages/components/skeleton.md +3 -1
- package/docs/pages/components/steps.md +19 -0
- package/docs/pages/components/table.md +3 -2
- package/docs/pages/enhancements/flyout.md +82 -38
- package/docs/pages/enhancements/overview.md +25 -0
- package/docs/pages/enhancements/status-bar.md +1 -1
- package/docs/pages/enhancements/tabs.md +45 -37
- package/docs/pages/enhancements/tooltip.md +36 -10
- package/docs/pages/examples/overview.md +38 -5
- package/docs/pages/forms/choice-cards.md +11 -6
- package/docs/pages/forms/combobox.md +1 -1
- package/docs/pages/forms/floating-field.md +4 -3
- package/docs/pages/forms/joined-controls.md +22 -0
- package/docs/pages/forms/otp.md +2 -1
- package/docs/pages/forms/overview.md +1 -1
- package/docs/pages/forms/states-and-sizes.md +16 -18
- package/docs/pages/forms/sticky-actions.md +2 -2
- package/docs/pages/forms/switch.md +1 -1
- package/docs/pages/foundations/philosophy.md +57 -26
- package/docs/pages/foundations/theming.md +2 -2
- package/docs/pages/foundations/tokens.md +118 -21
- package/docs/pages/foundations/typography.md +35 -14
- package/docs/pages/guides/introduction.md +11 -9
- package/docs/pages/guides/progressive-enhancement.md +115 -30
- package/docs/pages/guides/tailwind.md +3 -2
- package/docs/pages/layout/media.md +1 -1
- package/docs/pages/layout/overview.md +11 -1
- package/docs/pages/layout/scroll-snap.md +3 -3
- package/docs/pages/layout/stack.md +17 -9
- package/docs/pages/patterns/icon-input.md +4 -2
- package/docs/pages/patterns/icon-slot.md +2 -0
- package/docs/pages/patterns/intent-nesting.md +1 -1
- package/docs/pages/utilities/overview.md +23 -2
- package/docs/pages/utilities/sizing-wrapping.md +1 -1
- package/docs/pages/utilities/spacing.md +10 -8
- package/llms.txt +2 -1
- package/package.json +159 -149
- package/{scripts/reserved-classes.json → reserved-classes.json} +4 -6
- package/src/css/components/accordion.css +42 -3
- package/src/css/components/alert.css +25 -13
- package/src/css/components/app-nav.css +18 -8
- package/src/css/components/avatar.css +13 -1
- package/src/css/components/badge.css +78 -12
- package/src/css/components/breadcrumb.css +2 -3
- package/src/css/components/button.css +9 -16
- package/src/css/components/card.css +9 -2
- package/src/css/components/chat.css +1 -0
- package/src/css/components/data-list.css +1 -1
- package/src/css/components/drawer.css +101 -32
- package/src/css/components/fab.css +0 -6
- package/src/css/components/flyout.css +35 -98
- package/src/css/components/index.css +0 -1
- package/src/css/components/indicator.css +21 -0
- package/src/css/components/key.css +10 -0
- package/src/css/components/list.css +40 -4
- package/src/css/components/menu.css +2 -1
- package/src/css/components/modal.css +48 -33
- package/src/css/components/navbar.css +37 -4
- package/src/css/components/pagination.css +1 -0
- package/src/css/components/skeleton.css +15 -4
- package/src/css/components/spinner.css +6 -6
- package/src/css/components/status-bar.css +0 -5
- package/src/css/components/steps.css +45 -59
- package/src/css/components/tab.css +29 -9
- package/src/css/components/table.css +13 -5
- package/src/css/components/tooltip.css +36 -40
- package/src/css/core/base.css +23 -1
- package/src/css/core/intents.css +17 -6
- package/src/css/core/theme.css +27 -0
- package/src/css/core/tokens.css +35 -15
- package/src/css/core/variants.css +64 -15
- package/src/css/forms/choice-card.css +4 -4
- package/src/css/forms/choice.css +76 -29
- package/src/css/forms/control.css +2 -1
- package/src/css/forms/floating-field.css +3 -3
- package/src/css/forms/form.css +4 -3
- package/src/css/forms/input-icon.css +20 -14
- package/src/css/forms/native.css +6 -1
- package/src/css/forms/otp.css +9 -1
- package/src/css/forms/range.css +2 -0
- package/src/css/forms/select.css +29 -9
- package/src/css/forms/switch.css +28 -8
- package/src/css/layout/app-layout.css +1 -1
- package/src/css/layout/grid.css +50 -76
- package/src/css/layout/scroll-snap.css +11 -7
- package/src/css/layout/scroller.css +3 -4
- package/src/css/layout/stack.css +2 -2
- package/src/css/typography/fluid.css +4 -3
- package/src/css/typography/prose.css +6 -5
- package/src/css/utilities/base.css +2 -1
- package/src/css/utilities/extra.css +20 -17
- package/src/js/context-menu.js +27 -26
- package/src/js/dialog.js +2 -1
- package/src/js/dismiss.js +5 -0
- package/src/js/enhance.js +34 -2
- package/src/js/enhancement-loader.js +36 -43
- package/src/js/events.js +0 -3
- package/src/js/filter.js +0 -1
- package/src/js/floating-space.js +42 -0
- package/src/js/flyout.js +9 -44
- package/src/js/focus-group.js +7 -0
- package/src/js/focus.js +6 -0
- package/src/js/menu.js +10 -1
- package/src/js/selectors.js +5 -9
- package/src/js/status.js +6 -5
- package/src/js/surface.js +97 -139
- package/src/js/tab.js +6 -1
- package/src/js/tooltip.js +150 -51
- package/src/js/validation.js +1 -1
- package/src/css/components/surface.css +0 -49
|
@@ -7,9 +7,10 @@ negative, which are the ones a future attempt would otherwise repeat.
|
|
|
7
7
|
Question asked: can `popover="manual"` replace the DOM reparenting in
|
|
8
8
|
`surface.js` without changing anything else?
|
|
9
9
|
|
|
10
|
-
Answer: yes
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
Answer: yes. The branch demonstrated it, the WebKit gate below closed it, and
|
|
11
|
+
the migration checklist at the end has been executed — so this note is now also
|
|
12
|
+
the record of what the checklist got wrong. The cost was a browser floor, not
|
|
13
|
+
an architecture change; see [browser-support](browser-support.md).
|
|
13
14
|
|
|
14
15
|
## Branch
|
|
15
16
|
|
|
@@ -138,14 +139,13 @@ pass, it is no longer a transport swap and the claim does not hold.
|
|
|
138
139
|
|
|
139
140
|
## Go / no-go: WebKit verification
|
|
140
141
|
|
|
141
|
-
|
|
142
|
+
The measurements above were taken on Chromium — the browser test harness
|
|
142
143
|
hardcodes `backend: "chrome"`. The floor's most notable step is Safari 17, so
|
|
143
|
-
the transport
|
|
144
|
-
|
|
145
|
-
checklist below that is not already demonstrated.
|
|
144
|
+
the transport needed evidence on the engine the raise actually buys. That was
|
|
145
|
+
the one open technical gate on the migration. It is now closed.
|
|
146
146
|
|
|
147
|
-
A permanent WebKit matrix
|
|
148
|
-
manual pass on a real Safari at the chosen floor
|
|
147
|
+
A permanent WebKit matrix was disproportionate for a decision taken once. One
|
|
148
|
+
manual pass on a real Safari at the chosen floor:
|
|
149
149
|
|
|
150
150
|
```text
|
|
151
151
|
1. showPopover() / hidePopover() in manual mode
|
|
@@ -158,24 +158,66 @@ manual pass on a real Safari at the chosen floor, recorded here, is enough:
|
|
|
158
158
|
8. the sheet scrim covers and intercepts the click
|
|
159
159
|
```
|
|
160
160
|
|
|
161
|
-
|
|
161
|
+
`demo/templates/popover-transport.html` is that pass, scored by the page. It
|
|
162
|
+
loads no Actual JavaScript and measures the platform subset above, so it
|
|
163
|
+
answers on `master` without checking the branch out. Scenarios 1–6 run
|
|
164
|
+
unattended; 7 and 8 need a real gesture. It scores 8/8 on Chromium and
|
|
165
|
+
reproduces the findings above, `::backdrop` included — so a deviation on WebKit
|
|
166
|
+
is the engine, not the bench.
|
|
167
|
+
|
|
168
|
+
### Result — gate passed
|
|
162
169
|
|
|
163
170
|
```text
|
|
164
171
|
Verified manually on:
|
|
165
|
-
Safari
|
|
166
|
-
|
|
167
|
-
Date:
|
|
168
|
-
Scenarios: _/8 passed
|
|
172
|
+
iOS Safari 17.0 / iOS 17.0 8/8 passed
|
|
173
|
+
Safari 17.3 / macOS unrecorded 8/8 passed
|
|
174
|
+
Date: 2026-09-04
|
|
169
175
|
```
|
|
170
176
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
177
|
+
iOS 17.0 is the bottom of the proposed floor, measured rather than inferred.
|
|
178
|
+
The desktop score is the union of two page loads on one engine, the iOS score a
|
|
179
|
+
single session at 390×745; a second iOS pass at 341×651 reproduced everything
|
|
180
|
+
but scenario 7, whose assertion the bench has since corrected — see below.
|
|
181
|
+
Safari freezes its macOS UA token at `10_15_7`, so the desktop host version is
|
|
182
|
+
not recoverable from a report. macOS Safari 17.0–17.2 stay inferred, on the
|
|
183
|
+
same WebKit that iOS 17.0 exercised.
|
|
184
|
+
|
|
185
|
+
Scenario 7 ran through the synthetic fallback on both iOS passes: iOS fires no
|
|
186
|
+
`contextmenu` from a long press, so that event path is unexercised on the port
|
|
187
|
+
where it matters most, and the scenario attests placement only.
|
|
188
|
+
|
|
189
|
+
Both ports report the same subset: the four runtime primitives and
|
|
190
|
+
`selector(:popover-open)` present; `checkVisibility()`, `overlay`,
|
|
191
|
+
`transition-behavior: allow-discrete` and `@starting-style` absent. That
|
|
192
|
+
matches WebKit's published timeline — Popover in 17.0, the rest in 17.4 and
|
|
193
|
+
17.5 — which is corroboration that the bench measures rather than guesses.
|
|
194
|
+
|
|
195
|
+
Four results carry consequences:
|
|
196
|
+
|
|
197
|
+
**Scenario 5 passes on both ports**, which is what makes the transport worth
|
|
198
|
+
making mandatory: the native top layer solves on WebKit the problem that
|
|
199
|
+
motivated `getSurfaceRoot()` in the first place, on a 390px phone included.
|
|
200
|
+
|
|
201
|
+
**Scenario 2 passes on the fallback branch.** `checkVisibility()` does not
|
|
202
|
+
exist below Safari 17.4, so `isElementVisible()` runs its `getClientRects()`
|
|
203
|
+
branch at the floor — the branch that had never been exercised. It works, and
|
|
204
|
+
it is load-bearing rather than dead.
|
|
205
|
+
|
|
206
|
+
**`::backdrop` is weaker on WebKit, not stronger.** Author `pointer-events` on
|
|
207
|
+
an open popover's backdrop still computes to `none`, the native backdrop still
|
|
208
|
+
lets the pointer through, and an Actual scrim element still absorbs it,
|
|
209
|
+
confirmed with a real tap. The one thing `::backdrop` did well on Chromium —
|
|
210
|
+
resolving a custom property scoped to an ancestor of the panel — resolves to
|
|
211
|
+
nothing on WebKit. The scrim stays an Actual element on both engines, and not
|
|
212
|
+
by Chromium's accident.
|
|
213
|
+
|
|
214
|
+
**An overflowing top-layer box is adjusted differently.** Asked to place a
|
|
215
|
+
panel at a point that would push it past the right edge of a 341px viewport,
|
|
216
|
+
iOS Safari 17.0 offset it by 11px; Chromium at the identical viewport and
|
|
217
|
+
overflow offset it by 0. Not a gate: `surface.js` positions through floating's
|
|
218
|
+
`shift` middleware with `shiftPadding: 4`, so it never asks for such a point
|
|
219
|
+
and measures the rendered rect afterwards. The bench now picks a point that
|
|
220
|
+
fits, so scenario 7 measures placement rather than the UA's correction.
|
|
179
221
|
|
|
180
222
|
## Changelog framing for the raise
|
|
181
223
|
|
|
@@ -202,7 +244,7 @@ For the release that raises Minimal. Items 4–7 are scaffolding built for a flo
|
|
|
202
244
|
that will no longer exist; a gate outliving its reason is silent dead weight, so
|
|
203
245
|
they belong here rather than being found later.
|
|
204
246
|
|
|
205
|
-
1.
|
|
247
|
+
1. WebKit verification — done, 8/8 on Safari 17.3, recorded above. Cleared.
|
|
206
248
|
2. Raise Minimal to the required subset above. The justification is the defect
|
|
207
249
|
and the transparent migration, not a download count: npm figures conflate
|
|
208
250
|
CI, bots, caches and the author, and say nothing about the end browser.
|
|
@@ -213,11 +255,16 @@ they belong here rather than being found later.
|
|
|
213
255
|
`scrollbar-gutter`, `scrollbar-width`, `scrollbar-color`) as Minimal
|
|
214
256
|
baseline and drop their `check-compat` entries.
|
|
215
257
|
5. Remove the `@supports selector(:popover-open)` gates in `flyout.css` and
|
|
216
|
-
`tooltip.css`.
|
|
258
|
+
`tooltip.css`. Those only: the `@supports (transition-behavior:
|
|
259
|
+
allow-discrete)` gate and the `@starting-style` blocks stay, since neither
|
|
260
|
+
is in the floor — Safari 17.4 and 17.5.
|
|
217
261
|
6. Remove the two `popover` entries from `check-compat`'s feature table.
|
|
218
262
|
7. Remove the associated `compat-ok:` pragmas, and `hasPragma()` itself if
|
|
219
263
|
nothing else consumes it.
|
|
220
|
-
8. Move `surface.js` to the manual transport, with no fallback branch.
|
|
264
|
+
8. Move `surface.js` to the manual transport, with no fallback branch. This
|
|
265
|
+
does not reach `isElementVisible()` in `focus.js`: `checkVisibility()` is
|
|
266
|
+
Safari 17.4, so its `getClientRects()` fallback is the branch that runs at
|
|
267
|
+
the floor and must stay.
|
|
221
268
|
9. Drop `test.failing` from the reparenting tests in
|
|
222
269
|
`tests/browser/surface-inherited-context.test.js`, keeping the assertions.
|
|
223
270
|
10. Update [surface-reparenting](surface-reparenting.md) from a known limitation
|
|
@@ -229,6 +276,39 @@ the transport changes — but nothing forces step 9. The test file points at the
|
|
|
229
276
|
note, and the note points back here; that is as far as documentation can close
|
|
230
277
|
the loop.
|
|
231
278
|
|
|
279
|
+
### Executed — where the checklist was wrong
|
|
280
|
+
|
|
281
|
+
All ten items are done. Three needed correcting on contact, and the corrections
|
|
282
|
+
are worth more than the items were.
|
|
283
|
+
|
|
284
|
+
**Item 4 rested on a false premise.** Raising Minimal does not make a CSS
|
|
285
|
+
capability baseline, because Minimal is the *JavaScript* floor: a Degraded
|
|
286
|
+
browser runs no supported runtime and still receives every stylesheet, so CSS
|
|
287
|
+
owes its fallbacks that far down. `check:compat` now states the Degraded floor
|
|
288
|
+
and keeps the entries, relabelled — dropping them would have removed the only
|
|
289
|
+
machine check that keeps those fallbacks honest. Two of the six features were
|
|
290
|
+
also mistagged before the migration: `scrollbar-gutter`, `scrollbar-width` and
|
|
291
|
+
`scrollbar-color` land in Safari 18.2, above every tier including Recommended,
|
|
292
|
+
so they are `optional` rather than absorbed. The same relabelling moved `:has()`
|
|
293
|
+
and `@container` to `minimal` — the new floor genuinely does absorb them, which
|
|
294
|
+
the checklist missed because it enumerated the Intermediate tier's features
|
|
295
|
+
instead of reading the new floor.
|
|
296
|
+
|
|
297
|
+
**Item 7 cost more than it removed.** `hasPragma()` had no consumer left once
|
|
298
|
+
the popover entries went, so it is gone — and with it five tests, including the
|
|
299
|
+
one recording that prose must not excuse an optional structural use. The
|
|
300
|
+
discovery is now a comment in `check-compat.js` naming the pragma to reinstate.
|
|
301
|
+
Deleting a guard whose only proof of value was its own test is the trade this
|
|
302
|
+
item asked for; it should be re-read before the next optional structural
|
|
303
|
+
capability arrives.
|
|
304
|
+
|
|
305
|
+
**Item 8 left one author-facing decision open.** The POC preserved an author's
|
|
306
|
+
own `popover` value and only defaulted a missing one. `prepareSurface()` now
|
|
307
|
+
writes `manual` unconditionally: any other mode hands the UA a dismissal policy,
|
|
308
|
+
and native light dismiss closes on outside clicks only — which silently defeats
|
|
309
|
+
`data-flyout-auto-close="false"`. The old `[popover]` conflict warning had
|
|
310
|
+
caught that case; overwriting the attribute is what replaces it.
|
|
311
|
+
|
|
232
312
|
## What was deliberately left out
|
|
233
313
|
|
|
234
314
|
Not attempted, and not because it looked hard:
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
# Surface reparenting
|
|
1
|
+
# Surface reparenting broke inherited presentation context
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Resolved. The record is kept because the defect had no generic repair, three of
|
|
4
|
+
the obvious ones look right and are wrong, and the transport that fixed it is
|
|
5
|
+
the reason the JavaScript floor sits where it does.
|
|
6
6
|
|
|
7
|
-
## What
|
|
7
|
+
## What used to happen
|
|
8
8
|
|
|
9
|
-
`surface.js`
|
|
9
|
+
`surface.js` moved an open surface out of its place in the document:
|
|
10
10
|
|
|
11
11
|
```text
|
|
12
12
|
getSurfaceRoot() → nearest <dialog>, else body
|
|
@@ -14,15 +14,15 @@ mountSurface() → root.append(panel)
|
|
|
14
14
|
restoreSurface() → put it back once the exit transition ends
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
The move
|
|
18
|
-
ancestor and
|
|
19
|
-
every scope that reached it *by inheritance*. The panel
|
|
20
|
-
ancestors while
|
|
17
|
+
The move existed for good reasons — it escaped `overflow` clipping on any
|
|
18
|
+
ancestor and sidestepped `z-index` stacking. But it also took the panel out of
|
|
19
|
+
every scope that reached it *by inheritance*. The panel was styled by one set of
|
|
20
|
+
ancestors while closed, and by a different set from the moment it became
|
|
21
21
|
visible.
|
|
22
22
|
|
|
23
23
|
Actual leans on inheritance deliberately: theme islands, density, `.inverted`,
|
|
24
24
|
and the custom properties an application scopes to its own containers all
|
|
25
|
-
travel that way. So this
|
|
25
|
+
travel that way. So this was not one bug about themes. It was the same bug in as
|
|
26
26
|
many forms as there are inherited contexts.
|
|
27
27
|
|
|
28
28
|
## Measured
|
|
@@ -38,23 +38,24 @@ click that opens it:
|
|
|
38
38
|
| app-scoped `--app-accent` | `rgb(1, 2, 3)` | *(unset)* |
|
|
39
39
|
| `.sm` inside a `<dialog>` | `--control-size: 2rem` | `2.375rem` |
|
|
40
40
|
|
|
41
|
-
Every context
|
|
41
|
+
Every context broke. Three consequences were worth stating plainly:
|
|
42
42
|
|
|
43
|
-
- The theme case
|
|
44
|
-
|
|
45
|
-
- The density case
|
|
46
|
-
scope asked for.
|
|
47
|
-
- The dialog case
|
|
48
|
-
panel
|
|
43
|
+
- The theme case was visible, not theoretical: a flyout inside a dark island
|
|
44
|
+
turned light at the moment it opened.
|
|
45
|
+
- The density case was measurable: menu rows rendered 38px instead of the 32px
|
|
46
|
+
the scope asked for.
|
|
47
|
+
- The dialog case showed the `getSurfaceRoot()` special case did not help. The
|
|
48
|
+
panel did land inside the dialog, and still lost any scope sitting between
|
|
49
49
|
that dialog and its trigger. Choosing a nearer root loses less context; it
|
|
50
50
|
does not preserve it.
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
transport stops reparenting.
|
|
52
|
+
`tests/browser/surface-inherited-context.test.js` holds those assertions and
|
|
53
|
+
now passes against the shipped transport.
|
|
55
54
|
|
|
56
55
|
## Why the obvious repairs are wrong
|
|
57
56
|
|
|
57
|
+
Still wrong, and still tempting — this section is the reason the note is kept.
|
|
58
|
+
|
|
58
59
|
**Copying `data-theme` onto the moved panel.** Fixes the one symptom that was
|
|
59
60
|
noticed and leaves the other four broken. It also turns an inherited contract
|
|
60
61
|
into an enumerated one, so every future scope has to be added to a list.
|
|
@@ -67,13 +68,13 @@ would also freeze values that should stay live.
|
|
|
67
68
|
**Mounting to a nearer root.** The dialog row above is the experiment: it
|
|
68
69
|
reduces the damage without addressing the cause.
|
|
69
70
|
|
|
70
|
-
The cause
|
|
71
|
+
The cause was that the panel was moved at all. The repair is a transport that
|
|
71
72
|
promotes the element without relocating it.
|
|
72
73
|
|
|
73
|
-
## The
|
|
74
|
+
## The repair, as shipped
|
|
74
75
|
|
|
75
76
|
A native popover renders in the top layer while staying exactly where the
|
|
76
|
-
author put it in the DOM. That
|
|
77
|
+
author put it in the DOM. That removed the reason to reparent, and with it the
|
|
77
78
|
whole mechanism:
|
|
78
79
|
|
|
79
80
|
```text
|
|
@@ -86,37 +87,45 @@ restoreSurface()
|
|
|
86
87
|
`popover="manual"` is the mode that fits: it supplies the top layer and nothing
|
|
87
88
|
else, so `surface.js` keeps its dismissal policy, Escape ordering, focus
|
|
88
89
|
restoration and placement orchestration unchanged. See
|
|
89
|
-
[platform-alignment](platform-alignment.md) for the responsibility split and
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
[platform-alignment](platform-alignment.md) for the responsibility split, and
|
|
91
|
+
[browser-support](browser-support.md) for the floor it requires.
|
|
92
|
+
|
|
93
|
+
Adopting it behind a capability branch was rejected rather than overlooked: the
|
|
94
|
+
fallback path *is* the defect, so a branch would have kept the bug alive for
|
|
95
|
+
exactly the browsers it was written to serve. Raising the floor was the cheaper
|
|
96
|
+
half of the decision.
|
|
92
97
|
|
|
93
|
-
|
|
94
|
-
available there today, and adopting it behind a capability branch would keep
|
|
95
|
-
the reparenting code alive for the fallback path — so the defect would remain
|
|
96
|
-
for exactly the browsers the branch was written to serve.
|
|
98
|
+
## The contract now
|
|
97
99
|
|
|
98
|
-
|
|
100
|
+
> A surface managed by `surface.js` is presented in its trigger's context. A
|
|
101
|
+
> scoped theme, density, or custom property reaches an anchored surface through
|
|
102
|
+
> inheritance, open or closed.
|
|
99
103
|
|
|
100
|
-
The
|
|
104
|
+
The two workarounds this note used to recommend — declaring the scope on `body`
|
|
105
|
+
or on the panel itself — are no longer needed and can be removed from
|
|
106
|
+
application code.
|
|
101
107
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
108
|
+
`tooltip.js` reached the same contract later, and for the same reason: it moved
|
|
109
|
+
an explicit tooltip to the dialog or the body on first hover, which is this
|
|
110
|
+
defect in a second component. An explicit tooltip now stays where it was
|
|
111
|
+
written and inherits accordingly.
|
|
105
112
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
113
|
+
A generated tooltip (`data-tooltip="text"`) is deliberately outside the
|
|
114
|
+
contract. Actual creates that element, so Actual places it, and it goes to
|
|
115
|
+
document level rather than beside the trigger — a generated sibling would take
|
|
116
|
+
`.join > :last-child` from a trigger that ends a group, since structural
|
|
117
|
+
pseudo-classes are DOM-based and `position: fixed` does not exempt an element
|
|
118
|
+
from them. The shorthand therefore promises no inheritance; the explicit form
|
|
119
|
+
is the one to use when local context matters.
|
|
110
120
|
|
|
111
121
|
## Experiment
|
|
112
122
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
123
|
+
[popover-manual-poc](popover-manual-poc.md) records what the branch measured,
|
|
124
|
+
what it refuted, and the checklist the migration followed. Read that before
|
|
125
|
+
touching the transport again — it holds the negative results, `::backdrop`
|
|
126
|
+
included.
|
|
117
127
|
|
|
118
128
|
## Review trigger
|
|
119
129
|
|
|
120
|
-
Revisit
|
|
121
|
-
|
|
122
|
-
becomes available by another route.
|
|
130
|
+
Revisit if a further inherited context is found to break, or if a transport
|
|
131
|
+
that promotes without the top layer becomes available by another route.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Tooltip coordinate space, and what an unpositionable tip means
|
|
2
|
+
|
|
3
|
+
Two changes to `tooltip.js`, both consequences of one question: what a floating
|
|
4
|
+
element's coordinates are relative to, and what it means when there are none.
|
|
5
|
+
|
|
6
|
+
## The lag
|
|
7
|
+
|
|
8
|
+
Scrolling is asynchronous. The compositor can move the page before any script
|
|
9
|
+
runs, so a `position: fixed` element written from JavaScript is always one frame
|
|
10
|
+
behind the content it is attached to. `autoUpdate()` coalesces to a frame by
|
|
11
|
+
design, which is correct for the work and still visible as a wobble on touch
|
|
12
|
+
devices, where scrolling is driven off the main thread.
|
|
13
|
+
|
|
14
|
+
`@lekoala/floating` 0.2.0 adds `coordinateSpace: "document"`: the same viewport
|
|
15
|
+
measurement, written as `left`/`top` plus the page scroll, for an absolutely
|
|
16
|
+
positioned box whose containing block is the initial one. The browser then
|
|
17
|
+
carries the element with the page and there is nothing to correct.
|
|
18
|
+
|
|
19
|
+
Two candidates were rejected before this one:
|
|
20
|
+
|
|
21
|
+
- **Popper's `strategy: "absolute"`.** Relative to a *containing block*, not to
|
|
22
|
+
the reference — which means resolving the offset parent, its borders, its
|
|
23
|
+
scroll offsets, its transforms, and its own nested scrollers. That is the
|
|
24
|
+
machinery `floating` exists without.
|
|
25
|
+
- **Repositioning synchronously on scroll** instead of at the next frame.
|
|
26
|
+
Cheaper to write, and it only shortens the lag; it cannot remove it, because
|
|
27
|
+
the scroll that moved the page never reached the main thread in time.
|
|
28
|
+
|
|
29
|
+
## The containing block is not a problem here
|
|
30
|
+
|
|
31
|
+
Document coordinates assume the initial containing block. A tip is an open
|
|
32
|
+
popover, so it is in the top layer, where an absolutely positioned box resolves
|
|
33
|
+
against the initial containing block whatever its ancestors do. Measured in
|
|
34
|
+
Chrome, writing viewport point `(200, 300)` in document coordinates at
|
|
35
|
+
`scrollY: 640` and reading the resulting rect back:
|
|
36
|
+
|
|
37
|
+
| Ancestor of the tip | Rect |
|
|
38
|
+
| -------------------- | --------- |
|
|
39
|
+
| `position: relative` | `200,300` |
|
|
40
|
+
| `transform` | `200,300` |
|
|
41
|
+
| `filter` | `200,300` |
|
|
42
|
+
| `contain: paint` | `200,300` |
|
|
43
|
+
| `dialog` (`show()`) | `200,300` |
|
|
44
|
+
| `dialog` (modal) | `200,300` |
|
|
45
|
+
|
|
46
|
+
So where the tip lives never enters into it, and the placement contract — an
|
|
47
|
+
explicit tip stays where the author wrote it, a generated one goes to the
|
|
48
|
+
dialog subtree or the body — is untouched.
|
|
49
|
+
|
|
50
|
+
## The trigger is the problem
|
|
51
|
+
|
|
52
|
+
Document coordinates are only right while the *trigger* scrolls with the page.
|
|
53
|
+
Against a trigger that does not, the browser carries the tip away on every
|
|
54
|
+
scroll and the next frame snaps it back: a worse artefact than the lag it
|
|
55
|
+
removes, and a real one — `.fab`, `.status-bar` and `.drawer` are fixed,
|
|
56
|
+
`.navbar`, `.topbar`, `.app-nav` and `.form-actions` are sticky.
|
|
57
|
+
|
|
58
|
+
`isViewportAnchored()` therefore walks up from the trigger for a computed
|
|
59
|
+
`fixed` or `sticky`, and separately asks for `dialog:modal, :popover-open`,
|
|
60
|
+
which the walk cannot see: a modal dialog computes `position: absolute` and is
|
|
61
|
+
laid out against the viewport by the top layer. Sticky counts even while
|
|
62
|
+
unstuck, because nothing in the DOM changes when it becomes stuck.
|
|
63
|
+
|
|
64
|
+
The modal case is the one worth naming, because a modal dialog does **not**
|
|
65
|
+
stop the page from scrolling behind it — measured, wheel included. A trigger
|
|
66
|
+
inside one is as viewport-anchored as a fixed bar, and keeps `position: fixed`.
|
|
67
|
+
|
|
68
|
+
## The scrollable overflow case
|
|
69
|
+
|
|
70
|
+
An absolutely positioned box in the top layer joins the document's scrollable
|
|
71
|
+
overflow; a fixed one never does. The scrollable height becomes the greater of
|
|
72
|
+
the content's own bottom and the tip's, and the threshold is exact: walking a
|
|
73
|
+
20px tip down a 2000px document in Chrome, `scrollHeight` reads 2000 for every
|
|
74
|
+
tip bottom through 2000, and the tip's own bottom from 2001 on.
|
|
75
|
+
|
|
76
|
+
A tip past the end of the content therefore raises the scroll maximum while it
|
|
77
|
+
is up, and hiding it clamps back any position only that tip made reachable —
|
|
78
|
+
the page appears to jump upwards.
|
|
79
|
+
|
|
80
|
+
Placement is what prevents this, not anything in `tooltip.js`. Flip and shift
|
|
81
|
+
put the tip inside the viewport, which at any scroll position is inside the
|
|
82
|
+
document, so only overflow the engine explicitly accepts — a tip it could not
|
|
83
|
+
fit anywhere — reaches past the end of the content. Nor does the demote and
|
|
84
|
+
promote path add a case: the coordinates are a document point, so a tip that
|
|
85
|
+
was inside the document when it went down is still inside it when it comes
|
|
86
|
+
back up, exit fade included.
|
|
87
|
+
|
|
88
|
+
It is a failure mode to test for, then, not a standing cost of document
|
|
89
|
+
coordinates. If the page ever does jump, `documentElement.scrollHeight` across
|
|
90
|
+
a show and a hide separates this from everything else that moves a page: it
|
|
91
|
+
moves with the tip here, and holds steady for anything else.
|
|
92
|
+
|
|
93
|
+
## `false` is a geometry answer
|
|
94
|
+
|
|
95
|
+
`reposition()` returns `false` when it cannot place the element, an anchor
|
|
96
|
+
outside the boundary included. `tooltip.js` used to read that as the end of the
|
|
97
|
+
interaction and call `hideTip()`, which stopped position tracking.
|
|
98
|
+
|
|
99
|
+
On a touch device that loses the tooltip for good: tap a trigger, it takes
|
|
100
|
+
focus and the tip appears; scroll the trigger past the boundary and the tip is
|
|
101
|
+
torn down; scroll back and nothing is watching any more. Tapping again does not
|
|
102
|
+
help either — the trigger is already focused, so no `focusin` fires, and a
|
|
103
|
+
repeat tap on the same element fires no `mouseover`.
|
|
104
|
+
|
|
105
|
+
So the runtime now keeps two states. `wanted` is what the interaction asks for
|
|
106
|
+
and only `hideTip()` clears it; `visible` is what is promoted. An
|
|
107
|
+
unpositionable tip is demoted and stays tracked, and the next tick that can
|
|
108
|
+
place it brings it back up.
|
|
109
|
+
|
|
110
|
+
`floating` was deliberately left out of this: no `{ positioned, reason }`
|
|
111
|
+
return, no option. Its README already documents the pattern — a failed
|
|
112
|
+
positioning may be temporary without `autoUpdate()` being stopped.
|
|
113
|
+
|
|
114
|
+
## Surfaces share geometry, not lifecycle
|
|
115
|
+
|
|
116
|
+
Element-anchored surfaces use the same coupled coordinate and CSS position mode
|
|
117
|
+
as tooltips. Point-positioned surfaces stay fixed because their `x` and `y` are
|
|
118
|
+
client coordinates. A nested scroll container remains a separate boundary:
|
|
119
|
+
document coordinates move natively with page scroll, while container scroll
|
|
120
|
+
still waits for `autoUpdate()` to correct the placement.
|
|
121
|
+
|
|
122
|
+
A surface that fits inside its boundary must not create scrollable overflow
|
|
123
|
+
merely because it uses document coordinates. This is not a guarantee for an
|
|
124
|
+
arbitrarily large custom surface: the positioner deliberately accepts overflow
|
|
125
|
+
when no placement can physically contain it.
|
|
126
|
+
|
|
127
|
+
`surface.js` still closes on `false`, and that stays a separate decision. A
|
|
128
|
+
flyout close restores focus, fires an event and releases an Escape entry;
|
|
129
|
+
hiding one while its trigger is off-boundary would leave a panel the user
|
|
130
|
+
cannot see holding all three. The prerequisite named in
|
|
131
|
+
[css-anchor](css-anchor.md) — "an out-of-view surface hides instead of closing"
|
|
132
|
+
— is a component decision, and it has been taken for tooltips only.
|
|
@@ -9,7 +9,7 @@ The JS runtime doubles as a primitive kit for building custom widgets.
|
|
|
9
9
|
| Discovery, lifecycle, cleanup | `enhance` | `actual-css/js/enhance` |
|
|
10
10
|
| Per-document LIFO Escape dismissal | `escape` | `actual-css/js/escape` |
|
|
11
11
|
| Positioning, flip and shift | `floating` | `@lekoala/floating` |
|
|
12
|
-
| Open/close,
|
|
12
|
+
| Open/close, dismissal, focus | `surface` | `actual-css/js/surface` |
|
|
13
13
|
| Directional item lookup | `keys` | `actual-css/js/keys` |
|
|
14
14
|
| Menu-item vocabulary | `menu` | `actual-css/js/menu` |
|
|
15
15
|
| Focusable lookup, visibility | `focus` | `actual-css/js/focus` |
|
|
@@ -22,7 +22,7 @@ The JS runtime doubles as a primitive kit for building custom widgets.
|
|
|
22
22
|
|
|
23
23
|
The named exports each module carries:
|
|
24
24
|
|
|
25
|
-
- `enhance` — `enhance`, `registerEnhancement`
|
|
25
|
+
- `enhance` — `enhance`, `registerEnhancement`, `applyEnhancement`
|
|
26
26
|
- `escape` — `registerEscapeDismissal`
|
|
27
27
|
- `floating` — `autoUpdate`, `reposition`, `repositionAt`, plus
|
|
28
28
|
`--available-height` and `data-placement`
|
|
@@ -89,12 +89,12 @@ an inline autocomplete that should survive an outside click).
|
|
|
89
89
|
|
|
90
90
|
```
|
|
91
91
|
enhance
|
|
92
|
-
surface (Escape, outside click, focus restore
|
|
92
|
+
surface (Escape, outside click, focus restore)
|
|
93
93
|
floating
|
|
94
94
|
keys
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
-
Less code. Inherits the runtime's dismissal semantics
|
|
97
|
+
Less code. Inherits the runtime's dismissal semantics. Use for
|
|
98
98
|
anything that behaves like a popup.
|
|
99
99
|
|
|
100
100
|
> A `[role="listbox"]` surface with `[role="option"]` children gets no key handling and
|
|
@@ -130,6 +130,9 @@ state to style or ignore.
|
|
|
130
130
|
|
|
131
131
|
## Honest gaps
|
|
132
132
|
|
|
133
|
+
- No nested surfaces — one Actual surface is open per document, and opening one
|
|
134
|
+
closes the rest. A widget that needs a second level owns its own popup
|
|
135
|
+
(composition A above).
|
|
133
136
|
- No typeahead helper — filter, debounce, render are author concerns.
|
|
134
137
|
- No `aria-activedescendant` helper — the author manages the composite-widget pattern.
|
|
135
138
|
- No virtual-list support.
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
- Use native `<details>` and `<summary>` for collapsible content.
|
|
6
6
|
- Use the `name` attribute for exclusive accordions.
|
|
7
7
|
- Supports any valid body element (`div`, `p`, `ul`, ...).
|
|
8
|
-
-
|
|
8
|
+
- Three container treatments: the default common shell, `.flush` (bare separators, for embedding inside an existing surface), and `.separated` (independent items).
|
|
9
|
+
- The toggle marker color is customizable with `--accordion-marker-color`; it reinforces to the summary text color on hover, and the marker icon comes from the shared `--icon-chevron` token.
|
|
9
10
|
|
|
10
11
|
```html demo
|
|
11
12
|
<div class="accordion">
|
|
@@ -34,6 +35,44 @@
|
|
|
34
35
|
</div>
|
|
35
36
|
```
|
|
36
37
|
|
|
38
|
+
## Variants
|
|
39
|
+
|
|
40
|
+
The default builds one raised shell around the whole group. `.flush` and `.separated` only change that chrome — they are integration modes between the component and its container, not decorative skins.
|
|
41
|
+
|
|
42
|
+
- **`flush`** — the ambient box disappears and only the item separators remain, so the group sits edge to edge inside a surface that already exists: a card, a drawer, a page section. A FAQ list without a box inside a box.
|
|
43
|
+
|
|
44
|
+
```html demo
|
|
45
|
+
<div class="accordion flush">
|
|
46
|
+
<details open>
|
|
47
|
+
<summary>Flush keeps just the separators</summary>
|
|
48
|
+
<p>No shell: embed the group in a card, drawer, or page section.</p>
|
|
49
|
+
</details>
|
|
50
|
+
|
|
51
|
+
<details>
|
|
52
|
+
<summary>Edge to edge</summary>
|
|
53
|
+
<p>The summary and panel follow the surrounding surface's own gutter.</p>
|
|
54
|
+
</details>
|
|
55
|
+
</div>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
- **`separated`** — each `<details>` becomes its own card, for groups of standalone items.
|
|
59
|
+
|
|
60
|
+
```html demo
|
|
61
|
+
<div class="accordion separated">
|
|
62
|
+
<details open>
|
|
63
|
+
<summary>Each item is its own card</summary>
|
|
64
|
+
<p>Standalone items share the same gap, border, radius, and raised surface.</p>
|
|
65
|
+
</details>
|
|
66
|
+
|
|
67
|
+
<details>
|
|
68
|
+
<summary>Still native details</summary>
|
|
69
|
+
<p>Exclusive <code>name</code> groups and per-item icons work exactly as in the base component.</p>
|
|
70
|
+
</details>
|
|
71
|
+
</div>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The accordion owns its disclosure marker; the trigger content belongs to the markup. Item accents, per-item shadows, an underlined trigger label, and alternative markers stay application recipes, not framework variants.
|
|
75
|
+
|
|
37
76
|
## Exclusive groups
|
|
38
77
|
|
|
39
78
|
```html demo
|
|
@@ -77,4 +116,4 @@ The end marker is a shared token-based chevron; swapping it for a local icon is
|
|
|
77
116
|
## CSS hooks
|
|
78
117
|
|
|
79
118
|
- `--accordion-radius` — outer corner radius.
|
|
80
|
-
- `--accordion-marker-color` — color of the end marker; forced-colors mode overrides it to `CanvasText`.
|
|
119
|
+
- `--accordion-marker-color` — color of the end marker; it reinforces to the summary text color on hover, and forced-colors mode overrides it to `CanvasText`.
|