stimulus_plumbers 0.4.8 → 0.4.10

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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +39 -0
  3. data/README.md +7 -9
  4. data/app/assets/javascripts/stimulus-plumbers/controllers.manifest.json +367 -23
  5. data/app/assets/javascripts/stimulus-plumbers/index.es.js +678 -233
  6. data/app/assets/javascripts/stimulus-plumbers/index.es.js.map +1 -1
  7. data/app/assets/javascripts/stimulus-plumbers/index.umd.js +1 -1
  8. data/app/assets/javascripts/stimulus-plumbers/index.umd.js.map +1 -1
  9. data/app/assets/stylesheets/stimulus_plumbers/tokens.css +58 -53
  10. data/docs/architecture.md +0 -2
  11. data/docs/component/avatar.md +1 -0
  12. data/docs/component/button.md +1 -0
  13. data/docs/component/calendar.md +7 -16
  14. data/docs/component/checklist.md +153 -0
  15. data/docs/component/form.md +27 -5
  16. data/docs/component/icon.md +7 -5
  17. data/docs/component/indicator.md +55 -0
  18. data/docs/component/list.md +2 -3
  19. data/docs/component/ordered_list.md +118 -0
  20. data/docs/component/plumber.md +21 -0
  21. data/docs/component/popover.md +1 -2
  22. data/docs/component/progress.md +65 -0
  23. data/docs/component/timeline.md +47 -12
  24. data/docs/guide.md +21 -0
  25. data/lib/generators/stimulus_plumbers/install/install_generator.rb +21 -11
  26. data/lib/stimulus_plumbers/components/button.rb +1 -0
  27. data/lib/stimulus_plumbers/components/checklist/item/slots.rb +13 -0
  28. data/lib/stimulus_plumbers/components/checklist/item.rb +64 -0
  29. data/lib/stimulus_plumbers/components/checklist.rb +50 -0
  30. data/lib/stimulus_plumbers/components/combobox/typeahead.rb +1 -0
  31. data/lib/stimulus_plumbers/components/icon.rb +2 -2
  32. data/lib/stimulus_plumbers/components/indicator.rb +30 -0
  33. data/lib/stimulus_plumbers/components/link.rb +1 -0
  34. data/lib/stimulus_plumbers/components/ordered_list/item/slots.rb +13 -0
  35. data/lib/stimulus_plumbers/components/ordered_list/item.rb +129 -0
  36. data/lib/stimulus_plumbers/components/ordered_list.rb +24 -0
  37. data/lib/stimulus_plumbers/components/progress_bar.rb +48 -0
  38. data/lib/stimulus_plumbers/components/progress_meter.rb +37 -0
  39. data/lib/stimulus_plumbers/components/progress_ring.rb +40 -0
  40. data/lib/stimulus_plumbers/components/timeline/event.rb +2 -1
  41. data/lib/stimulus_plumbers/components/timeline/group.rb +5 -3
  42. data/lib/stimulus_plumbers/engine.rb +0 -4
  43. data/lib/stimulus_plumbers/form/builder.rb +4 -0
  44. data/lib/stimulus_plumbers/form/field.rb +1 -1
  45. data/lib/stimulus_plumbers/form/fields/inputs/code.rb +86 -0
  46. data/lib/stimulus_plumbers/form/fields/inputs/credit_card.rb +98 -0
  47. data/lib/stimulus_plumbers/form/fields/inputs/password.rb +1 -0
  48. data/lib/stimulus_plumbers/form/fields/inputs/search.rb +1 -0
  49. data/lib/stimulus_plumbers/form/fields/renderer.rb +2 -0
  50. data/lib/stimulus_plumbers/generators/css_entrypoint.rb +37 -9
  51. data/lib/stimulus_plumbers/generators/tokens_directive.rb +9 -5
  52. data/lib/stimulus_plumbers/helpers/avatar_helper.rb +1 -7
  53. data/lib/stimulus_plumbers/helpers/button_helper.rb +1 -7
  54. data/lib/stimulus_plumbers/helpers/calendar_helper.rb +2 -9
  55. data/lib/stimulus_plumbers/helpers/calendar_turbo_helper.rb +3 -6
  56. data/lib/stimulus_plumbers/helpers/checklist_helper.rb +11 -0
  57. data/lib/stimulus_plumbers/helpers/indicator_helper.rb +11 -0
  58. data/lib/stimulus_plumbers/helpers/ordered_list_helper.rb +11 -0
  59. data/lib/stimulus_plumbers/helpers/popover_helper.rb +1 -7
  60. data/lib/stimulus_plumbers/helpers/progress_helper.rb +21 -0
  61. data/lib/stimulus_plumbers/helpers.rb +8 -0
  62. data/lib/stimulus_plumbers/themes/base.rb +4 -0
  63. data/lib/stimulus_plumbers/themes/schema/icon.rb +21 -4
  64. data/lib/stimulus_plumbers/themes/schema/indicator/ranges.rb +14 -0
  65. data/lib/stimulus_plumbers/themes/schema.rb +82 -25
  66. data/lib/stimulus_plumbers/version.rb +1 -1
  67. data/lib/stimulus_plumbers.rb +10 -0
  68. data/vendor/ARIA.md +29 -2
  69. data/vendor/component/manifest.json +215 -0
  70. data/vendor/controller/docs/calendar.md +4 -7
  71. data/vendor/controller/docs/checklist.md +45 -0
  72. data/vendor/controller/docs/combobox.md +1 -31
  73. data/vendor/controller/docs/input-clearable.md +1 -4
  74. data/vendor/controller/docs/input-formatter.md +88 -23
  75. data/vendor/controller/docs/modal.md +1 -4
  76. data/vendor/controller/docs/popover.md +1 -3
  77. data/vendor/controller/docs/progress.md +82 -0
  78. data/vendor/controller/docs/reorderable.md +82 -0
  79. data/vendor/controller/docs/visibility.md +31 -0
  80. data/vendor/controller/manifest.json +367 -23
  81. metadata +27 -1
