actual-css 0.6.0 → 0.8.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 +214 -1
- package/README.md +13 -11
- package/components.json +668 -0
- package/dist/actual.css +166 -37
- package/dist/actual.full.css +1005 -550
- 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 +82 -25
- 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 +99 -2
- package/docs/design-notes/popover-manual-poc.md +328 -0
- package/docs/design-notes/surface-reparenting.md +131 -0
- package/docs/design-notes/tooltip-coordinate-space.md +132 -0
- package/docs/design-notes/widget-primitives.md +9 -6
- package/docs/pages/components/accordion.md +41 -2
- package/docs/pages/components/alert.md +80 -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 +16 -6
- 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/steps.md +19 -0
- package/docs/pages/components/table.md +3 -2
- package/docs/pages/enhancements/flyout.md +144 -24
- package/docs/pages/enhancements/overview.md +25 -0
- package/docs/pages/enhancements/status-bar.md +20 -0
- package/docs/pages/enhancements/tabs.md +45 -37
- package/docs/pages/enhancements/tooltip.md +40 -2
- package/docs/pages/examples/overview.md +38 -5
- package/docs/pages/forms/choice-cards.md +6 -2
- 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 +67 -22
- package/docs/pages/foundations/theming.md +10 -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 +136 -49
- 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 +19 -8
- 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 +79 -25
- package/src/css/components/fab.css +0 -6
- package/src/css/components/flyout.css +46 -63
- package/src/css/components/index.css +0 -1
- package/src/css/components/indicator.css +21 -0
- package/src/css/components/join.css +7 -0
- package/src/css/components/key.css +10 -0
- package/src/css/components/list.css +40 -4
- package/src/css/components/menu.css +18 -11
- 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/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 +5 -5
- package/src/css/components/tooltip.css +45 -13
- 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 +6 -3
- 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 +19 -5
- 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 +44 -7
- package/src/js/surface.js +106 -132
- package/src/js/tab.js +6 -1
- package/src/js/tooltip.js +152 -53
- package/src/js/validation.js +1 -1
- package/src/css/components/surface.css +0 -49
- package/src/js/floating.js +0 -432
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
# POC: native popover as the surface transport
|
|
2
|
+
|
|
3
|
+
A record of an experiment, not a plan of record. It exists so the reasoning
|
|
4
|
+
outlives the branch that produced it — including the parts that came out
|
|
5
|
+
negative, which are the ones a future attempt would otherwise repeat.
|
|
6
|
+
|
|
7
|
+
Question asked: can `popover="manual"` replace the DOM reparenting in
|
|
8
|
+
`surface.js` without changing anything else?
|
|
9
|
+
|
|
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).
|
|
14
|
+
|
|
15
|
+
## Branch
|
|
16
|
+
|
|
17
|
+
`poc/popover-manual-surfaces`, four commits on top of `0f4f8b7`:
|
|
18
|
+
|
|
19
|
+
| SHA | Step |
|
|
20
|
+
| --------- | ------------------------------------------------- |
|
|
21
|
+
| `bf1e53d` | Pass 1 — manual transport, reparenting removed |
|
|
22
|
+
| `4d598dd` | Extract the popover test stub, cut to the minimum |
|
|
23
|
+
| `ececcd6` | Pass 2 — keep a runtime scrim, fix its placement |
|
|
24
|
+
| `27b34f1` | Finish as a transparent migration |
|
|
25
|
+
|
|
26
|
+
## What the transport actually needs
|
|
27
|
+
|
|
28
|
+
Derived from the branch, not from a compatibility badge. The distinction between
|
|
29
|
+
the two contracts matters: they fail differently, and only the first gates the
|
|
30
|
+
runtime.
|
|
31
|
+
|
|
32
|
+
```text
|
|
33
|
+
JS requirement (gates the runtime)
|
|
34
|
+
----------------------------------
|
|
35
|
+
popover="manual"
|
|
36
|
+
showPopover()
|
|
37
|
+
hidePopover()
|
|
38
|
+
top-layer promotion + closed-state display:none
|
|
39
|
+
|
|
40
|
+
CSS enhancement (already @supports-gated, degrades)
|
|
41
|
+
---------------------------------------------------
|
|
42
|
+
:popover-open
|
|
43
|
+
overlay
|
|
44
|
+
transition-behavior: allow-discrete
|
|
45
|
+
@starting-style
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The runtime never reads `:popover-open`. It was removed from `isSurfaceOpen()`
|
|
49
|
+
and from the `autoUpdate` guard because a partially-implemented selector engine
|
|
50
|
+
answers `false` rather than throwing, which is a silent wrong answer (see the
|
|
51
|
+
happy-dom finding below). `.is-open` is the single state the runtime writes and
|
|
52
|
+
reads, and the class the CSS keys on.
|
|
53
|
+
|
|
54
|
+
Two capabilities that are *not* required, which matters because both are newer
|
|
55
|
+
than Popover and would have pushed the floor much higher:
|
|
56
|
+
|
|
57
|
+
- `overlay` / `allow-discrete` — the exit fade. Gated; without it the exit is
|
|
58
|
+
instant and correct.
|
|
59
|
+
- `::backdrop` — proved unusable, see below.
|
|
60
|
+
|
|
61
|
+
## Findings
|
|
62
|
+
|
|
63
|
+
**happy-dom implements no Popover element API.** Version 20.11 has the invoker
|
|
64
|
+
attributes (`popoverTargetElement`, `popoverTargetAction` on button and input)
|
|
65
|
+
but not `showPopover` / `hidePopover` / `togglePopover`, and
|
|
66
|
+
`matches(":popover-open")` answers `false` rather than throwing. The silent
|
|
67
|
+
false is the dangerous half: reading the pseudo-class in `isSurfaceOpen()` made
|
|
68
|
+
all 647 unit tests see "never open". Resolved by reading `.is-open` only, plus
|
|
69
|
+
two no-op stubs in `tests/helpers/popover-stub.js`. Transport behavior moved to
|
|
70
|
+
the browser layer, which is where it belonged.
|
|
71
|
+
|
|
72
|
+
**`::backdrop` cannot replace the sheet scrim.** Negative result, measured, and
|
|
73
|
+
the one most likely to be re-attempted. The native backdrop is painted for a
|
|
74
|
+
manual popover, covers the viewport, and does inherit custom properties from
|
|
75
|
+
its originating element — a scoped `--backdrop-fill` on an ancestor of the
|
|
76
|
+
panel resolved into it, which a body-level div cannot do. But the UA sets
|
|
77
|
+
`pointer-events: none` on an open popover's backdrop and it is not overridable
|
|
78
|
+
from author CSS: both `pointer-events: auto` and `pointer-events: auto
|
|
79
|
+
!important` computed as `none`, since a UA `!important` outranks an author one.
|
|
80
|
+
Measured with a real CDP click on the scrim, the element behind it received the
|
|
81
|
+
click and its handler fired. The scrim has two jobs — painting, and absorbing
|
|
82
|
+
the pointer — and the platform only offers the first, so it stays an Actual
|
|
83
|
+
element.
|
|
84
|
+
|
|
85
|
+
**The scrim's placement, not the scrim, was what pass 1 broke.**
|
|
86
|
+
`menu.before(backdrop)` put it beside a panel that no longer moves, stranding
|
|
87
|
+
it inside the author's container. It goes to the document root instead, which
|
|
88
|
+
reproduces exactly where it landed before. This is the one fragment of
|
|
89
|
+
`getSurfaceRoot()` that survives, and it is not the reparenting the transport
|
|
90
|
+
removed: the scrim is runtime-created, has no author identity, and nothing
|
|
91
|
+
inherited to preserve.
|
|
92
|
+
|
|
93
|
+
**A surface inside a modal dialog works.** This was the open question, since a
|
|
94
|
+
modal dialog inerts the rest of the document — which is why the old transport
|
|
95
|
+
moved the panel into the dialog. Measured: the panel is promoted above the
|
|
96
|
+
dialog, stays in the dialog subtree and in its own host element, wins the hit
|
|
97
|
+
test, and takes focus.
|
|
98
|
+
|
|
99
|
+
**`aria-expanded` is computed natively for a `popovertarget` invoker** —
|
|
100
|
+
`expanded: false` closed, `true` open, on the AX tree. Deliberately not
|
|
101
|
+
exploited: the public contract is `aria-controls`, so `popovertarget` would be
|
|
102
|
+
a markup migration for adopters rather than an internal transport detail, and
|
|
103
|
+
`linkedTriggers()` supports several triggers per panel where `popovertarget`
|
|
104
|
+
covers one.
|
|
105
|
+
|
|
106
|
+
## Measurements
|
|
107
|
+
|
|
108
|
+
| Measure | `master` | POC |
|
|
109
|
+
| ------------------------- | -------: | --------------: |
|
|
110
|
+
| `surface.js` code lines | 358 | **337** |
|
|
111
|
+
| `surface.js` total lines | 431 | 436 |
|
|
112
|
+
| Transport concepts | 4 | **1** |
|
|
113
|
+
| Inherited contexts broken | 3/3 | **0/3** |
|
|
114
|
+
| Unit tests | 647 | **650**, 0 fail |
|
|
115
|
+
| Browser tests | 50 | **55**, 0 fail |
|
|
116
|
+
| JS full, brotli | 16.1 KB | **16.0 KB** |
|
|
117
|
+
| Public contracts changed | — | **none** |
|
|
118
|
+
|
|
119
|
+
Removed: `mountedSurfaces`, `getSurfaceRoot()`, `mountSurface()`,
|
|
120
|
+
`restoreSurface()`, `disconnectSurface`'s `{ restore }` option, and the
|
|
121
|
+
`[popover]` conflict warning — the runtime is that owner now.
|
|
122
|
+
|
|
123
|
+
The total line count *grows* by five. The 21 fewer code lines are outweighed by
|
|
124
|
+
added comments. "Remove more than you add" is a signal, not a rule: what leaves
|
|
125
|
+
is a concept — a component no longer has to leave its place in the DOM to be
|
|
126
|
+
painted above the page — plus a defect with no generic repair and three
|
|
127
|
+
transport abstractions.
|
|
128
|
+
|
|
129
|
+
## Acceptance criterion
|
|
130
|
+
|
|
131
|
+
> A transport migration may change assertions about transport markers, but must
|
|
132
|
+
> not require changing behavioral assertions.
|
|
133
|
+
|
|
134
|
+
This is stricter and more useful than "no public API change". The 57
|
|
135
|
+
`hidden → .is-open` edits qualify: they replace observing a mechanism with
|
|
136
|
+
observing the lifecycle that owns it. If a future attempt has to modify an
|
|
137
|
+
assertion about focus, dismissal, positioning or events to make the transport
|
|
138
|
+
pass, it is no longer a transport swap and the claim does not hold.
|
|
139
|
+
|
|
140
|
+
## Go / no-go: WebKit verification
|
|
141
|
+
|
|
142
|
+
The measurements above were taken on Chromium — the browser test harness
|
|
143
|
+
hardcodes `backend: "chrome"`. The floor's most notable step is Safari 17, so
|
|
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
|
+
|
|
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
|
+
|
|
150
|
+
```text
|
|
151
|
+
1. showPopover() / hidePopover() in manual mode
|
|
152
|
+
2. a closed panel is genuinely invisible and unfocusable
|
|
153
|
+
3. the DOM parent is unchanged before, during and after opening
|
|
154
|
+
4. inherited theme, density and custom properties survive opening
|
|
155
|
+
5. a surface inside <dialog open> and inside a modal dialog
|
|
156
|
+
6. reopening during the exit transition
|
|
157
|
+
7. a context menu at pointer coordinates
|
|
158
|
+
8. the sheet scrim covers and intercepts the click
|
|
159
|
+
```
|
|
160
|
+
|
|
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
|
|
169
|
+
|
|
170
|
+
```text
|
|
171
|
+
Verified manually on:
|
|
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
|
|
175
|
+
```
|
|
176
|
+
|
|
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.
|
|
221
|
+
|
|
222
|
+
## Changelog framing for the raise
|
|
223
|
+
|
|
224
|
+
State what stops working before what it buys, with versions named. Something
|
|
225
|
+
along these lines:
|
|
226
|
+
|
|
227
|
+
> **JavaScript browser floor raised** to Safari 17+, Firefox 125+ and
|
|
228
|
+
> Chromium 116+. Interactive surfaces now use the native manual Popover
|
|
229
|
+
> transport (`popover`, `showPopover()`, `hidePopover()`) instead of
|
|
230
|
+
> physically reparenting author DOM. This preserves inherited themes, density
|
|
231
|
+
> and application custom properties while leaving Actual's dismissal, focus
|
|
232
|
+
> and positioning contracts unchanged.
|
|
233
|
+
>
|
|
234
|
+
> Browsers below this JavaScript floor no longer receive the supported full
|
|
235
|
+
> Actual runtime. Core HTML/CSS degradation remains governed separately by the
|
|
236
|
+
> Degraded tier.
|
|
237
|
+
|
|
238
|
+
Not "Popover is now required": that reads as a dependency on the whole API
|
|
239
|
+
family, when the requirement is the four primitives listed above.
|
|
240
|
+
|
|
241
|
+
## Migration checklist
|
|
242
|
+
|
|
243
|
+
For the release that raises Minimal. Items 4–7 are scaffolding built for a floor
|
|
244
|
+
that will no longer exist; a gate outliving its reason is silent dead weight, so
|
|
245
|
+
they belong here rather than being found later.
|
|
246
|
+
|
|
247
|
+
1. WebKit verification — done, 8/8 on Safari 17.3, recorded above. Cleared.
|
|
248
|
+
2. Raise Minimal to the required subset above. The justification is the defect
|
|
249
|
+
and the transparent migration, not a download count: npm figures conflate
|
|
250
|
+
CI, bots, caches and the author, and say nothing about the end browser.
|
|
251
|
+
3. Delete the `Intermediate` tier. It is not merely less useful — with a new
|
|
252
|
+
Minimal above it on all three engines it is arithmetically absorbed, so its
|
|
253
|
+
features become floor-guaranteed and no information is lost.
|
|
254
|
+
4. Reclassify those features (`color-mix()`, `oklch()/oklab()`, `dvh/svh/lvh`,
|
|
255
|
+
`scrollbar-gutter`, `scrollbar-width`, `scrollbar-color`) as Minimal
|
|
256
|
+
baseline and drop their `check-compat` entries.
|
|
257
|
+
5. Remove the `@supports selector(:popover-open)` gates in `flyout.css` and
|
|
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.
|
|
261
|
+
6. Remove the two `popover` entries from `check-compat`'s feature table.
|
|
262
|
+
7. Remove the associated `compat-ok:` pragmas, and `hasPragma()` itself if
|
|
263
|
+
nothing else consumes it.
|
|
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.
|
|
268
|
+
9. Drop `test.failing` from the reparenting tests in
|
|
269
|
+
`tests/browser/surface-inherited-context.test.js`, keeping the assertions.
|
|
270
|
+
10. Update [surface-reparenting](surface-reparenting.md) from a known limitation
|
|
271
|
+
to resolved.
|
|
272
|
+
|
|
273
|
+
Steps 8 and 9 are linked but not enforced. The `test.failing` markers guarantee
|
|
274
|
+
that step 8 becomes unavoidable — they turn red as unexpected passes the moment
|
|
275
|
+
the transport changes — but nothing forces step 9. The test file points at the
|
|
276
|
+
note, and the note points back here; that is as far as documentation can close
|
|
277
|
+
the loop.
|
|
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
|
+
|
|
312
|
+
## What was deliberately left out
|
|
313
|
+
|
|
314
|
+
Not attempted, and not because it looked hard:
|
|
315
|
+
|
|
316
|
+
- **`popover="auto"`.** It cannot express `data-flyout-auto-close`, whose
|
|
317
|
+
default closes on an inside click; native light-dismiss closes on outside
|
|
318
|
+
clicks only, and `false` is inexpressible altogether. Adopting it would be a
|
|
319
|
+
behavior change to the component, not a transport swap.
|
|
320
|
+
- **Nested surfaces.** `openSurface()` closes every other open surface in the
|
|
321
|
+
document; the runtime is strictly flat and the transport change does not
|
|
322
|
+
alter that. Nesting is a separate feature decision, and the choice between
|
|
323
|
+
extending Actual's own stack and adopting the native one is functional before
|
|
324
|
+
it is technical.
|
|
325
|
+
- **`popover="hint"`, interest invokers, CSS anchor positioning.** Watch.
|
|
326
|
+
- **Further deletion for its own sake.** The branch's value is that it changes
|
|
327
|
+
the transport and nothing else. Broadening it would cost exactly the property
|
|
328
|
+
that makes it usable as a migration.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Surface reparenting broke inherited presentation context
|
|
2
|
+
|
|
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
|
+
|
|
7
|
+
## What used to happen
|
|
8
|
+
|
|
9
|
+
`surface.js` moved an open surface out of its place in the document:
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
getSurfaceRoot() → nearest <dialog>, else body
|
|
13
|
+
mountSurface() → root.append(panel)
|
|
14
|
+
restoreSurface() → put it back once the exit transition ends
|
|
15
|
+
```
|
|
16
|
+
|
|
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
|
+
visible.
|
|
22
|
+
|
|
23
|
+
Actual leans on inheritance deliberately: theme islands, density, `.inverted`,
|
|
24
|
+
and the custom properties an application scopes to its own containers all
|
|
25
|
+
travel that way. So this was not one bug about themes. It was the same bug in as
|
|
26
|
+
many forms as there are inherited contexts.
|
|
27
|
+
|
|
28
|
+
## Measured
|
|
29
|
+
|
|
30
|
+
Chrome, one `.flyout` per scope, values read off the panel before and after the
|
|
31
|
+
click that opens it:
|
|
32
|
+
|
|
33
|
+
| Scope | Closed | Open |
|
|
34
|
+
| ------------------------- | ---------------------------------------- | -------------------- |
|
|
35
|
+
| `[data-theme="dark"]` | `background: rgb(30, 26, 35)` | `rgb(255, 255, 255)` |
|
|
36
|
+
| `.sm` | `--control-size: 2rem` | `2.375rem` |
|
|
37
|
+
| `.inverted` | `--ui-fg: light-dark(hsl(0 0% 100%), …)` | *(unset)* |
|
|
38
|
+
| app-scoped `--app-accent` | `rgb(1, 2, 3)` | *(unset)* |
|
|
39
|
+
| `.sm` inside a `<dialog>` | `--control-size: 2rem` | `2.375rem` |
|
|
40
|
+
|
|
41
|
+
Every context broke. Three consequences were worth stating plainly:
|
|
42
|
+
|
|
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
|
+
that dialog and its trigger. Choosing a nearer root loses less context; it
|
|
50
|
+
does not preserve it.
|
|
51
|
+
|
|
52
|
+
`tests/browser/surface-inherited-context.test.js` holds those assertions and
|
|
53
|
+
now passes against the shipped transport.
|
|
54
|
+
|
|
55
|
+
## Why the obvious repairs are wrong
|
|
56
|
+
|
|
57
|
+
Still wrong, and still tempting — this section is the reason the note is kept.
|
|
58
|
+
|
|
59
|
+
**Copying `data-theme` onto the moved panel.** Fixes the one symptom that was
|
|
60
|
+
noticed and leaves the other four broken. It also turns an inherited contract
|
|
61
|
+
into an enumerated one, so every future scope has to be added to a list.
|
|
62
|
+
|
|
63
|
+
**Snapshotting custom properties before the move.** There is no bounded set to
|
|
64
|
+
snapshot — an application's own hooks are unknowable by definition, and
|
|
65
|
+
`getComputedStyle` cannot enumerate inherited custom properties portably. It
|
|
66
|
+
would also freeze values that should stay live.
|
|
67
|
+
|
|
68
|
+
**Mounting to a nearer root.** The dialog row above is the experiment: it
|
|
69
|
+
reduces the damage without addressing the cause.
|
|
70
|
+
|
|
71
|
+
The cause was that the panel was moved at all. The repair is a transport that
|
|
72
|
+
promotes the element without relocating it.
|
|
73
|
+
|
|
74
|
+
## The repair, as shipped
|
|
75
|
+
|
|
76
|
+
A native popover renders in the top layer while staying exactly where the
|
|
77
|
+
author put it in the DOM. That removed the reason to reparent, and with it the
|
|
78
|
+
whole mechanism:
|
|
79
|
+
|
|
80
|
+
```text
|
|
81
|
+
mountedSurfaces
|
|
82
|
+
getSurfaceRoot()
|
|
83
|
+
mountSurface()
|
|
84
|
+
restoreSurface()
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`popover="manual"` is the mode that fits: it supplies the top layer and nothing
|
|
88
|
+
else, so `surface.js` keeps its dismissal policy, Escape ordering, focus
|
|
89
|
+
restoration and placement orchestration unchanged. See
|
|
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.
|
|
97
|
+
|
|
98
|
+
## The contract now
|
|
99
|
+
|
|
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.
|
|
103
|
+
|
|
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.
|
|
107
|
+
|
|
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.
|
|
112
|
+
|
|
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.
|
|
120
|
+
|
|
121
|
+
## Experiment
|
|
122
|
+
|
|
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.
|
|
127
|
+
|
|
128
|
+
## Review trigger
|
|
129
|
+
|
|
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.
|
|
@@ -8,8 +8,8 @@ The JS runtime doubles as a primitive kit for building custom widgets.
|
|
|
8
8
|
| ---------------------------------- | ------------- | --------------------------- |
|
|
9
9
|
| Discovery, lifecycle, cleanup | `enhance` | `actual-css/js/enhance` |
|
|
10
10
|
| Per-document LIFO Escape dismissal | `escape` | `actual-css/js/escape` |
|
|
11
|
-
| Positioning, flip and shift | `floating` |
|
|
12
|
-
| Open/close,
|
|
11
|
+
| Positioning, flip and shift | `floating` | `@lekoala/floating` |
|
|
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,9 +22,9 @@ 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
|
-
- `floating` — `
|
|
27
|
+
- `floating` — `autoUpdate`, `reposition`, `repositionAt`, plus
|
|
28
28
|
`--available-height` and `data-placement`
|
|
29
29
|
- `keys` — `itemForKey`, `firstItem`, `lastItem`, `nextItem`, with edge,
|
|
30
30
|
wrapping and RTL handling
|
|
@@ -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.
|