stimulus_plumbers 0.4.5 → 0.4.8

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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/README.md +16 -0
  4. data/docs/component/button.md +1 -1
  5. data/docs/component/calendar.md +11 -4
  6. data/docs/component/card.md +14 -12
  7. data/docs/component/combobox.md +2 -2
  8. data/docs/component/form.md +32 -6
  9. data/docs/component/list.md +2 -2
  10. data/docs/component/plumber.md +22 -0
  11. data/docs/component/theme.md +15 -2
  12. data/docs/component/timeline.md +2 -2
  13. data/docs/guide.md +27 -0
  14. data/lib/generators/stimulus_plumbers/install/install_generator.rb +29 -0
  15. data/lib/stimulus_plumbers/components/button.rb +1 -1
  16. data/lib/stimulus_plumbers/components/card.rb +1 -1
  17. data/lib/stimulus_plumbers/components/combobox.rb +1 -1
  18. data/lib/stimulus_plumbers/components/link.rb +1 -1
  19. data/lib/stimulus_plumbers/components/list/item.rb +1 -1
  20. data/lib/stimulus_plumbers/components/timeline/event.rb +1 -1
  21. data/lib/stimulus_plumbers/engine.rb +4 -0
  22. data/lib/stimulus_plumbers/generators/css_entrypoint.rb +74 -0
  23. data/lib/stimulus_plumbers/generators/tokens_directive.rb +20 -0
  24. data/lib/stimulus_plumbers/plumber/slots.rb +10 -3
  25. data/lib/stimulus_plumbers/version.rb +1 -1
  26. data/lib/tasks/stimulus_plumbers.rake +18 -0
  27. data/vendor/ARIA.md +88 -0
  28. data/vendor/controller/docs/calendar.md +215 -0
  29. data/vendor/controller/docs/clipboard.md +61 -0
  30. data/vendor/controller/docs/combobox.md +242 -0
  31. data/vendor/controller/docs/dismisser.md +39 -0
  32. data/vendor/controller/docs/flipper.md +46 -0
  33. data/vendor/controller/docs/input-clearable.md +46 -0
  34. data/vendor/controller/docs/input-formatter.md +90 -0
  35. data/vendor/controller/docs/modal.md +60 -0
  36. data/vendor/controller/docs/panner.md +24 -0
  37. data/vendor/controller/docs/popover.md +113 -0
  38. data/vendor/controller/docs/timeline.md +84 -0
  39. data/vendor/controller/guide.md +16 -0
  40. metadata +20 -2
  41. /data/vendor/{controllers.manifest.json → controller/manifest.json} +0 -0