@@ -0,0 +1,45 @@
1
+ # Checklist
2
+
3
+ Master "select all" toggle for a group of native `<input type="checkbox">` checklist items. Aggregates their `.checked` state onto a master checkbox's `checked`/`indeterminate` properties and toggles them all at once. See [stimulus-plumbers-rails's docs/component/checklist.md](../../../stimulus-plumbers-rails/docs/component/checklist.md) for the Rails render options.
4
+
5
+ ## Stimulus Identifier
6
+
7
+ `checklist`
8
+
9
+ ## Targets
10
+
11
+ | Name | Purpose |
12
+ | -------- | --------------------------------------------------------------------- |
13
+ | `master` | The "select all" `<input type="checkbox">` — receives aggregate state |
14
+ | `item` | Each checklist item `<input type="checkbox">` |
15
+
16
+ ## Actions
17
+
18
+ | Name | Purpose |
19
+ | ----------- | -------------------------------------------------------------------------------------------------------------------------------- |
20
+ | `onChange` | Wired to `change` on the wrapper — event adapter: if the event came from the master, calls `toggleAll`; always calls `recompute` |
21
+ | `toggleAll` | Programmatic API — sets every enabled item's `.checked` to the given value |
22
+ | `recompute` | Programmatic API — writes the master's `.checked`/`.indeterminate` from the enabled items' aggregate state |
23
+
24
+ ## Example HTML
25
+
26
+ ```html
27
+ <div data-controller="checklist" data-action="change->checklist#onChange">
28
+ <label>
29
+ <input type="checkbox" data-checklist-target="master" />
30
+ Select all
31
+ </label>
32
+
33
+ <label>
34
+ <input type="checkbox" data-checklist-target="item" checked />
35
+ Buy milk
36
+ </label>
37
+
38
+ <label>
39
+ <input type="checkbox" data-checklist-target="item" />
40
+ Walk the dog
41
+ </label>
42
+ </div>
43
+ ```
44
+
45
+ Disabled items (`disabled` attribute) are excluded from both aggregation and bulk toggling — the controller filters them out via `enabledItems()`. `indeterminate` is a JS-only property with no HTML attribute; it is set on connect and after every change, never rendered server-side.
@@ -53,37 +53,7 @@ Owns the trigger input, popover visibility, and hidden value. Always co-located
53
53
 
54
54
  ## input-formatter
55
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 |
56
+ Formats and displays values. Always co-located with `input-combobox`. See [input-formatter.md](input-formatter.md) for targets, values, methods, and dispatches.
87
57
 
88
58
  ---
89
59
 
@@ -34,10 +34,7 @@ Shows a clear button when an input has a value, hides it when empty, and clears
34
34
  ## Accessibility
