stimeo-ui 0.4.0 → 0.6.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 +184 -0
- package/dist/controllers/announcer_controller.js.map +1 -1
- package/dist/controllers/aspect_ratio_controller.d.ts +3 -5
- package/dist/controllers/aspect_ratio_controller.js +19 -11
- package/dist/controllers/aspect_ratio_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.d.ts +36 -15
- package/dist/controllers/avatar_controller.js +195 -40
- package/dist/controllers/avatar_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.js +5 -1
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +12 -3
- package/dist/controllers/carousel_controller.js +90 -10
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/checkbox_controller.d.ts +31 -5
- package/dist/controllers/checkbox_controller.js +136 -25
- package/dist/controllers/checkbox_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.js +8 -3
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.d.ts +1 -1
- package/dist/controllers/collapsible_controller.js +4 -1
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +9 -3
- package/dist/controllers/color_picker_controller.js +41 -11
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +1 -1
- package/dist/controllers/context_menu_controller.js +2 -2
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +5 -3
- package/dist/controllers/countdown_controller.js +5 -1
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +23 -4
- package/dist/controllers/date_range_picker_controller.js +162 -31
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.d.ts +1 -1
- package/dist/controllers/direct_upload_controller.js +3 -3
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/empty_state_controller.d.ts +27 -7
- package/dist/controllers/empty_state_controller.js +107 -16
- package/dist/controllers/empty_state_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.d.ts +7 -1
- package/dist/controllers/file_dropzone_controller.js +26 -3
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +25 -5
- package/dist/controllers/flash_controller.js +161 -21
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +2 -3
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js +8 -2
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +19 -2
- package/dist/controllers/frame_loading_controller.js +94 -22
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.d.ts +8 -4
- package/dist/controllers/highlight_controller.js +38 -1
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +2 -2
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +5 -4
- package/dist/controllers/idle_controller.js +39 -6
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.js +2 -0
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +1 -1
- package/dist/controllers/masonry_controller.js +1 -1
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.js +5 -3
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.js +3 -1
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +46 -12
- package/dist/controllers/multi_select_controller.js +460 -151
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.d.ts +15 -13
- package/dist/controllers/network_status_controller.js +1 -3
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +29 -3
- package/dist/controllers/number_input_controller.js +455 -64
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.js +5 -1
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.js +38 -1
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_strength_controller.d.ts +4 -4
- package/dist/controllers/password_strength_controller.js +21 -3
- package/dist/controllers/password_strength_controller.js.map +1 -1
- package/dist/controllers/persist_controller.d.ts +4 -4
- package/dist/controllers/persist_controller.js +24 -5
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.js +10 -0
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +2 -2
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +1 -2
- package/dist/controllers/portal_controller.js +10 -0
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/progress_controller.d.ts +1 -1
- package/dist/controllers/progress_controller.js +7 -3
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.d.ts +41 -15
- package/dist/controllers/radio_group_controller.js +540 -56
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.d.ts +23 -0
- package/dist/controllers/range_slider_controller.js +385 -94
- package/dist/controllers/range_slider_controller.js.map +1 -1
- package/dist/controllers/rating_controller.d.ts +36 -31
- package/dist/controllers/rating_controller.js +274 -89
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.js +2 -0
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/resizable_controller.js +33 -0
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.d.ts +4 -0
- package/dist/controllers/roving_controller.js +60 -5
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.d.ts +7 -1
- package/dist/controllers/scroll_area_controller.js +155 -23
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.js +33 -0
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/separator_controller.js +13 -17
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.d.ts +2 -2
- package/dist/controllers/skeleton_controller.js +71 -3
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +17 -1
- package/dist/controllers/slider_controller.js +325 -48
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.d.ts +15 -10
- package/dist/controllers/spinner_controller.js +18 -3
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +1 -1
- package/dist/controllers/step_indicator_controller.js +3 -1
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.js +2 -0
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/switch_controller.d.ts +12 -8
- package/dist/controllers/switch_controller.js +162 -18
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +41 -11
- package/dist/controllers/tags_input_controller.js +356 -120
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.js +1 -1
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.d.ts +42 -10
- package/dist/controllers/time_picker_controller.js +296 -104
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.d.ts +39 -13
- package/dist/controllers/toggle_group_controller.js +378 -55
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.js +5 -3
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +2 -2
- package/dist/controllers/tooltip_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +2 -3
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +1 -2
- package/dist/controllers/tree_view_controller.js +24 -4
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.js +3811 -1048
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +92 -4
- package/dist/inspector/cli.js +174 -6
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +234 -8
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +24 -24
- package/dist/inspector/manifest.json +573 -59
- package/dist/positioning/index.d.ts +2 -2
- package/dist/positioning/index.js.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,188 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
While the version is `0.x`, the public API (the `stimeo--*` data attributes) may
|
|
8
8
|
change between releases.
|
|
9
9
|
|
|
10
|
+
## [0.6.0] - 2026-08-15
|
|
11
|
+
|
|
12
|
+
Minor release that separates reconciled state from user edits: `change` now
|
|
13
|
+
means the user committed a value, and a controller that re-derives its own state
|
|
14
|
+
reports `reconcile` instead. Several markup contracts tightened, and authored
|
|
15
|
+
prose — remove-button names, announcements — moved out of the library. The
|
|
16
|
+
Inspector manifest stays on schema v11, but **`stimeo check` can report errors on
|
|
17
|
+
views that passed under 0.5.0**, so run it before deploying.
|
|
18
|
+
|
|
19
|
+
### Upgrade notes
|
|
20
|
+
|
|
21
|
+
- Listening to `change` to observe controller-driven repair? Subscribe to
|
|
22
|
+
`reconcile` too: carousel, checkbox, color-picker, multi-select, number-input,
|
|
23
|
+
radio-group, rating, tags-input, time-picker. Neither fires on connect.
|
|
24
|
+
- rating: delete `max` and every `data-rating-value`. The DOM order of the
|
|
25
|
+
`symbol` targets is the scale.
|
|
26
|
+
- aspect-ratio: delete the `content` target.
|
|
27
|
+
- toggle-group, radio-group: items must be `<button type="button">` or a
|
|
28
|
+
non-interactive host with `role="button"` / `role="radio"`. Others are left
|
|
29
|
+
untouched at runtime.
|
|
30
|
+
- tags-input, multi-select: delete the `status` target, put a
|
|
31
|
+
`stimeo--announcer` on the page, and set `announceText` /
|
|
32
|
+
`announceRemovedText`. In the chip template add `label` and `remove` targets
|
|
33
|
+
and author the remove button's `aria-label` (`{label}` / `{value}` expand).
|
|
34
|
+
Drop `data-*-slot="label"` and tags-input's `removeTag` action.
|
|
35
|
+
- date-range-picker: declare unavailable days with `disabled-dates`; an
|
|
36
|
+
`aria-disabled` you write on a cell is overwritten on the next paint.
|
|
37
|
+
- time-picker: AM/PM is a `role="spinbutton"` segment, not an `aria-pressed`
|
|
38
|
+
toggle. `seconds` only decides whether the composed value carries seconds.
|
|
39
|
+
- List values are read as JSON strings — idle's `events`, password-strength's
|
|
40
|
+
`levels`, persist's `exclude`, date-range-picker's `disabled-dates`. The
|
|
41
|
+
markup is unchanged; the manifest reports `String` where it reported `Array`.
|
|
42
|
+
|
|
43
|
+
### Removed
|
|
44
|
+
|
|
45
|
+
- **Breaking** — rating: `max` and `data-rating-value`. aspect-ratio: the
|
|
46
|
+
`content` target. tags-input, multi-select: the `status` target, tags-input's
|
|
47
|
+
`removeTag` action, and the English `Remove {value}` label the library wrote
|
|
48
|
+
onto generated remove buttons.
|
|
49
|
+
|
|
50
|
+
### Added
|
|
51
|
+
|
|
52
|
+
- A `reconcile` event, carrying the same `detail` as that component's `change`,
|
|
53
|
+
on carousel, checkbox, color-picker, multi-select, number-input, radio-group,
|
|
54
|
+
rating, tags-input, and time-picker.
|
|
55
|
+
- checkbox: a group works without a `parent`; the root still reports
|
|
56
|
+
`all` / `partial` / `none`.
|
|
57
|
+
- rating: `Delete` and `Backspace` clear the rating when `clearable`.
|
|
58
|
+
- date-range-picker: `disabled-dates`, a `monthchange` event,
|
|
59
|
+
`Shift+PageUp` / `Shift+PageDown` for stepping a year, and month labels
|
|
60
|
+
formatted from `documentElement.lang`.
|
|
61
|
+
- multi-select: a `fields` target holding one hidden input per selected value,
|
|
62
|
+
with `name` and `form`, plus `announceText` / `announceRemovedText`.
|
|
63
|
+
tags-input: `label` and `remove` targets and the same two announce values.
|
|
64
|
+
- avatar: an `empty` state beside `loading`, `loaded`, and `error`.
|
|
65
|
+
- Inspector: `invalid-host` for radio-group and toggle-group items;
|
|
66
|
+
chip-template parts required *inside* the template; `aria-label` on remove
|
|
67
|
+
buttons and the group role on toggle groups; at most one checkbox `parent`
|
|
68
|
+
and one checked radio; a positive integer time-picker `step`. An empty
|
|
69
|
+
attribute value no longer satisfies a requirement that asks only for the
|
|
70
|
+
attribute's presence.
|
|
71
|
+
|
|
72
|
+
### Changed
|
|
73
|
+
|
|
74
|
+
- **Breaking** — `change` is reserved for user-committed values. DOM
|
|
75
|
+
normalization, target churn, morphs, and runtime value changes report
|
|
76
|
+
`reconcile`, and a repair that leaves the committed state unchanged reports
|
|
77
|
+
nothing.
|
|
78
|
+
- **Breaking** — radio-group, time-picker: the hidden `field` fires a native
|
|
79
|
+
`change` only for user edits, so a server-rendered repaint cannot drive an
|
|
80
|
+
`auto-submit` companion into a resubmit loop.
|
|
81
|
+
- **Breaking** — multi-select: `max` is enforced at all times, not only on the
|
|
82
|
+
click path; surplus options normalize to `aria-selected="false"`.
|
|
83
|
+
- **Breaking** — avatar: a present `src` value wins over an authored `src`, and
|
|
84
|
+
an empty one means "no image". Remove the attribute to hand the element's own
|
|
85
|
+
`src` back.
|
|
86
|
+
- **Breaking** — checkbox: a parent toggle sets each child's `checked` without
|
|
87
|
+
synthesizing a native `change` per child; the root reports one event.
|
|
88
|
+
- toggle-group, radio-group, tags-input, multi-select: interaction is delegated
|
|
89
|
+
from the container, so items added at runtime work without per-item
|
|
90
|
+
`data-action`. Existing per-item actions stay compatible.
|
|
91
|
+
- toggle-group, radio-group: `aria-disabled` items stay reachable by arrow keys
|
|
92
|
+
with activation suppressed; natively disabled and `hidden` items are skipped.
|
|
93
|
+
- number-input, rating, scroll-area, time-picker: ARIA and `tabindex` the
|
|
94
|
+
controller writes are treated as loans and handed back to their authored
|
|
95
|
+
values, without overwriting a change you made in the meantime.
|
|
96
|
+
- date-range-picker: a preset that only partly overlaps `min` / `max` is
|
|
97
|
+
narrowed to the selectable span instead of committing an unavailable endpoint.
|
|
98
|
+
- multi-select, tags-input: `Backspace` in an empty input removes the last chip
|
|
99
|
+
and keeps focus in the input.
|
|
100
|
+
- time-picker: a unit that wraps past noon or midnight carries AM/PM with it,
|
|
101
|
+
and one operation reports one `change` with the final value.
|
|
102
|
+
|
|
103
|
+
### Fixed
|
|
104
|
+
|
|
105
|
+
- avatar, carousel, checkbox, radio-group, rating, roving, toggle-group: targets
|
|
106
|
+
added, removed, or morphed at runtime settle on one state — a single tab stop,
|
|
107
|
+
a single checked or pressed item, and focus on a surviving item.
|
|
108
|
+
- date-range-picker: an unavailable day cannot become a committed endpoint
|
|
109
|
+
through a preset, a preview, or Enter/Space; a reversed stored range is
|
|
110
|
+
normalized on connect; a pending selection is rewound before Turbo caches the
|
|
111
|
+
page.
|
|
112
|
+
- number-input: commits compare numbers rather than the displayed text, and IME
|
|
113
|
+
composition never commits a value.
|
|
114
|
+
- time-picker: with `wrap` disabled, 11 and 12 no longer step into each other,
|
|
115
|
+
and an unfinished direct-entry buffer is discarded when focus leaves.
|
|
116
|
+
- multi-select, tags-input: a chip is built only when the template can produce a
|
|
117
|
+
complete one, only the `remove` target inside a chip acts on it, and a
|
|
118
|
+
replaced `tags` container is re-wired.
|
|
119
|
+
- file-dropzone, scroll-area: a replaced `list` / `viewport` keeps its state and
|
|
120
|
+
takes over the listeners; the old element stops responding.
|
|
121
|
+
- idle, password-strength, persist, date-range-picker: a malformed list value
|
|
122
|
+
falls back to its default instead of stopping the controller from connecting
|
|
123
|
+
and leaving the whole element inert.
|
|
124
|
+
|
|
125
|
+
## [0.5.0] - 2026-08-13
|
|
126
|
+
|
|
127
|
+
### Changed
|
|
128
|
+
|
|
129
|
+
- **Breaking** — every CSS custom property the library writes now carries the
|
|
130
|
+
`--stimeo--` namespace (two hyphens). Rename them in your stylesheets: a name
|
|
131
|
+
you miss raises no error, it just falls back to `var()`'s default and the bar
|
|
132
|
+
or thumb stops moving.
|
|
133
|
+
- `--stimeo-range-start` / `--stimeo-range-end` →
|
|
134
|
+
`--stimeo--range-slider-start` / `--stimeo--range-slider-end`
|
|
135
|
+
- `--stimeo-<name>` → `--stimeo--<name>` for `aspect-ratio`,
|
|
136
|
+
`collapsible-content-height`, `color`, `context-menu-x`, `context-menu-y`,
|
|
137
|
+
`masonry-columns`, `meter-ratio`, `password-strength`, `progress-ratio`,
|
|
138
|
+
`scroll-progress`, `step-indicator-ratio`, `textarea-rows`,
|
|
139
|
+
`upload-progress`
|
|
140
|
+
- **Breaking** — spinner's `indicator`, the network-status banners, and
|
|
141
|
+
countdown's `status` slot are no longer live regions. Drop `role="status"` /
|
|
142
|
+
`role="alert"` / `aria-live` from them and set `announceText` (and its
|
|
143
|
+
siblings) with a `stimeo--announcer` on the page; keeping the role reads the
|
|
144
|
+
change twice.
|
|
145
|
+
- time-picker: committing a field change dispatches a native `change` from the
|
|
146
|
+
`field` target, and `stimeo--time-picker:change` reports the composed value
|
|
147
|
+
even when no `field` target is present.
|
|
148
|
+
- tree-view: more native elements count as nested-interactive (`label`,
|
|
149
|
+
`summary`, `details`, `area[href]`, media with `controls`, `iframe`, `object`,
|
|
150
|
+
`embed`), so a click or key inside one is left alone instead of moving the
|
|
151
|
+
tree selection.
|
|
152
|
+
|
|
153
|
+
### Added
|
|
154
|
+
|
|
155
|
+
- Inspector: the manifest advances from schema v8 to **v11** — tools that read it
|
|
156
|
+
raw must accept the new `valueConstraints`, `hosts`, and `valueRelations`
|
|
157
|
+
fields. Two diagnostics are new, so **re-run `stimeo check` before deploying;
|
|
158
|
+
it can report errors on views that passed under 0.4.0**:
|
|
159
|
+
- `invalid-value` — slider, range-slider, and number-input need a finite,
|
|
160
|
+
positive `step`; range-slider needs finite `min`, `max`, `start`, and `end`
|
|
161
|
+
with `min` at or below `max`.
|
|
162
|
+
- `invalid-host` — switch, time-picker, and tree-view report an unsupported
|
|
163
|
+
host element before it reaches the browser.
|
|
164
|
+
|
|
165
|
+
### Fixed
|
|
166
|
+
|
|
167
|
+
- range-slider: two thumbs resting on the same value can be pulled apart with
|
|
168
|
+
the pointer again, and invalid or reversed bounds fall back to a finite
|
|
169
|
+
ordered range.
|
|
170
|
+
- slider, range-slider, number-input: an endpoint that does not sit on the step
|
|
171
|
+
grid is reachable, and an invalid `step` falls back to `1`.
|
|
172
|
+
- slider, range-slider: a Turbo morph that swaps a value repaints instead of
|
|
173
|
+
freezing, a replaced thumb or track picks up the current state, and a drag
|
|
174
|
+
stays with the pointer that started it.
|
|
175
|
+
- switch: holding Space no longer scrolls a non-`<button>` host, and an ancestor
|
|
176
|
+
`fieldset[disabled]` or `aria-disabled` blocks activation.
|
|
177
|
+
- flash: hover and focus pause the auto-dismiss independently, a flash paused
|
|
178
|
+
after its deadline dismisses instead of staying forever, a second dismiss on a
|
|
179
|
+
leaving flash is ignored, and a replaced `region` target is picked up.
|
|
180
|
+
- frame-loading: `aria-busy`, the skeleton, and `inert` are rolled back when the
|
|
181
|
+
controller is detached, instead of burning in with nothing left to finish the
|
|
182
|
+
load.
|
|
183
|
+
- empty-state: a replaced `list` or `empty` element and a runtime `itemSelector`
|
|
184
|
+
change are followed, so the placeholder no longer sticks beside a filled list.
|
|
185
|
+
- highlight: a highlight that outlives a Turbo navigation is cleaned up on the
|
|
186
|
+
way back instead of staying marked.
|
|
187
|
+
- idle: `data-idle` is cleared when a new measurement cycle connects, and
|
|
188
|
+
`disconnect()` removes the listeners it actually registered even when `events`
|
|
189
|
+
changed while connected.
|
|
190
|
+
- skeleton: a skeleton moved within the page keeps its ready intent.
|
|
191
|
+
|
|
10
192
|
## [0.4.0] - 2026-08-11
|
|
11
193
|
|
|
12
194
|
The feedback and status components announce through the shared
|
|
@@ -391,6 +573,8 @@ Initial public alpha: 101 behavior-only, accessible Stimulus controllers driven
|
|
|
391
573
|
by `data-*` attributes, shipping no CSS. Published to npm (with provenance) and
|
|
392
574
|
RubyGems.
|
|
393
575
|
|
|
576
|
+
[0.6.0]: https://github.com/taiyaky/stimeo-ui/releases/tag/v0.6.0
|
|
577
|
+
[0.5.0]: https://github.com/taiyaky/stimeo-ui/releases/tag/v0.5.0
|
|
394
578
|
[0.4.0]: https://github.com/taiyaky/stimeo-ui/releases/tag/v0.4.0
|
|
395
579
|
[0.3.0]: https://github.com/taiyaky/stimeo-ui/releases/tag/v0.3.0
|
|
396
580
|
[0.2.1]: https://github.com/taiyaky/stimeo-ui/releases/tag/v0.2.1
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/before_cache_reset.ts","../../src/utils/microtask_coalescer.ts","../../src/utils/safe_timeout.ts","../../src/controllers/announcer_controller.ts"],"names":[],"mappings":"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","file":"announcer_controller.js","sourcesContent":["/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way (the `MicrotaskCoalescer` split).\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","/**\n * Collapses many target callbacks from one DOM mutation into a single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element, so\n * replacing a list of N options delivers N callbacks — but the useful unit of\n * work is \"reconcile against the DOM that resulted\", once, after the batch has\n * settled. Every controller that owns a reconcilable target set needs the same\n * shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers the *initial* target callbacks\n * ahead of `connect()`. Reconciling there would compute a fallback against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/** The two politeness levels this controller keeps a region for. */\nconst LEVELS = [\"polite\", \"assertive\"] as const;\n\n/** One of the politeness levels in {@link LEVELS}. */\ntype Level = (typeof LEVELS)[number];\n\n/**\n * Headless, shared **live-region announcer** — a polite/assertive screen-reader\n * announcement base (no dedicated APG pattern; follows the WAI-ARIA \"Alert\" /\n * \"Status\" live-region guidance and WCAG 2.2 **4.1.3 Status Messages**).\n *\n * Markup contract (identifier: `stimeo--announcer`):\n * <!-- Place once per page; the consumer visually hides the regions in CSS. -->\n * <div data-controller=\"stimeo--announcer\">\n * <div data-stimeo--announcer-target=\"polite\" aria-live=\"polite\" aria-atomic=\"true\"></div>\n * <div data-stimeo--announcer-target=\"assertive\" aria-live=\"assertive\" aria-atomic=\"true\"></div>\n * </div>\n *\n * <!-- Attribute-only trigger: declare the activation event explicitly. -->\n * <button data-action=\"click->stimeo--announcer#announce\"\n * data-stimeo--announcer-message-param=\"Saved\"\n * data-stimeo--announcer-assertive-param=\"false\">Save</button>\n *\n * <!-- Programmatic trigger (e.g. from another controller / Turbo Stream). -->\n * window.dispatchEvent(new CustomEvent(\"stimeo--announcer:announce\", {\n * detail: { message: \"12 results\", assertive: false },\n * }))\n *\n * The announcer is the shared substrate other controllers (Auto-Submit, Flash,\n * Bulk Select, …) lean on instead of each carrying their own live region.\n *\n * @remarks\n * Behavior only, with **one deliberate exception**: when a `polite`/`assertive`\n * target is absent the controller *generates* the missing region **on connect**\n * and applies the canonical visually-hidden inline style (see {@link visuallyHide}).\n * A live region must exist and be visually hidden to do its job, and a generated\n * node has no consumer CSS hook to hide it; consumers who want to own styling\n * supply their own targets. Generation happens up front rather than on the first\n * announcement because assistive tech reports changes to a region it already\n * knows about — a region created and written within one task loses that first\n * message. The controller never moves focus — announcements must not steal it\n * (WCAG 2.2 4.1.3). Listeners and clear timers are torn down on `disconnect()`\n * (Turbo included), and any generated regions are removed — from the cached\n * snapshot too, via {@link BeforeCacheReset}, because `connect()` cannot reuse a\n * restored region (it carries no target attribute) and would pair another one\n * with it on every visit.\n *\n * **Messages are queued, one write per task.** Assistive tech announces what it\n * observes changing, so two messages written into one region within a single task\n * are one observed change and the earlier message is never read. Each politeness\n * has its own FIFO — an assertive message never waits behind polite ones — and a\n * message is written only in a task where its region already existed.\n *\n * **The region set is kept whole while connected.** Exactly one region per\n * politeness is present: an authored target retires the stand-in generated for it,\n * losing one materialises a stand-in again, and a generated region removed by a\n * morph (it is absent from the server's HTML, so a morph drops it) is put back\n * without waiting for the next message.\n */\nexport class AnnouncerController extends Controller<HTMLElement> {\n static override targets = [\"polite\", \"assertive\"];\n static override values = {\n clearAfter: { type: Number, default: 1000 },\n dedupeReannounce: { type: Boolean, default: true },\n };\n static actions = [\"announce\"] as const;\n\n declare readonly politeTarget: HTMLElement;\n declare readonly assertiveTarget: HTMLElement;\n declare readonly hasPoliteTarget: boolean;\n declare readonly hasAssertiveTarget: boolean;\n\n declare clearAfterValue: number;\n declare dedupeReannounceValue: boolean;\n\n /** Clear/re-announce timers; one `clearAll()` in disconnect tears them all down. */\n readonly #timers = new SafeTimeout();\n\n /** Live regions generated to stand in for absent targets, for teardown. */\n readonly #generated = new Map<Level, HTMLElement>();\n\n /** Messages waiting to be written, oldest first, one queue per politeness. */\n readonly #queues = new Map<Level, string[]>();\n\n /** Politeness levels whose next drain is already armed. */\n readonly #draining = new Set<Level>();\n\n /** Collapses a batch of target callbacks (and morph removals) into one pass. */\n readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileRegions());\n\n /**\n * Watches the host's own children for a generated region disappearing. A morph\n * drops it — the server's HTML never had it — and no target callback reports\n * that, because a generated region carries no target attribute. `subtree` stays\n * off so writing a message inside a region does not re-enter this pass.\n */\n readonly #hostWatch = new MutationObserver(() => {\n this.#reconcile.schedule();\n });\n\n /** Rewinds to an announceable initial state for the snapshot; see the remarks. */\n readonly #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());\n\n /**\n * The one timer a region may have outstanding — its dedupe re-set, then its\n * auto-clear. Held weakly so a swapped-out target is not retained; a leftover\n * id is harmless because {@link SafeTimeout.clear} no-ops on an id it does not\n * own.\n */\n readonly #pending = new WeakMap<HTMLElement, number>();\n\n /**\n * Guards against handling the same CustomEvent twice. An event dispatched on\n * the controller element with `bubbles: true` reaches both the element and the\n * `window` listener; this WeakSet ensures it announces only once.\n */\n readonly #handled = new WeakSet<Event>();\n\n /** Receives programmatic announcements at the element or bubbled to `window`. */\n readonly #onAnnounceEvent = (event: Event): void => {\n if (this.#handled.has(event)) return;\n this.#handled.add(event);\n const detail = (event as CustomEvent<unknown>).detail;\n const message = this.#messageFromDetail(detail);\n if (!message) return;\n this.#announce(message, this.#assertiveFromDetail(detail));\n };\n\n override connect(): void {\n this.#reconcile.activate();\n this.#beforeCache.activate();\n // Materialise any missing region now, so it is in the accessibility tree\n // before the first message rather than appearing with it.\n this.#reconcileRegions();\n this.#hostWatch.observe(this.element, { childList: true });\n this.element.addEventListener(\"stimeo--announcer:announce\", this.#onAnnounceEvent);\n window.addEventListener(\"stimeo--announcer:announce\", this.#onAnnounceEvent);\n }\n\n override disconnect(): void {\n this.#reconcile.cancel();\n this.#hostWatch.disconnect();\n this.#beforeCache.deactivate();\n this.element.removeEventListener(\"stimeo--announcer:announce\", this.#onAnnounceEvent);\n window.removeEventListener(\"stimeo--announcer:announce\", this.#onAnnounceEvent);\n this.#timers.clearAll();\n this.#queues.clear();\n this.#draining.clear();\n this.#removeGenerated();\n }\n\n /** Retires the stand-in once the consumer supplies a polite region. */\n politeTargetConnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Materialises a stand-in once the consumer's polite region goes away. */\n politeTargetDisconnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Retires the stand-in once the consumer supplies an assertive region. */\n assertiveTargetConnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Materialises a stand-in once the consumer's assertive region goes away. */\n assertiveTargetDisconnected(): void {\n this.#reconcile.schedule();\n }\n\n /**\n * Brings the region set back to exactly one region per politeness and reports\n * whether anything had to be created.\n *\n * A region created here is not written to in the same task: assistive tech\n * reports changes to regions it already knows about, so {@link drain} waits a\n * task whenever this says a region is new.\n */\n #reconcileRegions(): boolean {\n let created = false;\n for (const level of LEVELS) {\n if (this.#hasTargetFor(level)) {\n // The consumer owns this politeness now; a stand-in would be a second\n // region for it, and an empty one nothing ever writes to.\n const generated = this.#generated.get(level);\n if (generated) {\n generated.remove();\n this.#generated.delete(level);\n }\n continue;\n }\n const existing = this.#generated.get(level);\n if (existing?.isConnected) continue;\n this.#generated.set(level, this.#createRegion(level));\n created = true;\n }\n return created;\n }\n\n /** Whether the consumer supplied a target for `level`. */\n #hasTargetFor(level: Level): boolean {\n return level === \"assertive\" ? this.hasAssertiveTarget : this.hasPoliteTarget;\n }\n\n /** Builds a visually hidden live region for `level` and attaches it. */\n #createRegion(level: Level): HTMLElement {\n const region = document.createElement(\"div\");\n region.setAttribute(\"aria-live\", level);\n region.setAttribute(\"aria-atomic\", \"true\");\n visuallyHide(region);\n this.element.appendChild(region);\n return region;\n }\n\n /**\n * Removes and forgets every region this controller generated. Authored targets\n * belong to the consumer and are left untouched. Forgetting them is what keeps\n * the live page working after a snapshot rewind: the next announcement finds an\n * empty map and materialises a fresh region.\n */\n #removeGenerated(): void {\n for (const region of this.#generated.values()) {\n region.remove();\n }\n this.#generated.clear();\n }\n\n /**\n * Announces a message. Reads the text from a Stimulus action param\n * (`message`, plus optional `assertive`) for attribute-only triggers, falling\n * back to a CustomEvent `detail` when the same handler is wired to an event.\n * An empty/non-string message is ignored so untrusted payloads cannot blank\n * the region.\n */\n announce(event: Event): void {\n const params = (event as { params?: Record<string, unknown> }).params;\n const fromParam = params?.message;\n const message =\n typeof fromParam === \"string\" && fromParam.length > 0\n ? fromParam\n : this.#messageFromDetail((event as CustomEvent<unknown>).detail);\n if (!message) return;\n\n const assertive =\n params?.assertive === true ||\n this.#assertiveFromDetail((event as CustomEvent<unknown>).detail);\n this.#announce(message, assertive);\n }\n\n /**\n * Queues `message` for its politeness and arms the drain.\n *\n * Queuing is what makes a burst audible: assistive tech announces the changes it\n * observes, so several messages written into one region within a single task are\n * one change and only the last is read.\n */\n #announce(message: string, assertive: boolean): void {\n const level: Level = assertive ? \"assertive\" : \"polite\";\n const queue = this.#queues.get(level);\n if (queue) {\n queue.push(message);\n } else {\n this.#queues.set(level, [message]);\n }\n this.#scheduleDrain(level);\n }\n\n /** Arms one drain pass for `level`; further messages ride the pass already armed. */\n #scheduleDrain(level: Level): void {\n if (this.#draining.has(level)) return;\n this.#draining.add(level);\n this.#timers.set(() => {\n this.#draining.delete(level);\n this.#drain(level);\n }, 0);\n }\n\n /**\n * Writes one queued message, then arms the next pass while the queue holds more.\n *\n * Two steps take a whole pass without consuming the message: materialising a\n * region (it has to be in the accessibility tree before the text arrives) and\n * emptying a region that already holds this exact text (an unchanged node is not\n * re-read, so `dedupeReannounce` clears first and writes on the following pass).\n */\n #drain(level: Level): void {\n const queue = this.#queues.get(level);\n const message = queue?.[0];\n if (queue === undefined || message === undefined) return;\n\n if (this.#reconcileRegions()) {\n this.#scheduleDrain(level);\n return;\n }\n\n const region = this.#regionFor(level);\n if (this.dedupeReannounceValue && region.textContent === message) {\n this.#cancelPending(region);\n region.textContent = \"\";\n this.#scheduleDrain(level);\n return;\n }\n\n queue.shift();\n this.#cancelPending(region);\n region.textContent = message;\n this.#scheduleClear(region, message);\n if (queue.length > 0) this.#scheduleDrain(level);\n }\n\n /** Clears the region after `clearAfter` ms, unless a newer message replaced it. */\n #scheduleClear(region: HTMLElement, message: string): void {\n if (this.clearAfterValue <= 0) return;\n this.#schedule(\n region,\n () => {\n if (region.textContent === message) region.textContent = \"\";\n },\n this.clearAfterValue,\n );\n }\n\n /**\n * Arms `region`'s single pending timer. Callers reach here with the slot\n * already free — `#announce` releases it, and a fired timer clears its own\n * entry below — so this does not cancel again.\n */\n #schedule(region: HTMLElement, callback: () => void, delay: number): void {\n const id = this.#timers.set(() => {\n // Drop the id as it fires: platform timer ids are recycled, and a stale\n // one left here could later cancel the *other* region's live timer.\n this.#pending.delete(region);\n callback();\n }, delay);\n this.#pending.set(region, id);\n }\n\n /** Releases `region`'s pending timer, if it has one. */\n #cancelPending(region: HTMLElement): void {\n // `SafeTimeout.clear` ignores an id it does not own, so the \"no pending\n // timer\" case needs no branch of its own.\n this.#timers.clear(this.#pending.get(region) ?? -1);\n this.#pending.delete(region);\n }\n\n /**\n * Resolves the live region for a politeness level.\n *\n * The remembered stand-in is used only while it is still in the document: a morph\n * can drop it, and writing into the detached node would announce nothing at all.\n */\n #regionFor(level: Level): HTMLElement {\n if (level === \"assertive\" && this.hasAssertiveTarget) return this.assertiveTarget;\n if (level === \"polite\" && this.hasPoliteTarget) return this.politeTarget;\n\n const existing = this.#generated.get(level);\n if (existing?.isConnected) return existing;\n\n const region = this.#createRegion(level);\n this.#generated.set(level, region);\n return region;\n }\n\n /**\n * Restores the announceable initial state for the snapshot Turbo is about to\n * take: queued and displayed messages go, generated regions go, and the live\n * page — which keeps running when a visit is aborted — gets its regions back on\n * the next task, after the clone.\n */\n #rewindForCache(): void {\n this.#queues.clear();\n this.#draining.clear();\n this.#timers.clearAll();\n for (const level of LEVELS) {\n if (this.#hasTargetFor(level)) {\n const target = level === \"assertive\" ? this.assertiveTarget : this.politeTarget;\n target.textContent = \"\";\n }\n }\n this.#removeGenerated();\n this.#timers.set(() => this.#reconcileRegions(), 0);\n }\n\n /** Extracts a non-empty string `message` from a CustomEvent detail, else null. */\n #messageFromDetail(detail: unknown): string | null {\n if (detail && typeof detail === \"object\" && \"message\" in detail) {\n const value = (detail as Record<string, unknown>).message;\n if (typeof value === \"string\" && value.length > 0) return value;\n }\n return null;\n }\n\n /** Reads an `assertive === true` flag from a CustomEvent detail (default polite). */\n #assertiveFromDetail(detail: unknown): boolean {\n return (\n !!detail &&\n typeof detail === \"object\" &&\n (detail as Record<string, unknown>).assertive === true\n );\n }\n}\n\n/**\n * Applies the canonical visually-hidden (\"sr-only\") inline style to a generated\n * live region so its text is announced without being seen. Inline so the library\n * stays self-contained when the consumer provides no target/CSS of its own.\n *\n * Pure (no `this`); exported for direct unit testing.\n */\nexport function visuallyHide(node: HTMLElement): void {\n const { style } = node;\n style.position = \"absolute\";\n style.width = \"1px\";\n style.height = \"1px\";\n style.margin = \"-1px\";\n style.padding = \"0\";\n style.border = \"0\";\n style.overflow = \"hidden\";\n style.clip = \"rect(0 0 0 0)\";\n style.clipPath = \"inset(50%)\";\n style.whiteSpace = \"nowrap\";\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/before_cache_reset.ts","../../src/utils/microtask_coalescer.ts","../../src/utils/safe_timeout.ts","../../src/controllers/announcer_controller.ts"],"names":[],"mappings":"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","file":"announcer_controller.js","sourcesContent":["/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way (the `MicrotaskCoalescer` split).\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","/**\n * Collapses many Stimulus lifecycle callbacks from one DOM mutation into a\n * single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element and\n * `<name>ValueChanged` once per changed attribute. Replacing a list of N options\n * or morphing several render Values therefore delivers N callbacks — but the\n * useful unit of work is \"reconcile against the resulting declarative input\",\n * once, after the batch has settled. Every controller with reconcilable targets\n * or render Values needs the same shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers initial target and Value callbacks\n * ahead of `connect()`. Reconciling there would compute output against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/** The two politeness levels this controller keeps a region for. */\nconst LEVELS = [\"polite\", \"assertive\"] as const;\n\n/** One of the politeness levels in {@link LEVELS}. */\ntype Level = (typeof LEVELS)[number];\n\n/**\n * Headless, shared **live-region announcer** — a polite/assertive screen-reader\n * announcement base (no dedicated APG pattern; follows the WAI-ARIA \"Alert\" /\n * \"Status\" live-region guidance and WCAG 2.2 **4.1.3 Status Messages**).\n *\n * Markup contract (identifier: `stimeo--announcer`):\n * <!-- Place once per page; the consumer visually hides the regions in CSS. -->\n * <div data-controller=\"stimeo--announcer\">\n * <div data-stimeo--announcer-target=\"polite\" aria-live=\"polite\" aria-atomic=\"true\"></div>\n * <div data-stimeo--announcer-target=\"assertive\" aria-live=\"assertive\" aria-atomic=\"true\"></div>\n * </div>\n *\n * <!-- Attribute-only trigger: declare the activation event explicitly. -->\n * <button data-action=\"click->stimeo--announcer#announce\"\n * data-stimeo--announcer-message-param=\"Saved\"\n * data-stimeo--announcer-assertive-param=\"false\">Save</button>\n *\n * <!-- Programmatic trigger (e.g. from another controller / Turbo Stream). -->\n * window.dispatchEvent(new CustomEvent(\"stimeo--announcer:announce\", {\n * detail: { message: \"12 results\", assertive: false },\n * }))\n *\n * The announcer is the shared substrate other controllers (Auto-Submit, Flash,\n * Bulk Select, …) lean on instead of each carrying their own live region.\n *\n * @remarks\n * Behavior only, with **one deliberate exception**: when a `polite`/`assertive`\n * target is absent the controller *generates* the missing region **on connect**\n * and applies the canonical visually-hidden inline style (see {@link visuallyHide}).\n * A live region must exist and be visually hidden to do its job, and a generated\n * node has no consumer CSS hook to hide it; consumers who want to own styling\n * supply their own targets. Generation happens up front rather than on the first\n * announcement because assistive tech reports changes to a region it already\n * knows about — a region created and written within one task loses that first\n * message. The controller never moves focus — announcements must not steal it\n * (WCAG 2.2 4.1.3). Listeners and clear timers are torn down on `disconnect()`\n * (Turbo included), and any generated regions are removed — from the cached\n * snapshot too, via {@link BeforeCacheReset}, because `connect()` cannot reuse a\n * restored region (it carries no target attribute) and would pair another one\n * with it on every visit.\n *\n * **Messages are queued, one write per task.** Assistive tech announces what it\n * observes changing, so two messages written into one region within a single task\n * are one observed change and the earlier message is never read. Each politeness\n * has its own FIFO — an assertive message never waits behind polite ones — and a\n * message is written only in a task where its region already existed.\n *\n * **The region set is kept whole while connected.** Exactly one region per\n * politeness is present: an authored target retires the stand-in generated for it,\n * losing one materialises a stand-in again, and a generated region removed by a\n * morph (it is absent from the server's HTML, so a morph drops it) is put back\n * without waiting for the next message.\n */\nexport class AnnouncerController extends Controller<HTMLElement> {\n static override targets = [\"polite\", \"assertive\"];\n static override values = {\n clearAfter: { type: Number, default: 1000 },\n dedupeReannounce: { type: Boolean, default: true },\n };\n static actions = [\"announce\"] as const;\n\n declare readonly politeTarget: HTMLElement;\n declare readonly assertiveTarget: HTMLElement;\n declare readonly hasPoliteTarget: boolean;\n declare readonly hasAssertiveTarget: boolean;\n\n declare clearAfterValue: number;\n declare dedupeReannounceValue: boolean;\n\n /** Clear/re-announce timers; one `clearAll()` in disconnect tears them all down. */\n readonly #timers = new SafeTimeout();\n\n /** Live regions generated to stand in for absent targets, for teardown. */\n readonly #generated = new Map<Level, HTMLElement>();\n\n /** Messages waiting to be written, oldest first, one queue per politeness. */\n readonly #queues = new Map<Level, string[]>();\n\n /** Politeness levels whose next drain is already armed. */\n readonly #draining = new Set<Level>();\n\n /** Collapses a batch of target callbacks (and morph removals) into one pass. */\n readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileRegions());\n\n /**\n * Watches the host's own children for a generated region disappearing. A morph\n * drops it — the server's HTML never had it — and no target callback reports\n * that, because a generated region carries no target attribute. `subtree` stays\n * off so writing a message inside a region does not re-enter this pass.\n */\n readonly #hostWatch = new MutationObserver(() => {\n this.#reconcile.schedule();\n });\n\n /** Rewinds to an announceable initial state for the snapshot; see the remarks. */\n readonly #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());\n\n /**\n * The one timer a region may have outstanding — its dedupe re-set, then its\n * auto-clear. Held weakly so a swapped-out target is not retained; a leftover\n * id is harmless because {@link SafeTimeout.clear} no-ops on an id it does not\n * own.\n */\n readonly #pending = new WeakMap<HTMLElement, number>();\n\n /**\n * Guards against handling the same CustomEvent twice. An event dispatched on\n * the controller element with `bubbles: true` reaches both the element and the\n * `window` listener; this WeakSet ensures it announces only once.\n */\n readonly #handled = new WeakSet<Event>();\n\n /** Receives programmatic announcements at the element or bubbled to `window`. */\n readonly #onAnnounceEvent = (event: Event): void => {\n if (this.#handled.has(event)) return;\n this.#handled.add(event);\n const detail = (event as CustomEvent<unknown>).detail;\n const message = this.#messageFromDetail(detail);\n if (!message) return;\n this.#announce(message, this.#assertiveFromDetail(detail));\n };\n\n override connect(): void {\n this.#reconcile.activate();\n this.#beforeCache.activate();\n // Materialise any missing region now, so it is in the accessibility tree\n // before the first message rather than appearing with it.\n this.#reconcileRegions();\n this.#hostWatch.observe(this.element, { childList: true });\n this.element.addEventListener(\"stimeo--announcer:announce\", this.#onAnnounceEvent);\n window.addEventListener(\"stimeo--announcer:announce\", this.#onAnnounceEvent);\n }\n\n override disconnect(): void {\n this.#reconcile.cancel();\n this.#hostWatch.disconnect();\n this.#beforeCache.deactivate();\n this.element.removeEventListener(\"stimeo--announcer:announce\", this.#onAnnounceEvent);\n window.removeEventListener(\"stimeo--announcer:announce\", this.#onAnnounceEvent);\n this.#timers.clearAll();\n this.#queues.clear();\n this.#draining.clear();\n this.#removeGenerated();\n }\n\n /** Retires the stand-in once the consumer supplies a polite region. */\n politeTargetConnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Materialises a stand-in once the consumer's polite region goes away. */\n politeTargetDisconnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Retires the stand-in once the consumer supplies an assertive region. */\n assertiveTargetConnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Materialises a stand-in once the consumer's assertive region goes away. */\n assertiveTargetDisconnected(): void {\n this.#reconcile.schedule();\n }\n\n /**\n * Brings the region set back to exactly one region per politeness and reports\n * whether anything had to be created.\n *\n * A region created here is not written to in the same task: assistive tech\n * reports changes to regions it already knows about, so {@link drain} waits a\n * task whenever this says a region is new.\n */\n #reconcileRegions(): boolean {\n let created = false;\n for (const level of LEVELS) {\n if (this.#hasTargetFor(level)) {\n // The consumer owns this politeness now; a stand-in would be a second\n // region for it, and an empty one nothing ever writes to.\n const generated = this.#generated.get(level);\n if (generated) {\n generated.remove();\n this.#generated.delete(level);\n }\n continue;\n }\n const existing = this.#generated.get(level);\n if (existing?.isConnected) continue;\n this.#generated.set(level, this.#createRegion(level));\n created = true;\n }\n return created;\n }\n\n /** Whether the consumer supplied a target for `level`. */\n #hasTargetFor(level: Level): boolean {\n return level === \"assertive\" ? this.hasAssertiveTarget : this.hasPoliteTarget;\n }\n\n /** Builds a visually hidden live region for `level` and attaches it. */\n #createRegion(level: Level): HTMLElement {\n const region = document.createElement(\"div\");\n region.setAttribute(\"aria-live\", level);\n region.setAttribute(\"aria-atomic\", \"true\");\n visuallyHide(region);\n this.element.appendChild(region);\n return region;\n }\n\n /**\n * Removes and forgets every region this controller generated. Authored targets\n * belong to the consumer and are left untouched. Forgetting them is what keeps\n * the live page working after a snapshot rewind: the next announcement finds an\n * empty map and materialises a fresh region.\n */\n #removeGenerated(): void {\n for (const region of this.#generated.values()) {\n region.remove();\n }\n this.#generated.clear();\n }\n\n /**\n * Announces a message. Reads the text from a Stimulus action param\n * (`message`, plus optional `assertive`) for attribute-only triggers, falling\n * back to a CustomEvent `detail` when the same handler is wired to an event.\n * An empty/non-string message is ignored so untrusted payloads cannot blank\n * the region.\n */\n announce(event: Event): void {\n const params = (event as { params?: Record<string, unknown> }).params;\n const fromParam = params?.message;\n const message =\n typeof fromParam === \"string\" && fromParam.length > 0\n ? fromParam\n : this.#messageFromDetail((event as CustomEvent<unknown>).detail);\n if (!message) return;\n\n const assertive =\n params?.assertive === true ||\n this.#assertiveFromDetail((event as CustomEvent<unknown>).detail);\n this.#announce(message, assertive);\n }\n\n /**\n * Queues `message` for its politeness and arms the drain.\n *\n * Queuing is what makes a burst audible: assistive tech announces the changes it\n * observes, so several messages written into one region within a single task are\n * one change and only the last is read.\n */\n #announce(message: string, assertive: boolean): void {\n const level: Level = assertive ? \"assertive\" : \"polite\";\n const queue = this.#queues.get(level);\n if (queue) {\n queue.push(message);\n } else {\n this.#queues.set(level, [message]);\n }\n this.#scheduleDrain(level);\n }\n\n /** Arms one drain pass for `level`; further messages ride the pass already armed. */\n #scheduleDrain(level: Level): void {\n if (this.#draining.has(level)) return;\n this.#draining.add(level);\n this.#timers.set(() => {\n this.#draining.delete(level);\n this.#drain(level);\n }, 0);\n }\n\n /**\n * Writes one queued message, then arms the next pass while the queue holds more.\n *\n * Two steps take a whole pass without consuming the message: materialising a\n * region (it has to be in the accessibility tree before the text arrives) and\n * emptying a region that already holds this exact text (an unchanged node is not\n * re-read, so `dedupeReannounce` clears first and writes on the following pass).\n */\n #drain(level: Level): void {\n const queue = this.#queues.get(level);\n const message = queue?.[0];\n if (queue === undefined || message === undefined) return;\n\n if (this.#reconcileRegions()) {\n this.#scheduleDrain(level);\n return;\n }\n\n const region = this.#regionFor(level);\n if (this.dedupeReannounceValue && region.textContent === message) {\n this.#cancelPending(region);\n region.textContent = \"\";\n this.#scheduleDrain(level);\n return;\n }\n\n queue.shift();\n this.#cancelPending(region);\n region.textContent = message;\n this.#scheduleClear(region, message);\n if (queue.length > 0) this.#scheduleDrain(level);\n }\n\n /** Clears the region after `clearAfter` ms, unless a newer message replaced it. */\n #scheduleClear(region: HTMLElement, message: string): void {\n if (this.clearAfterValue <= 0) return;\n this.#schedule(\n region,\n () => {\n if (region.textContent === message) region.textContent = \"\";\n },\n this.clearAfterValue,\n );\n }\n\n /**\n * Arms `region`'s single pending timer. Callers reach here with the slot\n * already free — `#announce` releases it, and a fired timer clears its own\n * entry below — so this does not cancel again.\n */\n #schedule(region: HTMLElement, callback: () => void, delay: number): void {\n const id = this.#timers.set(() => {\n // Drop the id as it fires: platform timer ids are recycled, and a stale\n // one left here could later cancel the *other* region's live timer.\n this.#pending.delete(region);\n callback();\n }, delay);\n this.#pending.set(region, id);\n }\n\n /** Releases `region`'s pending timer, if it has one. */\n #cancelPending(region: HTMLElement): void {\n // `SafeTimeout.clear` ignores an id it does not own, so the \"no pending\n // timer\" case needs no branch of its own.\n this.#timers.clear(this.#pending.get(region) ?? -1);\n this.#pending.delete(region);\n }\n\n /**\n * Resolves the live region for a politeness level.\n *\n * The remembered stand-in is used only while it is still in the document: a morph\n * can drop it, and writing into the detached node would announce nothing at all.\n */\n #regionFor(level: Level): HTMLElement {\n if (level === \"assertive\" && this.hasAssertiveTarget) return this.assertiveTarget;\n if (level === \"polite\" && this.hasPoliteTarget) return this.politeTarget;\n\n const existing = this.#generated.get(level);\n if (existing?.isConnected) return existing;\n\n const region = this.#createRegion(level);\n this.#generated.set(level, region);\n return region;\n }\n\n /**\n * Restores the announceable initial state for the snapshot Turbo is about to\n * take: queued and displayed messages go, generated regions go, and the live\n * page — which keeps running when a visit is aborted — gets its regions back on\n * the next task, after the clone.\n */\n #rewindForCache(): void {\n this.#queues.clear();\n this.#draining.clear();\n this.#timers.clearAll();\n for (const level of LEVELS) {\n if (this.#hasTargetFor(level)) {\n const target = level === \"assertive\" ? this.assertiveTarget : this.politeTarget;\n target.textContent = \"\";\n }\n }\n this.#removeGenerated();\n this.#timers.set(() => this.#reconcileRegions(), 0);\n }\n\n /** Extracts a non-empty string `message` from a CustomEvent detail, else null. */\n #messageFromDetail(detail: unknown): string | null {\n if (detail && typeof detail === \"object\" && \"message\" in detail) {\n const value = (detail as Record<string, unknown>).message;\n if (typeof value === \"string\" && value.length > 0) return value;\n }\n return null;\n }\n\n /** Reads an `assertive === true` flag from a CustomEvent detail (default polite). */\n #assertiveFromDetail(detail: unknown): boolean {\n return (\n !!detail &&\n typeof detail === \"object\" &&\n (detail as Record<string, unknown>).assertive === true\n );\n }\n}\n\n/**\n * Applies the canonical visually-hidden (\"sr-only\") inline style to a generated\n * live region so its text is announced without being seen. Inline so the library\n * stays self-contained when the consumer provides no target/CSS of its own.\n *\n * Pure (no `this`); exported for direct unit testing.\n */\nexport function visuallyHide(node: HTMLElement): void {\n const { style } = node;\n style.position = \"absolute\";\n style.width = \"1px\";\n style.height = \"1px\";\n style.margin = \"-1px\";\n style.padding = \"0\";\n style.border = \"0\";\n style.overflow = \"hidden\";\n style.clip = \"rect(0 0 0 0)\";\n style.clipPath = \"inset(50%)\";\n style.whiteSpace = \"nowrap\";\n}\n"]}
|
|
@@ -7,12 +7,11 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
7
7
|
* Markup contract (identifier: `stimeo--aspect-ratio`):
|
|
8
8
|
* <div data-controller="stimeo--aspect-ratio"
|
|
9
9
|
* data-stimeo--aspect-ratio-ratio-value="16/9">
|
|
10
|
-
* <img src="/cover.jpg" alt="Cover"
|
|
11
|
-
* data-stimeo--aspect-ratio-target="content" />
|
|
10
|
+
* <img src="/cover.jpg" alt="Cover" />
|
|
12
11
|
* </div>
|
|
13
12
|
*
|
|
14
|
-
* Supplies the requested ratio as the `--stimeo
|
|
15
|
-
* the host so consumer CSS can drive the box (`aspect-ratio: var(--stimeo
|
|
13
|
+
* Supplies the requested ratio as the `--stimeo--aspect-ratio` custom property on
|
|
14
|
+
* the host so consumer CSS can drive the box (`aspect-ratio: var(--stimeo--aspect-ratio)`)
|
|
16
15
|
* or a padding-hack fallback. The drawing itself — `aspect-ratio`, `object-fit`,
|
|
17
16
|
* cropping — stays in the consumer's stylesheet.
|
|
18
17
|
*
|
|
@@ -24,7 +23,6 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
24
23
|
*/
|
|
25
24
|
declare class AspectRatioController extends Controller<HTMLElement> {
|
|
26
25
|
#private;
|
|
27
|
-
static targets: string[];
|
|
28
26
|
static values: {
|
|
29
27
|
ratio: {
|
|
30
28
|
type: StringConstructor;
|
|
@@ -1,33 +1,41 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
3
|
// src/controllers/aspect_ratio_controller.ts
|
|
4
|
+
var DEFAULT_RATIO = "1 / 1";
|
|
5
|
+
var RATIO_PROPERTY = "--stimeo--aspect-ratio";
|
|
6
|
+
var CSS_NUMBER_PATTERN = /^[+-]?(?:\d+|\d*\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
4
7
|
var AspectRatioController = class extends Controller {
|
|
5
|
-
static targets = ["content"];
|
|
6
8
|
static values = {
|
|
7
9
|
ratio: { type: String, default: "1/1" }
|
|
8
10
|
};
|
|
9
11
|
/** Applies the ratio on connect and whenever the value changes. */
|
|
10
12
|
ratioValueChanged() {
|
|
11
|
-
this.element.style.setProperty(
|
|
13
|
+
this.element.style.setProperty(RATIO_PROPERTY, this.#normalizeRatio(this.ratioValue));
|
|
12
14
|
}
|
|
13
15
|
/**
|
|
14
16
|
* Normalizes a ratio string to a valid CSS `<ratio>`:
|
|
15
|
-
* - `"16/9"` / `"16 / 9"` → `"16 / 9"` (both parts must be positive numbers)
|
|
16
|
-
* - `"1.5"` → `"1.5"` (a bare positive number)
|
|
17
|
+
* - `"16/9"` / `"16 / 9"` → `"16 / 9"` (both parts must be positive CSS numbers)
|
|
18
|
+
* - `"1.5"` → `"1.5"` (a bare positive CSS number)
|
|
17
19
|
* - anything else → `"1 / 1"` (the default), so the custom property is always valid.
|
|
18
20
|
*/
|
|
19
21
|
#normalizeRatio(raw) {
|
|
20
22
|
const value = raw.trim();
|
|
21
23
|
if (value.includes("/")) {
|
|
22
|
-
const
|
|
23
|
-
if (
|
|
24
|
-
|
|
24
|
+
const parts = value.split("/");
|
|
25
|
+
if (parts.length !== 2) return DEFAULT_RATIO;
|
|
26
|
+
const width = this.#parsePositiveNumber(parts[0] ?? "");
|
|
27
|
+
const height = this.#parsePositiveNumber(parts[1] ?? "");
|
|
28
|
+
return width !== void 0 && height !== void 0 ? `${width} / ${height}` : DEFAULT_RATIO;
|
|
25
29
|
}
|
|
26
|
-
const single =
|
|
27
|
-
return
|
|
30
|
+
const single = this.#parsePositiveNumber(value);
|
|
31
|
+
return single === void 0 ? DEFAULT_RATIO : String(single);
|
|
28
32
|
}
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
/** Parses one complete CSS `<number>` token and rejects units or trailing syntax. */
|
|
34
|
+
#parsePositiveNumber(raw) {
|
|
35
|
+
const token = raw.trim();
|
|
36
|
+
if (!CSS_NUMBER_PATTERN.test(token)) return void 0;
|
|
37
|
+
const value = Number(token);
|
|
38
|
+
return Number.isFinite(value) && value > 0 ? value : void 0;
|
|
31
39
|
}
|
|
32
40
|
};
|
|
33
41
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/controllers/aspect_ratio_controller.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"sources":["../../src/controllers/aspect_ratio_controller.ts"],"names":[],"mappings":";;;AAEA,IAAM,aAAA,GAAgB,OAAA;AACtB,IAAM,cAAA,GAAiB,wBAAA;AACvB,IAAM,kBAAA,GAAqB,0CAAA;AAuBpB,IAAM,qBAAA,GAAN,cAAoC,UAAA,CAAwB;AAAA,EACjE,OAAgB,MAAA,GAAS;AAAA,IACvB,KAAA,EAAO,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,KAAA;AAAM,GACxC;AAAA;AAAA,EAKA,iBAAA,GAA0B;AACxB,IAAA,IAAA,CAAK,OAAA,CAAQ,MAAM,WAAA,CAAY,cAAA,EAAgB,KAAK,eAAA,CAAgB,IAAA,CAAK,UAAU,CAAC,CAAA;AAAA,EACtF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,gBAAgB,GAAA,EAAqB;AACnC,IAAA,MAAM,KAAA,GAAQ,IAAI,IAAA,EAAK;AACvB,IAAA,IAAI,KAAA,CAAM,QAAA,CAAS,GAAG,CAAA,EAAG;AACvB,MAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,KAAA,CAAM,GAAG,CAAA;AAC7B,MAAA,IAAI,KAAA,CAAM,MAAA,KAAW,CAAA,EAAG,OAAO,aAAA;AAE/B,MAAA,MAAM,QAAQ,IAAA,CAAK,oBAAA,CAAqB,KAAA,CAAM,CAAC,KAAK,EAAE,CAAA;AACtD,MAAA,MAAM,SAAS,IAAA,CAAK,oBAAA,CAAqB,KAAA,CAAM,CAAC,KAAK,EAAE,CAAA;AACvD,MAAA,OAAO,KAAA,KAAU,UAAa,MAAA,KAAW,MAAA,GAAY,GAAG,KAAK,CAAA,GAAA,EAAM,MAAM,CAAA,CAAA,GAAK,aAAA;AAAA,IAChF;AAEA,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,oBAAA,CAAqB,KAAK,CAAA;AAC9C,IAAA,OAAO,MAAA,KAAW,MAAA,GAAY,aAAA,GAAgB,MAAA,CAAO,MAAM,CAAA;AAAA,EAC7D;AAAA;AAAA,EAGA,qBAAqB,GAAA,EAAiC;AACpD,IAAA,MAAM,KAAA,GAAQ,IAAI,IAAA,EAAK;AACvB,IAAA,IAAI,CAAC,kBAAA,CAAmB,IAAA,CAAK,KAAK,GAAG,OAAO,MAAA;AAE5C,IAAA,MAAM,KAAA,GAAQ,OAAO,KAAK,CAAA;AAC1B,IAAA,OAAO,OAAO,QAAA,CAAS,KAAK,CAAA,IAAK,KAAA,GAAQ,IAAI,KAAA,GAAQ,MAAA;AAAA,EACvD;AACF","file":"aspect_ratio_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\nconst DEFAULT_RATIO = \"1 / 1\";\nconst RATIO_PROPERTY = \"--stimeo--aspect-ratio\";\nconst CSS_NUMBER_PATTERN = /^[+-]?(?:\\d+|\\d*\\.\\d+)(?:[eE][+-]?\\d+)?$/;\n\n/**\n * Headless **Aspect Ratio** helper. No APG pattern — a pure layout utility with\n * no role or state.\n *\n * Markup contract (identifier: `stimeo--aspect-ratio`):\n * <div data-controller=\"stimeo--aspect-ratio\"\n * data-stimeo--aspect-ratio-ratio-value=\"16/9\">\n * <img src=\"/cover.jpg\" alt=\"Cover\" />\n * </div>\n *\n * Supplies the requested ratio as the `--stimeo--aspect-ratio` custom property on\n * the host so consumer CSS can drive the box (`aspect-ratio: var(--stimeo--aspect-ratio)`)\n * or a padding-hack fallback. The drawing itself — `aspect-ratio`, `object-fit`,\n * cropping — stays in the consumer's stylesheet.\n *\n * @remarks\n * Behavior only. The value accepts the CSS `<ratio>` forms `\"16/9\"` and a bare\n * number string (`\"1.5\"`); it is normalized to `\"w / h\"` (or the number) and an\n * unparseable value falls back to the default `1 / 1` rather than writing garbage\n * into the custom property. The reflection re-runs when the value changes.\n */\nexport class AspectRatioController extends Controller<HTMLElement> {\n static override values = {\n ratio: { type: String, default: \"1/1\" },\n };\n\n declare ratioValue: string;\n\n /** Applies the ratio on connect and whenever the value changes. */\n ratioValueChanged(): void {\n this.element.style.setProperty(RATIO_PROPERTY, this.#normalizeRatio(this.ratioValue));\n }\n\n /**\n * Normalizes a ratio string to a valid CSS `<ratio>`:\n * - `\"16/9\"` / `\"16 / 9\"` → `\"16 / 9\"` (both parts must be positive CSS numbers)\n * - `\"1.5\"` → `\"1.5\"` (a bare positive CSS number)\n * - anything else → `\"1 / 1\"` (the default), so the custom property is always valid.\n */\n #normalizeRatio(raw: string): string {\n const value = raw.trim();\n if (value.includes(\"/\")) {\n const parts = value.split(\"/\");\n if (parts.length !== 2) return DEFAULT_RATIO;\n\n const width = this.#parsePositiveNumber(parts[0] ?? \"\");\n const height = this.#parsePositiveNumber(parts[1] ?? \"\");\n return width !== undefined && height !== undefined ? `${width} / ${height}` : DEFAULT_RATIO;\n }\n\n const single = this.#parsePositiveNumber(value);\n return single === undefined ? DEFAULT_RATIO : String(single);\n }\n\n /** Parses one complete CSS `<number>` token and rejects units or trailing syntax. */\n #parsePositiveNumber(raw: string): number | undefined {\n const token = raw.trim();\n if (!CSS_NUMBER_PATTERN.test(token)) return undefined;\n\n const value = Number(token);\n return Number.isFinite(value) && value > 0 ? value : undefined;\n }\n}\n"]}
|
|
@@ -13,25 +13,28 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
13
13
|
* <span aria-hidden="true" hidden data-stimeo--avatar-target="fallback">JD</span>
|
|
14
14
|
* </span>
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
16
|
+
* The `image` and `fallback` targets are optional. A present `src` Value owns the
|
|
17
|
+
* current image's `src`, including an empty Value that explicitly clears it. When
|
|
18
|
+
* the Value is absent, a directly authored `<img src>` is used instead. Runtime
|
|
19
|
+
* Value changes, direct `src` changes, and target replacements all repaint.
|
|
20
|
+
*
|
|
21
|
+
* The accessible name lives on the container (`role="img"` + `aria-label`); the
|
|
22
|
+
* inner image and fallback are `aria-hidden` so assistive technology reads the
|
|
23
|
+
* name once in every visual state.
|
|
21
24
|
*
|
|
22
25
|
* @remarks
|
|
23
26
|
* Behavior only — shape, size, and colour are the consumer's CSS. Initials or
|
|
24
|
-
* colour generation are out of scope:
|
|
25
|
-
* loading
|
|
27
|
+
* colour generation are out of scope: fallback content comes from markup. The
|
|
28
|
+
* `empty`, `loading`, `loaded`, and `error` phases are exposed on `data-state`.
|
|
29
|
+
* A native error from the current image dispatches `stimeo--avatar:error` with
|
|
30
|
+
* `{ src: string }`; reconnecting or reconciling an already-broken image is
|
|
31
|
+
* silent because no new failure event occurred.
|
|
26
32
|
*/
|
|
27
33
|
declare class AvatarController extends Controller<HTMLElement> {
|
|
28
34
|
#private;
|
|
29
35
|
static targets: string[];
|
|
30
36
|
static values: {
|
|
31
|
-
src:
|
|
32
|
-
type: StringConstructor;
|
|
33
|
-
default: string;
|
|
34
|
-
};
|
|
37
|
+
src: StringConstructor;
|
|
35
38
|
};
|
|
36
39
|
static actions: readonly ["onError", "onLoad"];
|
|
37
40
|
static events: readonly ["error"];
|
|
@@ -40,11 +43,29 @@ declare class AvatarController extends Controller<HTMLElement> {
|
|
|
40
43
|
readonly hasImageTarget: boolean;
|
|
41
44
|
readonly hasFallbackTarget: boolean;
|
|
42
45
|
srcValue: string;
|
|
46
|
+
/** Starts retained-DOM observation and silently derives the current phase. */
|
|
43
47
|
connect(): void;
|
|
44
|
-
/**
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
+
/** Cancels asynchronous work while retaining the last materialized visual state. */
|
|
49
|
+
disconnect(): void;
|
|
50
|
+
/** Repaints when application code or a Turbo morph changes the source Value. */
|
|
51
|
+
srcValueChanged(): void;
|
|
52
|
+
/** Applies the current source and phase to an image inserted or replaced at runtime. */
|
|
53
|
+
imageTargetConnected(): void;
|
|
54
|
+
/** Returns attributes borrowed from an image that leaves this live avatar. */
|
|
55
|
+
imageTargetDisconnected(image: HTMLImageElement): void;
|
|
56
|
+
/** Applies the current phase to a fallback inserted or replaced at runtime. */
|
|
57
|
+
fallbackTargetConnected(): void;
|
|
58
|
+
/** Returns authored visibility when a fallback leaves this live avatar. */
|
|
59
|
+
fallbackTargetDisconnected(fallback: HTMLElement): void;
|
|
60
|
+
/** Reveals the current image after its native `load` event. */
|
|
61
|
+
onLoad(event: Event): void;
|
|
62
|
+
/**
|
|
63
|
+
* Reveals the fallback after the current image's native `error` event.
|
|
64
|
+
*
|
|
65
|
+
* Dispatches `stimeo--avatar:error` with `{ src: string }`, where `src` is
|
|
66
|
+
* the attempted attribute value. Empty/stale/detached image events are ignored.
|
|
67
|
+
*/
|
|
68
|
+
onError(event: Event): void;
|
|
48
69
|
}
|
|
49
70
|
|
|
50
71
|
export { AvatarController };
|