data/vendor/ARIA.md ADDED
@@ -0,0 +1,88 @@
1
+ # WCAG 2.1 AA / ARIA Reference
2
+
3
+ ## Core Criteria (all components)
4
+
5
+ | Criterion | Level | Rule |
6
+ |-----------|-------|------|
7
+ | 1.3.1 Info & Relationships | A | Convey structure via semantic HTML or ARIA roles, not visual styling alone |
8
+ | 1.4.1 Use of Color | A | Never use color as the only means to convey information |
9
+ | 1.4.3 Contrast (text) | AA | 4.5:1 normal text, 3:1 large text (18pt / 14pt bold) |
10
+ | 1.4.11 Non-text Contrast | AA | 3:1 for UI component boundaries and state indicators |
11
+ | 2.1.1 Keyboard | A | All functionality operable via keyboard; no mouse-only interactions |
12
+ | 2.1.2 No Keyboard Trap | A | Focus must be escapable from any component (Escape or documented key) |
13
+ | 2.4.3 Focus Order | A | Focus sequence must be logical and predictable |
14
+ | 2.4.7 Focus Visible | AA | Keyboard focus indicator must be visible |
15
+ | 4.1.2 Name, Role, Value | A | All UI components must have accessible name, role, and state via ARIA or native semantics; state must stay in sync dynamically (e.g. `aria-expanded`, `aria-selected`, `aria-checked`) |
16
+ | 4.1.3 Status Messages | AA | Status/error messages announced via `role="status"` or `aria-live` without focus move |
17
+
18
+ ## JS Keyboard Navigation Patterns
19
+
20
+ Two helper classes handle keyboard navigation in controllers — see [`stimulus-plumbers/docs/accessibility/design.md`](stimulus-plumbers/docs/accessibility/design.md) for full API.
21
+
22
+ | Pattern | Class | When to use |
23
+ | --- | --- | --- |
24
+ | Roving tabindex | `RovingTabIndex` | Disclosure widgets, trees, toolbars — focus moves between items |
25
+ | Managed focus / listbox | `ListboxNavigation` | Combobox listbox, `role="listbox"` — input keeps focus, `aria-selected` moves |
26
+
27
+ ## Component-Specific Patterns (APG)
28
+
29
+ #### Modal (`modal_controller`)
30
+ - `role="dialog"`, `aria-modal="true"`, `aria-labelledby` pointing to heading
31
+ - Focus moves into dialog on open; returns to trigger on close
32
+ - Focus trapped inside — Tab/Shift+Tab cycle within; Escape closes
33
+
34
+ #### Popover (`popover_controller`)
35
+ - `role="dialog"` or `role="tooltip"` depending on interactivity
36
+ - Trigger: `<button>` with `aria-haspopup="dialog"` and `aria-expanded="false"` initially
37
+ - `aria-expanded` toggled to `"true"` / `"false"` by the controller via the `trigger` Stimulus target
38
+ - `aria-controls` linking trigger to panel id is recommended but optional
39
+ - Escape closes and returns focus to trigger
40
+
41
+ #### Combobox (`input_combobox_controller`, `combobox/`)
42
+ - Trigger: `<input role="combobox">` with `aria-haspopup` (`listbox`/`dialog`) and `aria-controls` referencing the **popup element** (the `role="listbox"`/`role="dialog"`)
43
+ - `role="listbox"` permits only `option`/`group` children (`aria-required-children`, WCAG 1.3.1). Status messages (loading, "No results") must be `role="status"`/`aria-live` **siblings of the listbox, never children of it** — for typeahead the listbox is nested in a wrapper panel so the status regions can sit beside it
44
+ - Status/loading regions inside the popover panel must stay **non-focusable** (the popover moves focus into the panel on open)
45
+
46
+ #### Calendar / Date Picker (`calendar-month`, `combobox-date`)
47
+ - Grid: `role="grid"`, `role="row"`, `role="gridcell"`
48
+ - Navigation buttons: `aria-label="Previous Month"` / `"Next Month"` (via `Combobox::Date::Navigation`)
49
+ - Selected date: `aria-selected="true"`; today: `aria-current="date"`
50
+ - Disabled dates: `aria-disabled="true"`, `tabindex="-1"`
51
+ - Three views: month (days grid), year (months grid), decade (years grid); zoom out via `combobox-date#zoomOut`
52
+ - Arrow keys navigate cells; Enter/Space select; Escape closes picker
53
+
54
+ #### Form Fields (`form/`, `form-field_controller`)
55
+ - Every input must have a visible `<label>` via `for`/`id` or `aria-labelledby`
56
+ - Required fields: `required` attribute + `aria-required="true"`
57
+ - Invalid fields: `aria-invalid="true"` + `aria-describedby` pointing to error message
58
+ - Error message element: `role="alert"` or `aria-live="polite"` so it's announced
59
+
60
+ #### Password Reveal (`password_reveal_controller`)
61
+ - Toggle button: `aria-label` describes action ("Show password" / "Hide password")
62
+ - Or: `aria-pressed` on toggle button
63
+
64
+ #### Flipper / Visibility / Dismisser
65
+ - Trigger: `aria-expanded="true/false"` when toggling a region
66
+ - Controlled region: `aria-hidden="true"` when collapsed (or removed from DOM)
67
+ - `aria-controls` links trigger to region id
68
+
69
+ #### Button (`button/renderer`)
70
+ - Use `<button>` (not `<div>` / `<a>`) for actions
71
+ - Icon-only buttons must have `aria-label` or visually-hidden text
72
+ - Disabled: `disabled` attribute (not `aria-disabled` alone) unless intentionally focusable
73
+
74
+ #### List (`sp_list`, `sp_list_item`)
75
+ - Static list: `role="list"` + `role="listitem"` (explicitly set to preserve semantics when CSS resets strip list role)
76
+ - Active item: `aria-current="page"` on `<a>` links; `aria-current="true"` on `<button>` items — the value differs by element type per the ARIA spec
77
+
78
+ #### Avatar / Card / Icon
79
+ - Decorative images/icons: `aria-hidden="true"` or `alt=""`
80
+ - Meaningful images: descriptive `alt` text
81
+
82
+ #### Timeline (`timeline_controller`)
83
+ - List: `<ol>` (ordered) for chronological events; static timelines need no ARIA additions beyond semantic HTML
84
+ - Each item: `<li>`; timestamp displayed via `<time datetime="YYYY-MM-DD">`
85
+ - Indicators (dots, icons, avatars): decorative — always `aria-hidden="true"`
86
+ - Expandable items: trigger `<button>` with `aria-expanded="false/true"` + `aria-controls` → detail element id; detail has `hidden` attribute toggled by controller
87
+ - Trigger lives inside `<h3>` (`<h3><button aria-expanded>Title</button></h3>`) — WAI-ARIA Accordion pattern
88
+ - Keyboard (interactive): Up/Down arrows move focus between item triggers; Home/End jump to first/last trigger; Enter/Space toggle expansion (native button behaviour)
@@ -0,0 +1,215 @@
1
+ # calendar-month
2
+
3
+ Renders an accessible calendar grid for a given month. Driven by the `Calendar` plumber, which provides navigation, disabled dates, and range constraints.
4
+
5
+ ## Targets
6
+
7
+ | Target | Description |
8
+ | ------------- | ------------------------------------ |
9
+ | `daysOfWeek` | Container for the weekday header row |
10
+ | `daysOfMonth` | Container for the day grid rows |
11
+
12
+ ## Classes
13
+
14
+ | Class | Description |
15
+ | ----------------- | ----------------------------------------- |
16
+ | `dayOfWeek` | Applied to each weekday header cell |
17
+ | `dayOfMonth` | Applied to each day cell |
18
+ | `dayOfOtherMonth` | Applied to day cells from adjacent months |
19
+ | `row` | Applied to each week row |
20
+
21
+ ## Values
22
+
23
+ | Value | Type | Default | Description |
24
+ | ------------------ | ------- | ------------- | -------------------------------------------------------------------------------------- |
25
+ | `year` | Number | — | Year being displayed |
26
+ | `month` | Number | — | Month being displayed (0-indexed) |
27
+ | `since` | String | `""` | Earliest selectable date (ISO string) |
28
+ | `till` | String | `""` | Latest selectable date (ISO string) |
29
+ | `locales` | Array | `["default"]` | `Intl.DateTimeFormat` locale(s) |
30
+ | `weekdayFormat` | String | `"short"` | Weekday header format: `"short"` \| `"long"` \| `"narrow"` |
31
+ | `dayFormat` | String | `"numeric"` | Day number format |
32
+ | `daysOfOtherMonth` | Boolean | `false` | Show overflow days from adjacent months |
33
+ | `today` | String | `""` | Override the "today" marker (ISO date string); defaults to system date |
34
+ | `selected` | String | `""` | Currently selected date (ISO string); sets `aria-selected="true"` on the matching cell |
35
+
36
+ ## Calendar plumber options (programmatic only)
37
+
38
+ These options are not exposed as Stimulus values and must be passed via a subclass or custom controller:
39
+
40
+ | Option | Description |
41
+ | ------------------ | -------------------------------------------- |
42
+ | `firstDayOfWeek` | `0` = Sunday, `1` = Monday, … (default `0`) |
43
+ | `disabledDates` | Array of ISO date strings to disable |
44
+ | `disabledWeekdays` | Array of weekday names or numbers to disable |
45
+ | `disabledDays` | Array of day-of-month numbers to disable |
46
+ | `disabledMonths` | Array of month names or numbers to disable |
47
+
48
+ ## Standalone usage
49
+
50
+ ```html
51
+ <div
52
+ data-controller="calendar-month"
53
+ data-calendar-month-year-value="2024"
54
+ data-calendar-month-month-value="1"
55
+ data-calendar-month-locales-value='["en-US"]'
56
+ role="grid"
57
+ >
58
+ <div data-calendar-month-target="daysOfWeek"></div>
59
+ <div role="rowgroup" data-calendar-month-target="daysOfMonth"></div>
60
+ </div>
61
+ ```
62
+
63
+ ## Rails helper
64
+
65
+ ```ruby
66
+ module ApplicationHelper
67
+ include StimulusPlumbers::Helpers::CalendarHelper
68
+ end
69
+ ```
70
+
71
+ ```erb
72
+ <%# Current month %>
73
+ <%= sp_calendar_month %>
74
+
75
+ <%# Navigate to a specific date %>
76
+ <%= sp_calendar_month(date: Date.new(2024, 3, 15)) %>
77
+
78
+ <%# With custom classes %>
79
+ <%= sp_calendar_month(class: "my-calendar") %>
80
+ ```
81
+
82
+ ## Actions
83
+
84
+ | Method | Description |
85
+ | ----------------- | ------------------------------------------------------------------------------------------------------------------ |
86
+ | `select(iso)` | Selects a date by ISO string — sets `selectedValue`, updates `aria-selected`, dispatches `calendar-month:selected` |
87
+ | `navigate(date)` | Navigates to the given `Date` — updates `yearValue`/`monthValue` and re-renders the grid |
88
+ | `step(unit, dir)` | Steps the calendar by `unit` (`'day'`/`'month'`/`'year'`) in `dir` (`1`/`-1`) |
89
+
90
+ Click-handling is wired internally — `calendar-month` handles clicks on its own grid and dispatches `calendar-month:selected` automatically.
91
+
92
+ **Dispatches**
93
+
94
+ | Event | Detail | When |
95
+ | -------------------------- | ---------------- | --------------------------------------------------- |
96
+ | `calendar-month:selecting` | — | On every valid cell click, before date is confirmed |
97
+ | `calendar-month:selected` | `{ epoch, iso }` | After a valid date is parsed from the clicked cell |
98
+ | `calendar-month:navigate` | `{ from, to }` | Before navigation begins (ISO strings) |
99
+ | `calendar-month:navigated` | `{ from, to }` | After navigation completes (ISO strings) |
100
+
101
+ ## Accessibility
102
+
103
+ - Grid uses `role="grid"` → `role="row"` → `role="gridcell"`
104
+ - Weekday headers use `role="columnheader"` with `title` for the long name
105
+ - Today is marked with `aria-current="date"`
106
+ - Disabled dates use `disabled` (buttons) or `aria-disabled="true"` (non-interactive)
107
+ - Selected dates use `aria-selected="true"`
108
+
109
+ ---
110
+
111
+ ## calendar-year
112
+
113
+ Year-view grid controller — renders a 12-month grid and dispatches a `calendar-year:selected` event when a month button is clicked. Pair with `combobox-date` via `data-action` on the orchestrator element.
114
+
115
+ **Targets**
116
+
117
+ | Target | Description |
118
+ | ------ | ------------------------------------------------------- |
119
+ | `grid` | `role="rowgroup"` container the controller renders into |
120
+
121
+ **Values**
122
+
123
+ | Value | Type | Default | Description |
124
+ | ------------- | ------ | ------------- | -------------------------------------------------------------- |
125
+ | `current` | Number | — | Year being displayed |
126
+ | `today` | String | `""` | ISO date string for today marker |
127
+ | `selected` | String | `""` | ISO date string for selected marker |
128
+ | `since` | String | `""` | Earliest selectable date (ISO string) |
129
+ | `till` | String | `""` | Latest selectable date (ISO string) |
130
+ | `locales` | Array | `["default"]` | `Intl.DateTimeFormat` locale(s) |
131
+ | `monthFormat` | String | `"short"` | Month button label format: `"short"` \| `"long"` \| `"narrow"` |
132
+
133
+ **Dispatches**
134
+
135
+ | Event | Detail | When |
136
+ | ------------------------ | ----------- | ------------------------------------------------- |
137
+ | `calendar-year:selected` | `{ month }` | After a valid month button is clicked (1-indexed) |
138
+
139
+ ```html
140
+ <!-- action wired on the combobox-date element -->
141
+ <div
142
+ data-controller="combobox-date"
143
+ data-action="calendar-year:selected->combobox-date#onMonthSelect"
144
+ data-combobox-date-calendar-year-outlet="#year_view"
145
+ >
146
+ <div id="year_view" hidden data-controller="calendar-year" role="grid" aria-label="Year view">
147
+ <div data-calendar-year-target="grid" role="rowgroup"></div>
148
+ </div>
149
+ </div>
150
+ ```
151
+
152
+ ---
153
+
154
+ ## calendar-decade
155
+
156
+ Decade-view grid controller — renders a 12-year grid and dispatches a `calendar-decade:selected` event when a year button is clicked. Pair with `combobox-date` via `data-action` on the orchestrator element.
157
+
158
+ **Targets**
159
+
160
+ | Target | Description |
161
+ | ------ | ------------------------------------------------------- |
162
+ | `grid` | `role="rowgroup"` container the controller renders into |
163
+
164
+ **Values**
165
+
166
+ | Value | Type | Default | Description |
167
+ | ---------- | ------ | ------- | -------------------------------------- |
168
+ | `current` | Number | — | Year within the decade being displayed |
169
+ | `today` | String | `""` | ISO date string for today marker |
170
+ | `selected` | String | `""` | ISO date string for selected marker |
171
+ | `since` | String | `""` | Earliest selectable date (ISO string) |
172
+ | `till` | String | `""` | Latest selectable date (ISO string) |
173
+
174
+ **Dispatches**
175
+
176
+ | Event | Detail | When |
177
+ | -------------------------- | ---------- | ------------------------------------ |
178
+ | `calendar-decade:selected` | `{ year }` | After a valid year button is clicked |
179
+
180
+ ```html
181
+ <!-- action wired on the combobox-date element -->
182
+ <div
183
+ data-controller="combobox-date"
184
+ data-action="calendar-decade:selected->combobox-date#onYearSelect"
185
+ data-combobox-date-calendar-decade-outlet="#decade_view"
186
+ >
187
+ <div id="decade_view" hidden data-controller="calendar-decade" role="grid" aria-label="Decade view">
188
+ <div data-calendar-decade-target="grid" role="rowgroup"></div>
189
+ </div>
190
+ </div>
191
+ ```
192
+
193
+ ---
194
+
195
+ ## calendar-month-selector
196
+
197
+ Thin SSR/Turbo adapter for server-rendered month grids. Attaches click-to-select behaviour to a server-rendered day grid — no targets or values. The server renders each view as a `<turbo-frame>`; this controller handles day selection and emits the same `calendar-month:selected` event as `calendar-month`.
198
+
199
+ Used with `sp_calendar_turbo_month` (Rails). Pair with `combobox-date` exactly as you would `calendar-month`.
200
+
201
+ ---
202
+
203
+ ## calendar-year-selector
204
+
205
+ Thin SSR/Turbo adapter for server-rendered year (month-grid) views. Attaches click-to-select behaviour — no targets or values. Emits `calendar-year:selected` on month click.
206
+
207
+ Used with `sp_calendar_turbo_year` (Rails).
208
+
209
+ ---
210
+
211
+ ## calendar-decade-selector
212
+
213
+ Thin SSR/Turbo adapter for server-rendered decade (year-grid) views. Attaches click-to-select behaviour — no targets or values. Emits `calendar-decade:selected` on year click.
214
+
215
+ Used with `sp_calendar_turbo_decade` (Rails).
@@ -0,0 +1,61 @@
1
+ # clipboard
2
+
3
+ Intercepts native paste events and re-dispatches them as Stimulus events. Also provides a copy action that writes text to the clipboard via the Clipboard API.
4
+
5
+ ## Targets
6
+
7
+ | Target | Description |
8
+ | -------- | ----------------------------------------------------------------------------------- |
9
+ | `source` | Element whose `.value` or `.textContent` is copied when no `text` param is provided |
10
+
11
+ ## Values
12
+
13
+ | Value | Type | Default | Description |
14
+ | ------------- | ------ | -------------- | ---------------------------------------------------------------------------------------------------------------- |
15
+ | `contentType` | String | `"text/plain"` | MIME type to extract from `clipboardData` on paste. Common: `"text/plain"` \| `"text/html"` \| `"text/uri-list"` |
16
+
17
+ ## Methods
18
+
19
+ | Method | Wired via | Description |
20
+ | ---------------- | ----------------- | ---------------------------------------------------------------- |
21
+ | `onPaste(event)` | `paste` DOM event | Event adapter — intercepts paste, dispatches `clipboard:pasted` |
22
+ | `copy(event)` | `data-action` | Action — writes text to clipboard, dispatches `clipboard:copied` |
23
+
24
+ ## Dispatches
25
+
26
+ | Event | Detail | When |
27
+ | ----------------------- | ----------------- | -------------------------------- |
28
+ | `clipboard:pasted` | `{ text, types }` | On successful paste interception |
29
+ | `clipboard:copied` | `{ text }` | On successful clipboard write |
30
+ | `clipboard:copy-failed` | `{ error }` | On clipboard write failure |
31
+
32
+ ## Paste example
33
+
34
+ ```html
35
+ <%# Intercept paste, forward to input-formatter for normalisation %>
36
+ <input
37
+ data-controller="clipboard"
38
+ data-action="paste->clipboard#onPaste clipboard:pasted->input-formatter#onPaste"
39
+ data-clipboard-content-type-value="text/plain"
40
+ />
41
+ ```
42
+
43
+ ## Copy examples
44
+
45
+ ```html
46
+ <%# Copy from a source target %>
47
+ <div data-controller="clipboard">
48
+ <input data-clipboard-target="source" value="text to copy" readonly />
49
+ <button data-action="click->clipboard#copy">Copy</button>
50
+ </div>
51
+
52
+ <%# Copy hardcoded text via param %>
53
+ <button data-controller="clipboard" data-action="click->clipboard#copy" data-clipboard-text-param="https://example.com">
54
+ Copy link
55
+ </button>
56
+ ```
57
+
58
+ ## Notes
59
+
60
+ - `paste` calls `event.preventDefault()` — the original paste is suppressed
61
+ - Use `clipboard:pasted->input-formatter#onPaste` to forward pasted content for formatting
@@ -0,0 +1,242 @@
1
+ # Combobox Controllers
2
+
3
+ The combobox family uses a layered design. `input-combobox` and `input-formatter` live on the wrapper; a picker sub-controller lives inside the popover.
4
+
5
+ ```
6
+ input-combobox + input-formatter ← wrapper
7
+ └── [popover]
8
+ └── combobox-date ← date picker (calendar grid)
9
+ └── combobox-time ← time picker (drum/scroll-wheel)
10
+ └── combobox-dropdown ← listbox (dropdown & typeahead)
11
+ ```
12
+
13
+ ---
14
+
15
+ ## input-combobox
16
+
17
+ Owns the trigger input, popover visibility, and hidden value. Always co-located with `input-formatter`.
18
+
19
+ **Targets**
20
+
21
+ | Target | Element | Description |
22
+ | --------- | -------------------- | ----------------------------------------------- |
23
+ | `trigger` | `input[type=text]` | The combobox input (display + focus management) |
24
+ | `input` | `input[type=hidden]` | Holds the submitted value |
25
+
26
+ **Values**
27
+
28
+ | Value | Type | Default | Description |
29
+ | ----------- | ------ | ------- | ---------------------------------------------------------------------- |
30
+ | `value` | String | `""` | Current selected value; setting it triggers `valueValueChanged` |
31
+ | `minLength` | Number | `1` | Min query length before typeahead relays to `combobox-dropdown` outlet |
32
+
33
+ **Outlets**
34
+
35
+ | Outlet | Description |
36
+ | ------------------- | ----------------------------------- |
37
+ | `combobox-dropdown` | Optional; present in typeahead mode |
38
+
39
+ **Methods**
40
+
41
+ | Method | Wired via | Description |
42
+ | ----------------- | --------------------- | ---------------------------------------------------------------------------------- |
43
+ | `onSelect(event)` | `combobox-*:selected` | Event adapter — writes `event.detail.value` to `valueValue`; popover handles close |
44
+ | `onInput(event)` | `input` on trigger | Event adapter — extracts query, relays to `comboboxDropdownOutlet.filter(query)` |
45
+
46
+ **Dispatches**
47
+
48
+ | Event | Detail | When |
49
+ | ------------------------ | ----------- | ------------------------------------------------- |
50
+ | `input-combobox:changed` | `{ value }` | After `valueValue` changes (including on connect) |
51
+
52
+ ---
53
+
54
+ ## input-formatter
55
+
56
+ Formats and displays values. Always co-located with `input-combobox`.
57
+
58
+ **Targets**
59
+
60
+ | Target | Element | Description |
61
+ | -------- | ------------------------ | ------------------------------------------------------------------------- |
62
+ | `input` | `<input>` or any element | Write destination — sets `.value` for `<input>`, `.textContent` otherwise |
63
+ | `toggle` | `<button>` | Reveal/conceal button (maskable/password types only) |
64
+
65
+ **Values**
66
+
67
+ | Value | Type | Default | Description |
68
+ | ---------- | ------- | --------- | --------------------------------------------------------- |
69
+ | `format` | String | `"plain"` | `plain` \| `password` \| `creditCard` \| `date` \| `time` |
70
+ | `options` | Object | `{}` | Formatter options (e.g. `{ format: "h12" }` for time) |
71
+ | `revealed` | Boolean | `false` | Whether a masked value is currently revealed |
72
+
73
+ **Methods**
74
+
75
+ | Method | Wired via | Description |
76
+ | ----------------- | ------------------------ | -------------------------------------------------------------------------------- |
77
+ | `format(value)` | — | Programmatic API — normalises, formats, writes to target, dispatches `formatted` |
78
+ | `onChange(event)` | `input-combobox:changed` | Event adapter — extracts `event.detail.value`, calls `format(value)` |
79
+ | `onPaste(event)` | `clipboard:pasted` | Event adapter — normalises and validates pasted text, calls `format(value)` |
80
+ | `toggle()` | `data-action` | Action — flips `revealedValue` (maskable / password types only) |
81
+
82
+ **Dispatches**
83
+
84
+ | Event | Detail | When |
85
+ | --------------------------- | ----------- | --------------------------------------- |
86
+ | `input-formatter:formatted` | `{ value }` | After every write to the `input` target |
87
+
88
+ ---
89
+
90
+ ## combobox-date
91
+
92
+ Navigates a calendar grid with month, year, and decade views. Requires a `calendar-month` outlet; `calendar-year` and `calendar-decade` are optional for drill-down. View switching hides/shows outlet elements directly — no named targets for view containers.
93
+
94
+ **Targets**
95
+
96
+ | Target | Description |
97
+ | ----------- | ---------------------------------------------------------------------- |
98
+ | `previous` | Button that steps backward (one month / one year / one decade by view) |
99
+ | `next` | Button that steps forward (one month / one year / one decade by view) |
100
+ | `viewTitle` | Button showing the current view label (e.g. "June 2025" / "2025") |
101
+ | `day` | Rendered day label element (display only) |
102
+ | `month` | Rendered month label element (display only) |
103
+ | `year` | Rendered year label element (display only) |
104
+
105
+ **Outlets**
106
+
107
+ | Outlet | Description |
108
+ | ----------------- | ------------------------------------------------------------ |
109
+ | `calendar-month` | Required — provides the day grid and calendar navigation API |
110
+ | `calendar-year` | Optional — year view grid; shown when `view` is `"year"` |
111
+ | `calendar-decade` | Optional — decade view grid; shown when `view` is `"decade"` |
112
+
113
+ **Values**
114
+
115
+ | Value | Type | Default | Description |
116
+ | ------------- | ------ | ------------- | ----------------------------------------------------------- |
117
+ | `date` | String | `""` | ISO 8601 initial date; navigates calendar on outlet connect |
118
+ | `view` | String | `"month"` | Current view — `"month"` \| `"year"` \| `"decade"` |
119
+ | `locales` | Array | `["default"]` | `Intl.DateTimeFormat` locales |
120
+ | `dayFormat` | String | `"numeric"` | Day label format |
121
+ | `monthFormat` | String | `"long"` | Month label format |
122
+ | `yearFormat` | String | `"numeric"` | Year label format |
123
+
124
+ **Methods**
125
+
126
+ | Method | Wired via | Description |
127
+ | ---------------------- | --------------------------- | ---------------------------------------------------------------------------------------- |
128
+ | `previous()` | click on `previous` target | Steps back: one month (month view), one year (year view), one decade (decade view) |
129
+ | `next()` | click on `next` target | Steps forward: one month (month view), one year (year view), one decade (decade view) |
130
+ | `zoomOut()` | click on `viewTitle` target | Zooms out: month → year → decade view |
131
+ | `onDaySelect(event)` | `calendar-month:selected` | Event adapter — updates `dateValue`, redraws labels, dispatches `combobox-date:selected` |
132
+ | `onMonthSelect(event)` | `calendar-year:selected` | Event adapter — navigates calendar to selected month, switches to month view |
133
+ | `onYearSelect(event)` | `calendar-decade:selected` | Event adapter — navigates calendar to selected year, switches to year view |
134
+
135
+ **Dispatches**
136
+
137
+ | Event | Detail | When |
138
+ | ------------------------ | ---------------------- | ------------------------- |
139
+ | `combobox-date:selected` | `{ value }` (ISO 8601) | After a date is confirmed |
140
+
141
+ ---
142
+
143
+ ## combobox-time
144
+
145
+ Drum/scroll-wheel time picker. Each drum is a `ul[role=listbox]`.
146
+
147
+ **Targets:** `hour`, `minute`, `period` (period only present in h12 mode)
148
+
149
+ **Methods**
150
+
151
+ | Method | Wired via | Description |
152
+ | ------------------- | ------------------------ | ----------------------------------------------------------------------------------- |
153
+ | `select(value)` | — | Programmatic API — dispatches `combobox-time:selected` with the given 24-hour value |
154
+ | `onSelect(event)` | `click` on a drum option | Event adapter — marks clicked option `aria-selected="true"`, calls `select(value)` |
155
+ | `onNavigate(event)` | `keydown` on a drum | Event adapter — ArrowUp/ArrowDown call `step(drum, delta)` |
156
+ | `step(drum, delta)` | — | Programmatic API — moves selection by `delta` steps in the given drum element |
157
+
158
+ **Dispatches**
159
+
160
+ | Event | Detail | When |
161
+ | ------------------------ | ------------------------------- | ------------------------------------------------------------- |
162
+ | `combobox-time:selected` | `{ value }` (`"HH:MM"` 24-hour) | On connect (if pre-selected) and after every user interaction |
163
+
164
+ ---
165
+
166
+ ## combobox-dropdown
167
+
168
+ Listbox with client-side fuzzy filter or server-side fetch. Used by both dropdown and typeahead variants.
169
+
170
+ **Targets**
171
+
172
+ | Target | Description |
173
+ | --------- | ----------------------------------------- |
174
+ | `listbox` | `ul[role=listbox]` containing the options |
175
+ | `loading` | Shown during server fetch |
176
+ | `empty` | Shown when no options match |
177
+
178
+ **Values**
179
+
180
+ | Value | Type | Default | Description |
181
+ | ------- | ------ | ------- | ----------------------------------------------------------- |
182
+ | `url` | String | `""` | Fetch URL — empty string activates client-side fuzzy filter |
183
+ | `field` | String | `"q"` | Query parameter name appended to `url` |
184
+ | `delay` | Number | `300` | Debounce delay in ms before issuing a server fetch |
185
+
186
+ **Methods**
187
+
188
+ | Method | Wired via | Description |
189
+ | ------------------- | ------------------------ | --------------------------------------------------------------------------------------------- |
190
+ | `select(value)` | — | Programmatic API — sets `aria-selected` on matching option, dispatches `selected` |
191
+ | `onSelect(event)` | `click` on an option | Event adapter — extracts value from click target, calls `select(value)` |
192
+ | `onNavigate(event)` | `keydown` on listbox | Event adapter — ArrowUp/ArrowDown call `step(delta)`; Enter/Space activates current selection |
193
+ | `step(delta)` | — | Programmatic API — moves `aria-selected` by `delta` steps (+1 down, -1 up) |
194
+ | `filter(query)` | `input-combobox` outlet | Programmatic API — routes to fuzzy filter (client) or debounced fetch (server) |
195
+ | `showAll()` | `input-combobox#onInput` | Programmatic API — unhides all options when query drops below `minLength` |
196
+
197
+ **Dispatches**
198
+
199
+ | Event | Detail | When |
200
+ | ---------------------------- | ----------- | --------------------------- |
201
+ | `combobox-dropdown:selected` | `{ value }` | After an option is selected |
202
+
203
+ **Filter implementation**
204
+
205
+ `combobox-dropdown` uses two standalone utilities internally:
206
+
207
+ - **Client-side** (`url` is empty): calls `filterOptions(listboxTarget, query)` from `researcher.js` — hides non-matching `[role="option"]` elements using fuzzy matching by default. See [Researcher docs](../utility/researcher.md) for strategy and field options.
208
+ - **Server-side** (`url` is set): debounces requests via `Requestor` from `requestor.js`, then issues `GET {url}?{field}={query}`. See [Requestor docs](../utility/requestor.md) for abort and debounce behaviour.
209
+
210
+ **Server fetch contract**
211
+
212
+ `GET {url}?{field}={query}` — expected response: an HTML fragment of `<li role="option" data-value="...">` elements, which replaces the inner HTML of the `listbox` target.
213
+
214
+ ---
215
+
216
+ ## Event flow
217
+
218
+ ```
219
+ user picks value
220
+ └─ combobox-*:selected { value }
221
+ └─ input-combobox#onSelect ← event adapter
222
+ └─ valueValue = value → valueValueChanged
223
+ ├─ inputTarget.value = value
224
+ └─ dispatch input-combobox:changed { value }
225
+ └─ input-formatter#onChange ← event adapter
226
+ └─ format(value) ← programmatic API
227
+ ├─ formats value
228
+ ├─ writes to inputTarget
229
+ └─ dispatch input-formatter:formatted { value }
230
+
231
+ (popover closes separately via popover#closeOnSelect)
232
+ ```
233
+
234
+ ---
235
+
236
+ ## Naming convention
237
+
238
+ | Pattern | Wired via | Role | Example |
239
+ | ------------ | --------------------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
240
+ | `onX(event)` | DOM or Stimulus event | Event adapter — extracts payload, calls programmatic API | `onSelect(event)`, `onChange(event)`, `onPaste(event)`, `onInput(event)`, `onNavigate(event)` |
241
+ | `x(value)` | — (called directly) | Programmatic API — pure logic, no event awareness | `select('us')`, `format('4242…')`, `step(1)`, `filter('query')` |
242
+ | `past()` | Plumber | Plumber callback — called by plumber after async operation | `shown()`, `dismissed()`, `contentLoaded()` |
@@ -0,0 +1,39 @@
1
+ # dismisser
2
+
3
+ Closes/hides an element when the user clicks outside it. Backed by the `Dismisser` plumber.
4
+
5
+ ## Targets
6
+
7
+ | Target | Description |
8
+ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
9
+ | `trigger` | Optional — passed as `{ target }` context to the `dismissed()` callback. Does not change the click-outside boundary (which is always the controller element). |
10
+
11
+ ## Methods
12
+
13
+ | Method | Wired via | Description |
14
+ | ------------- | ----------------- | ---------------------------------------------------------------------------------------------------- |
15
+ | `dismissed()` | Dismisser plumber | Plumber callback — called when a click-outside is detected. Implement in your controller to respond. |
16
+
17
+ ## Example
18
+
19
+ ```html
20
+ <div data-controller="dismisser">
21
+ <div data-dismisser-target="trigger">
22
+ <p>Click outside this box to dismiss it.</p>
23
+ </div>
24
+ </div>
25
+ ```
26
+
27
+ Implement `dismissed()` on your controller to act on the dismissal:
28
+
29
+ ```javascript
30
+ async dismissed() {
31
+ await this.collapse()
32
+ }
33
+ ```
34
+
35
+ ## Notes
36
+
37
+ - The Dismisser plumber attaches a document-level `mousedown` listener. When a click lands outside the **controller element**, it calls `dismissed()`. If a `trigger` target is present, it is passed to `dismissed()` as `{ target }` context — it does not change the boundary.
38
+ - Used internally by `modal` (custom overlay mode) and `form-field` to handle click-outside-to-close.
39
+ - `dismissed()` is not called when clicking on the trigger element itself.