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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +16 -0
- data/docs/component/button.md +1 -1
- data/docs/component/calendar.md +11 -4
- data/docs/component/card.md +14 -12
- data/docs/component/combobox.md +2 -2
- data/docs/component/form.md +32 -6
- data/docs/component/list.md +2 -2
- data/docs/component/plumber.md +22 -0
- data/docs/component/theme.md +15 -2
- data/docs/component/timeline.md +2 -2
- data/docs/guide.md +27 -0
- data/lib/generators/stimulus_plumbers/install/install_generator.rb +29 -0
- data/lib/stimulus_plumbers/components/button.rb +1 -1
- data/lib/stimulus_plumbers/components/card.rb +1 -1
- data/lib/stimulus_plumbers/components/combobox.rb +1 -1
- data/lib/stimulus_plumbers/components/link.rb +1 -1
- data/lib/stimulus_plumbers/components/list/item.rb +1 -1
- data/lib/stimulus_plumbers/components/timeline/event.rb +1 -1
- data/lib/stimulus_plumbers/engine.rb +4 -0
- data/lib/stimulus_plumbers/generators/css_entrypoint.rb +74 -0
- data/lib/stimulus_plumbers/generators/tokens_directive.rb +20 -0
- data/lib/stimulus_plumbers/plumber/slots.rb +10 -3
- data/lib/stimulus_plumbers/version.rb +1 -1
- data/lib/tasks/stimulus_plumbers.rake +18 -0
- data/vendor/ARIA.md +88 -0
- data/vendor/controller/docs/calendar.md +215 -0
- data/vendor/controller/docs/clipboard.md +61 -0
- data/vendor/controller/docs/combobox.md +242 -0
- data/vendor/controller/docs/dismisser.md +39 -0
- data/vendor/controller/docs/flipper.md +46 -0
- data/vendor/controller/docs/input-clearable.md +46 -0
- data/vendor/controller/docs/input-formatter.md +90 -0
- data/vendor/controller/docs/modal.md +60 -0
- data/vendor/controller/docs/panner.md +24 -0
- data/vendor/controller/docs/popover.md +113 -0
- data/vendor/controller/docs/timeline.md +84 -0
- data/vendor/controller/guide.md +16 -0
- metadata +20 -2
- /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.
|