35
35
 
36
36
  - The clear button must carry `aria-label="Clear search"` — the controller never overrides it.
37
- - The clear button is hidden (`hidden` attribute) while the input is empty, so keyboard users only reach it when there is something to clear.
38
- - After clearing, focus returns to the input so the user can type immediately (WCAG 2.4.3 Focus Order).
39
- - Pressing Escape inside the input clears it when the field has a value; the event's default is prevented to avoid closing parent overlays unintentionally.
40
- - No `aria-live` region is needed — clearing is user-initiated and the button's disappearance is self-explanatory.
37
+ - See [ARIA.md's Input Clearable pattern](../../../ARIA.md) for the clear-button tab-order, Escape-key, and focus-return contract.
41
38
 
42
39
  ## Notes
43
40
 
@@ -6,18 +6,20 @@ Formats, masks, and reveals values written to an input element. Handles password
6
6
 
7
7
  ## Targets
8
8
 
9
- | Target | Element | Description |
10
- | -------- | ------------------------ | --------------------------------------------------------------------------- |
11
- | `input` | `<input>` or any element | Write destination — sets `.value` for `<input>`, `.textContent` otherwise |
12
- | `toggle` | `<button>` | Reveal/conceal button; hidden at connect when the formatter is not maskable |
9
+ | Target | Element | Description |
10
+ | -------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
11
+ | `input` | `<input>` or any element | Write destination — sets `.value` for `<input>`, `.textContent` otherwise |
12
+ | `toggle` | `<button>` | Reveal/conceal button; hidden at connect when the formatter is not maskable |
13
+ | `cell` | `<div>` (any element) | Optional character cells; when present, the canonical value is painted one character per cell, or (grouped mode — cell count equals `groups.length`) one group-sized slice per cell — see [character-cells](../plumber/character-cells.md) |
13
14
 
14
15
  ## Values
15
16
 
16
- | Value | Type | Default | Description |
17
- | ---------- | ------- | --------- | -------------------------------------------------------------------- |
18
- | `format` | String | `"plain"` | Formatter type — see [Formatters](#formatters) for valid identifiers |
19
- | `options` | Object | `{}` | Formatter-specific options (e.g. `{ locale: "en-US" }` for currency) |
20
- | `revealed` | Boolean | `false` | Whether a masked value is currently revealed; managed by `toggle()` |
17
+ | Value | Type | Default | Description |
18
+ | ---------- | ------- | --------- | --------------------------------------------------------------------------------- |
19
+ | `format` | String | `"plain"` | Formatter type — see [Formatters](#formatters) for valid identifiers |
20
+ | `options` | Object | `{}` | Formatter-specific options (e.g. `{ locale: "en-US" }` for currency) |
21
+ | `revealed` | Boolean | `false` | Whether a masked value is currently revealed; managed by `toggle()` |
22
+ | `groups` | Array | `[]` | Cell group widths (e.g. `[4,4,4,4]`); overrides the formatter's own grouping hint |
21
23
 
22
24
  ## Methods
23
25
 
@@ -27,24 +29,29 @@ Formats, masks, and reveals values written to an input element. Handles password
27
29
  | `toggle()` | `data-action` | Action — flips `revealedValue`; no-op unless `format` is `"password"` or formatter is maskable |
28
30
  | `onChange(event)` | `input-combobox:changed` | Event adapter — extracts `event.detail.value`, calls `format(value)` |
29
31
  | `onPaste(event)` | `clipboard:pasted` | Event adapter — normalises and validates pasted text, calls `format(value)` |
32
+ | `onInput(event)` | `input` DOM event | Event adapter — re-formats from the input's current value |
33
+ | `onFocus(event)` | `focus` DOM event | Event adapter — redraws cells so the caret cell appears |
34
+ | `onBlur(event)` | `blur` DOM event | Event adapter — redraws cells so the caret cell clears |
30
35
 
31
36
  ## Dispatches
32
37
 
33
- | Event | Detail | When |
34
- | --------------------------- | ----------- | --------------------------------------- |
35
- | `input-formatter:formatted` | `{ value }` | After every write to the `input` target |
38
+ | Event | Detail | When |
39
+ | --------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
40
+ | `input-formatter:formatted` | `{ value }` | After every write to the `input` target |
41
+ | `input-formatter:filled` | `{ value }` | When a valid, cell-capable formatter's value reaches its expected cell length — auto-submit hook. Fires once per fill, on the transition into "full"; not on connect with an already-full value. |
36
42
 
37
43
  ## Formatters
38
44
 
39
- | `type` | Description |
40
- | -------------- | ------------------------------------------------------------------ |
41
- | `"plain"` | No-op — passes the value through unchanged |
42
- | `"password"` | Switches `input[type]` between `"password"` and `"text"` on reveal |
43
- | `"creditCard"` | Groups digits as `#### #### #### ####` |
44
- | `"phone"` | Formats as a local phone number |
45
- | `"currency"` | Locale-aware thousands separator and decimal places |
46
- | `"date"` | Locale-aware date string |
47
- | `"time"` | Locale-aware time string |
45
+ | `type` | Description |
46
+ | -------------- | ------------------------------------------------------------------------------------------------------------------ |
47
+ | `"plain"` | No-op — passes the value through unchanged |
48
+ | `"password"` | Switches `input[type]` between `"password"` and `"text"` on reveal |
49
+ | `"creditCard"` | Groups digits as `#### #### #### ####` |
50
+ | `"phone"` | Formats as a local phone number |
51
+ | `"currency"` | Locale-aware thousands separator and decimal places |
52
+ | `"date"` | Locale-aware date string |
53
+ | `"time"` | Locale-aware time string |
54
+ | `"code"` | Charset-filtered fixed-length codes (OTP, PIN); options `{ charset: "digits"\|"letters"\|"alphanumeric", length }` |
48
55
 
49
56
  Custom formatters can be registered at runtime:
50
57
 
@@ -83,8 +90,66 @@ Formatter.register('iban', {
83
90
  </div>
84
91
  ```
85
92
 
93
+ ### One-time code entry (cells)
94
+
95
+ ```html
96
+ <div
97
+ data-controller="input-formatter"
98
+ data-input-formatter-format-value="code"
99
+ data-input-formatter-options-value='{"charset":"digits","length":6}'
100
+ >
101
+ <label for="otp" class="sr-only">Verification code</label>
102
+ <div class="cells-row">
103
+ <div data-input-formatter-target="cell"></div>
104
+ <div data-input-formatter-target="cell"></div>
105
+ <div data-input-formatter-target="cell"></div>
106
+ <div data-input-formatter-target="cell"></div>
107
+ <div data-input-formatter-target="cell"></div>
108
+ <div data-input-formatter-target="cell"></div>
109
+ </div>
110
+ <input
111
+ id="otp"
112
+ data-input-formatter-target="input"
113
+ autocomplete="one-time-code"
114
+ inputmode="numeric"
115
+ maxlength="6"
116
+ data-action="input->input-formatter#onInput focus->input-formatter#onFocus blur->input-formatter#onBlur"
117
+ />
118
+ </div>
119
+ ```
120
+
121
+ Cells are decoration; style them via `[data-filled]`, `[data-caret]`, `[data-group-end]`, `[data-inactive]`. See [ARIA.md's Code Input pattern](../../../ARIA.md) for the overlay/labeling requirements.
122
+
123
+ ### Credit card formatting (grouped cells)
124
+
125
+ Rendering exactly `groups.length` cells (instead of one cell per digit) puts the plumber into
126
+ grouped mode: each cell displays its whole group of digits. Separators between cells (e.g. a
127
+ dash) are authored HTML, never inserted by the plumber — see
128
+ [character-cells](../plumber/character-cells.md#grouped-mode).
129
+
130
+ ```html
131
+ <div data-controller="input-formatter" data-input-formatter-format-value="creditCard">
132
+ <div class="cells-row">
133
+ <span data-input-formatter-target="cell"></span>
134
+ <span aria-hidden="true">-</span>
135
+ <span data-input-formatter-target="cell"></span>
136
+ <span aria-hidden="true">-</span>
137
+ <span data-input-formatter-target="cell"></span>
138
+ <span aria-hidden="true">-</span>
139
+ <span data-input-formatter-target="cell"></span>
140
+ </div>
141
+ <input
142
+ data-input-formatter-target="input"
143
+ autocomplete="cc-number"
144
+ inputmode="numeric"
145
+ maxlength="16"
146
+ data-action="input->input-formatter#onInput focus->input-formatter#onFocus blur->input-formatter#onBlur"
147
+ />
148
+ </div>
149
+ ```
150
+
86
151
  ## Accessibility
87
152
 
88
- - The `toggle` button must carry `aria-label` describing the action: `"Show password"` / `"Hide password"`.
89
- - `aria-pressed` is managed automatically — initialised to `"false"`, flipped to `"true"` when the value is revealed.
90
153
  - Password `type` attribute switches between `"password"` (masked) and `"text"` (revealed), so screen readers announce the field type correctly.
154
+ - See [ARIA.md's Password Reveal pattern](../../../ARIA.md) for the `toggle` button's `aria-label`/`aria-pressed` requirements.
155
+ - Cell display: see [ARIA.md's Code Input pattern](../../../ARIA.md).
@@ -53,8 +53,5 @@ Focus is trapped inside `modal` target using `FocusTrap`. Escape key and click-o
53
53
 
54
54
  ## Accessibility
55
55
 
56
- - Focus moves into the modal on open; returns to the trigger on close
57
- - Focus is trapped — Tab/Shift+Tab cycle within the modal
58
- - Escape closes the modal
59
56
  - `body` scroll is locked while open (custom mode)
60
- - Status announcements via `aria-live` on open/close
57
+ - See [ARIA.md's Modal pattern](../../../ARIA.md) for focus management and the status-announcement contract
@@ -108,6 +108,4 @@ Selection events (from combobox sub-controllers, menu items, etc.) can be wired
108
108
 
109
109
  ## Accessibility
110
110
 
111
- - `trigger` should have `aria-haspopup` describing the popup type; `aria-expanded` is toggled automatically.
112
- - `panel` should have an appropriate `role` (`dialog`, `tooltip`, `listbox`, etc.).
113
- - Esc and outside-click both close the panel; focus returns to the `trigger` on close.
111
+ See [ARIA.md's Popover pattern](../../../ARIA.md) for role, `aria-haspopup`/`aria-expanded`, and focus-return requirements.
@@ -0,0 +1,82 @@
1
+ # Progress
2
+
3
+ Value-driven progress indicator supporting three render variants: a linear bar, an SVG ring, and a native `<meter>`.
4
+
5
+ ## Stimulus Identifier
6
+
7
+ `progress`
8
+
9
+ ## Targets
10
+
11
+ | Name | Element | Purpose |
12
+ | ------- | --------------------------------- | ----------------------------------------------------------------------------------- |
13
+ | `fill` | `<div>` (bar) / `<circle>` (ring) | Element whose `width` (bar) or `stroke-dasharray`/`stroke-dashoffset` (ring) is set |
14
+ | `meter` | `<meter>` | Present only for `variant: "meter"` — native element, attributes synced directly |
15
+
16
+ ## Values
17
+
18
+ | Name | Type | Default | Purpose |
19
+ | ----------------------- | ------- | ------- | ------------------------------------------------------------------------- |
20
+ | `variant` | String | `"bar"` | `"bar"` \| `"ring"` \| `"meter"` |
21
+ | `current` | Number | `0` | Current value |
22
+ | `min` | Number | `0` | Range minimum |
23
+ | `max` | Number | `100` | Range maximum |
24
+ | `optimum` | Number | — | Meter-only; maps to native `<meter optimum>` |
25
+ | `low` | Number | — | Meter-only; maps to native `<meter low>` |
26
+ | `high` | Number | — | Meter-only; maps to native `<meter high>` |
27
+ | `indeterminate` | Boolean | `false` | Suppresses `aria-valuenow`; toggles the `sp-progress-indeterminate` class |
28
+ | `indeterminateFraction` | Number | `0.25` | Static bar width / ring arc fraction rendered while indeterminate |
29
+
30
+ ## Methods
31
+
32
+ | Method | Wired via | Purpose |
33
+ | ---------------------------- | ----------------------- | --------------------------------------------------------------------------------------------------------- |
34
+ | `setValue(value)` | — | Programmatic API — clamps to `[min, max]`, updates `currentValue`, dispatches `progress:changed` |
35
+ | `currentValueChanged(value)` | Stimulus value callback | Recalculates fill/meter attrs whenever `current` changes (covers `setValue()` and direct attribute edits) |
36
+
37
+ ## Dispatches
38
+
39
+ | Event | Detail | When |
40
+ | ------------------ | --------------------- | ------------------------------------ |
41
+ | `progress:changed` | `{ value, min, max }` | After `setValue()` updates the value |
42
+
43
+ ## Example HTML
44
+
45
+ ```html
46
+ <!-- Bar -->
47
+ <div
48
+ role="progressbar"
49
+ data-controller="progress"
50
+ data-progress-current-value="30"
51
+ data-progress-min-value="0"
52
+ data-progress-max-value="100"
53
+ >
54
+ <div data-progress-target="fill"></div>
55
+ </div>
56
+
57
+ <!-- Ring -->
58
+ <svg
59
+ role="progressbar"
60
+ data-controller="progress"
61
+ data-progress-variant-value="ring"
62
+ data-progress-current-value="25"
63
+ data-progress-max-value="100"
64
+ >
65
+ <circle data-progress-target="fill" r="40"></circle>
66
+ </svg>
67
+
68
+ <!-- Meter -->
69
+ <meter
70
+ data-controller="progress"
71
+ data-progress-variant-value="meter"
72
+ data-progress-target="meter"
73
+ data-progress-current-value="40"
74
+ data-progress-min-value="0"
75
+ data-progress-max-value="100"
76
+ ></meter>
77
+
78
+ <!-- Indeterminate -->
79
+ <div role="progressbar" data-controller="progress" data-progress-indeterminate-value="true">
80
+ <div data-progress-target="fill"></div>
81
+ </div>
82
+ ```
@@ -0,0 +1,82 @@
1
+ # Reorderable
2
+
3
+ Reorders a vertical or horizontal list of items via pointer drag (a dedicated handle) or keyboard (`Alt+Arrow`, axis depends on `orientation`). No third-party drag library — built on the native Pointer Events API, backed by the [`Reorderable` plumber](../plumber/reorderable.md). Plain Arrow/Home/End keys move focus only, via [`RovingTabIndex`](../utility/accessibility.md).
4
+
5
+ ## Stimulus Identifier
6
+
7
+ `reorderable`
8
+
9
+ ## Targets
10
+
11
+ | Name | Element | Purpose |
12
+ | --------- | ------------------------------------------- | ---------------------------------------------------------------------------------- |
13
+ | `item` | Each reorderable row (`<li>`, `<tr>`, etc.) | Must have a stable `id` to appear in the `reorderable:reordered` event's `itemIds` |
14
+ | `handle` | Drag grip within each `item` | The only pointer-drag surface — wire `pointerdown`/`pointermove`/`pointerup` to it |
15
+ | `trigger` | The `<a>`/`<button>` inside an item, if any | Neutralized (via `aria-disabled`/`tabindex`) while `editingValue` is `true` |
16
+
17
+ ## Values
18
+
19
+ | Name | Type | Default | Purpose |
20
+ | ------------- | ------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
21
+ | `moveKey` | String | `"Alt"` | Modifier key that, combined with the move keys below on a focused item, moves it. One of `Alt`, `Control`, `Shift`, `Meta`. |
22
+ | `editing` | Boolean | `false` | While `true`, pointer drag and `Alt+Arrow` move are active and every `trigger` target is neutralized. While `false`, drag/keyboard-move are inert no-ops and every `trigger` behaves normally. Plain Arrow/Home/End focus movement is unaffected either way. |
23
+ | `orientation` | String | `"vertical"` | Reorder axis: `"vertical"` or `"horizontal"`. Also passed through to [`RovingTabIndex`](../utility/accessibility.md). |
24
+
25
+ ## Actions
26
+
27
+ | Name | Purpose |
28
+ | --------------- | ---------------------------------------------------------------------------------------------------------- |
29
+ | `onPointerDown` | Wire to `pointerdown` on the `handle` target — starts a drag (no-op unless `editingValue`) |
30
+ | `onPointerMove` | Wire to `pointermove` on the `handle` target — live-reorders while dragging (no-op unless `editingValue`) |
31
+ | `onPointerUp` | Wire to `pointerup` on the `handle` target — ends the drag (no-op unless `editingValue`) |
32
+ | `toggleEditing` | Flips `editingValue` — wire to e.g. `click->reorderable#toggleEditing` on an app-provided Edit/Done button |
33
+ | `enterEditing` | Sets `editingValue` to `true` explicitly |
34
+ | `exitEditing` | Sets `editingValue` to `false` explicitly |
35
+
36
+ ## Keyboard
37
+
38
+ | Key | Behaviour |
39
+ | ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------- |
40
+ | `ArrowDown`/`ArrowUp` (vertical) or `ArrowLeft`/`ArrowRight` (horizontal) | Focus next/previous item (wraps) — unchanged `RovingTabIndex` behavior |
41
+ | `Home` / `End` | Focus first/last item |
42
+ | `Alt+ArrowDown` (vertical) / `Alt+ArrowRight` (horizontal, `Left` under `dir="rtl"`) | Move the focused item forward one position, keeps focus on it |
43
+ | `Alt+ArrowUp` (vertical) / `Alt+ArrowLeft` (horizontal, `Right` under `dir="rtl"`) | Move the focused item back one position, keeps focus on it |
44
+
45
+ ## Dispatches
46
+
47
+ | Event | Detail | When |
48
+ | ----------------------- | ----------------------- | ------------------------------------------------------------------------------------------------- |
49
+ | `reorderable:reordered` | `{ itemIds: string[] }` | After a drag ends or a keyboard move completes. Items without an `id` are omitted from `itemIds`. |
50
+
51
+ ## Example HTML
52
+
53
+ ```html
54
+ <ul data-controller="reorderable">
55
+ <li id="row-1" data-reorderable-target="item" tabindex="0">
56
+ <span
57
+ data-reorderable-target="handle"
58
+ data-action="pointerdown->reorderable#onPointerDown pointermove->reorderable#onPointerMove pointerup->reorderable#onPointerUp"
59
+ >
60
+ ::
61
+ </span>
62
+ First item
63
+ </li>
64
+ <li id="row-2" data-reorderable-target="item" tabindex="-1">
65
+ <span
66
+ data-reorderable-target="handle"
67
+ data-action="pointerdown->reorderable#onPointerDown pointermove->reorderable#onPointerMove pointerup->reorderable#onPointerUp"
68
+ >
69
+ ::
70
+ </span>
71
+ Second item
72
+ </li>
73
+ </ul>
74
+ ```
75
+
76
+ ## Notes
77
+
78
+ - Persistence is not built in — listen for `reorderable:reordered` and send `event.detail.itemIds` to your backend.
79
+ - Single-list only. Grid orientation and cross-list drag are not supported.
80
+ - The dragged/moved item is repositioned live in the DOM — there is no placeholder or ghost element.
81
+ - See [ARIA.md's Reorderable pattern](../../../ARIA.md) for the drag/keyboard-move focus-and-announcement contract and the `trigger` neutralization rules.
82
+ - Pointer clicks on a `trigger` are not blocked by JS. Apps/themes must add their own CSS rule to block them while editing, e.g. `[data-reorderable-editing-value="true"] [data-reorderable-target="trigger"] { pointer-events: none }` — keeps the controller content-agnostic about link/button internals.
@@ -0,0 +1,31 @@
1
+ # visibility
2
+
3
+ Shows/hides a `content` element, keeping it within the viewport and dismissing it on click-outside. Backed by the `Visibility`, `Shifter`, and `Dismisser` plumbers.
4
+
5
+ ## Targets
6
+
7
+ | Target | Description |
8
+ | --------- | --------------------------------------------------------------------------------- |
9
+ | `content` | Element to show/hide. Optional — `toggle()`/`dismissed()` are no-ops when absent. |
10
+
11
+ ## Methods
12
+
13
+ | Method | Wired via | Description |
14
+ | ------------- | ----------------- | -------------------------------------------------------------------------- |
15
+ | `toggle()` | `data-action` | Shows `content` if hidden (shifting it into viewport), hides it if visible |
16
+ | `dismissed()` | Dismisser plumber | Plumber callback — hides `content` on click-outside |
17
+
18
+ ## Example
19
+
20
+ ```html
21
+ <div data-controller="visibility">
22
+ <button data-action="visibility#toggle">Toggle</button>
23
+ <div data-visibility-target="content" hidden>Content</div>
24
+ </div>
25
+ ```
26
+
27
+ ## Notes
28
+
29
+ - See [docs/plumber/visibility.md](../plumber/visibility.md) for the show/hide API and dispatched events.
30
+ - See [docs/plumber/shifter.md](../plumber/shifter.md) for the viewport-shifting behavior applied to `content` on show.
31
+ - See [docs/component/dismisser.md](dismisser.md) for click-outside-to-dismiss behavior.