stimeo-ui 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +184 -0
- package/dist/controllers/announcer_controller.js.map +1 -1
- package/dist/controllers/aspect_ratio_controller.d.ts +3 -5
- package/dist/controllers/aspect_ratio_controller.js +19 -11
- package/dist/controllers/aspect_ratio_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.d.ts +36 -15
- package/dist/controllers/avatar_controller.js +195 -40
- package/dist/controllers/avatar_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.js +5 -1
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +12 -3
- package/dist/controllers/carousel_controller.js +90 -10
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/checkbox_controller.d.ts +31 -5
- package/dist/controllers/checkbox_controller.js +136 -25
- package/dist/controllers/checkbox_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.js +8 -3
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.d.ts +1 -1
- package/dist/controllers/collapsible_controller.js +4 -1
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +9 -3
- package/dist/controllers/color_picker_controller.js +41 -11
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +1 -1
- package/dist/controllers/context_menu_controller.js +2 -2
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +5 -3
- package/dist/controllers/countdown_controller.js +5 -1
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +23 -4
- package/dist/controllers/date_range_picker_controller.js +162 -31
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.d.ts +1 -1
- package/dist/controllers/direct_upload_controller.js +3 -3
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/empty_state_controller.d.ts +27 -7
- package/dist/controllers/empty_state_controller.js +107 -16
- package/dist/controllers/empty_state_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.d.ts +7 -1
- package/dist/controllers/file_dropzone_controller.js +26 -3
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +25 -5
- package/dist/controllers/flash_controller.js +161 -21
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +2 -3
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js +8 -2
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +19 -2
- package/dist/controllers/frame_loading_controller.js +94 -22
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.d.ts +8 -4
- package/dist/controllers/highlight_controller.js +38 -1
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +2 -2
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +5 -4
- package/dist/controllers/idle_controller.js +39 -6
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.js +2 -0
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +1 -1
- package/dist/controllers/masonry_controller.js +1 -1
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.js +5 -3
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.js +3 -1
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +46 -12
- package/dist/controllers/multi_select_controller.js +460 -151
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.d.ts +15 -13
- package/dist/controllers/network_status_controller.js +1 -3
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +29 -3
- package/dist/controllers/number_input_controller.js +455 -64
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.js +5 -1
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.js +38 -1
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_strength_controller.d.ts +4 -4
- package/dist/controllers/password_strength_controller.js +21 -3
- package/dist/controllers/password_strength_controller.js.map +1 -1
- package/dist/controllers/persist_controller.d.ts +4 -4
- package/dist/controllers/persist_controller.js +24 -5
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.js +10 -0
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +2 -2
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +1 -2
- package/dist/controllers/portal_controller.js +10 -0
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/progress_controller.d.ts +1 -1
- package/dist/controllers/progress_controller.js +7 -3
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.d.ts +41 -15
- package/dist/controllers/radio_group_controller.js +540 -56
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.d.ts +23 -0
- package/dist/controllers/range_slider_controller.js +385 -94
- package/dist/controllers/range_slider_controller.js.map +1 -1
- package/dist/controllers/rating_controller.d.ts +36 -31
- package/dist/controllers/rating_controller.js +274 -89
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.js +2 -0
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/resizable_controller.js +33 -0
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.d.ts +4 -0
- package/dist/controllers/roving_controller.js +60 -5
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.d.ts +7 -1
- package/dist/controllers/scroll_area_controller.js +155 -23
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.js +33 -0
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/separator_controller.js +13 -17
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.d.ts +2 -2
- package/dist/controllers/skeleton_controller.js +71 -3
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +17 -1
- package/dist/controllers/slider_controller.js +325 -48
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.d.ts +15 -10
- package/dist/controllers/spinner_controller.js +18 -3
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +1 -1
- package/dist/controllers/step_indicator_controller.js +3 -1
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.js +2 -0
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/switch_controller.d.ts +12 -8
- package/dist/controllers/switch_controller.js +162 -18
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +41 -11
- package/dist/controllers/tags_input_controller.js +356 -120
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.js +1 -1
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.d.ts +42 -10
- package/dist/controllers/time_picker_controller.js +296 -104
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.d.ts +39 -13
- package/dist/controllers/toggle_group_controller.js +378 -55
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.js +5 -3
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +2 -2
- package/dist/controllers/tooltip_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +2 -3
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +1 -2
- package/dist/controllers/tree_view_controller.js +24 -4
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.js +3811 -1048
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +92 -4
- package/dist/inspector/cli.js +174 -6
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +234 -8
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +24 -24
- package/dist/inspector/manifest.json +573 -59
- package/dist/positioning/index.d.ts +2 -2
- package/dist/positioning/index.js.map +1 -1
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/controllers/stepper_controller.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"sources":["../../src/controllers/stepper_controller.ts"],"names":[],"mappings":"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","file":"stepper_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\n/**\n * Headless, accessible stepper / wizard navigation behavior.\n *\n * Markup contract (identifier: `stimeo--stepper`):\n * <ol data-controller=\"stimeo--stepper\" data-stimeo--stepper-index-value=\"0\">\n * <li data-stimeo--stepper-target=\"step\">\n * <button aria-current=\"step\" data-stimeo--stepper-index-param=\"0\"\n * data-action=\"click->stimeo--stepper#goto\">Account</button>\n * </li>\n * <!-- more steps -->\n * </ol>\n *\n * There is no dedicated APG widget; the current step is expressed with\n * `aria-current=\"step\"` on the operable `<button>`. Each step `<li>` also gets a\n * `data-state` (`complete`/`current`/`upcoming`) derived from the current index.\n * For a read-only progress display use\n * {@link StepIndicatorController | Step Indicator}; for panel switching use Tabs.\n *\n * @remarks\n * Behavior only. The controller never traps or restores focus — each step button\n * is in the natural Tab order. `data-state` is purely derived from `index`\n * (completion is not persisted).\n *\n * Behavior provided:\n * - `next`/`prev` move one step, ignoring moves past either end.\n * - `goto` jumps to the step in its `index` action param.\n * - With `linear=true`, `goto` may not skip more than one step ahead of the\n * current one (moving backward is always allowed).\n * - Runtime changes to the `index` Value re-derive every step's state.\n * - Each move re-derives `data-state`/`aria-current` and dispatches\n * `stimeo--stepper:change`.\n */\nexport class StepperController extends Controller<HTMLElement> {\n static override targets = [\"step\"];\n static override values = {\n index: { type: Number, default: 0 },\n linear: { type: Boolean, default: false },\n };\n static actions = [\"goto\", \"next\", \"prev\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly stepTargets: HTMLElement[];\n declare indexValue: number;\n declare linearValue: boolean;\n\n #isConnected = false;\n\n /** Normalizes an out-of-range initial `index` and renders the initial state. */\n override connect(): void {\n this.#isConnected = true;\n this.#normalizeAndRender();\n }\n\n override disconnect(): void {\n this.#isConnected = false;\n }\n\n /** Re-renders when Turbo Morph or application code changes `index` at runtime. */\n indexValueChanged(): void {\n if (!this.#isConnected) return;\n this.#normalizeAndRender();\n }\n\n /** Advances to the next step (ignored at the last step). */\n next(): void {\n this.#moveTo(this.#clampIndex(this.indexValue) + 1);\n }\n\n /** Returns to the previous step (ignored at the first step). */\n prev(): void {\n this.#moveTo(this.#clampIndex(this.indexValue) - 1);\n }\n\n /** Jumps to the step carried in the action's `index` param. */\n goto(event: { params: { index?: number } }): void {\n const target = Number(event.params.index);\n if (!Number.isFinite(target) || !Number.isInteger(target)) return;\n this.#moveTo(target);\n }\n\n /**\n * Moves the current step to `target` when allowed: in range, not a no-op, and\n * — under `linear` — not skipping more than one step ahead. Re-renders and\n * dispatches `change`.\n */\n #moveTo(target: number): void {\n const total = this.stepTargets.length;\n if (!Number.isFinite(target) || !Number.isInteger(target)) return;\n if (target < 0 || target >= total) return;\n const current = this.#clampIndex(this.indexValue);\n if (target === current) return;\n if (this.linearValue && target > current + 1) return;\n\n const previous = current;\n this.indexValue = target;\n this.#render(target);\n this.dispatch(\"change\", {\n detail: { index: target, previous, step: this.stepTargets[target] },\n });\n }\n\n /** Normalizes the public Value and reflects it without dispatching an action event. */\n #normalizeAndRender(): void {\n const normalized = this.#clampIndex(this.indexValue);\n if (!Object.is(normalized, this.indexValue)) this.indexValue = normalized;\n this.#render(normalized);\n }\n\n /**\n * Derives each step's `data-state` and the current button's `aria-current`.\n *\n * `aria-current=\"step\"` is placed on the step's **first** `<button>`; the markup\n * contract assumes one operable button per step. If a step needs multiple\n * buttons, mark the navigational one first (or this would target the wrong one).\n *\n * @stimeoRenderRoot\n */\n #render(current: number): void {\n this.stepTargets.forEach((step, index) => {\n step.dataset.state =\n index < current ? \"complete\" : index === current ? \"current\" : \"upcoming\";\n const button = step.querySelector<HTMLElement>(\"button\");\n if (!button) return;\n if (index === current) {\n button.setAttribute(\"aria-current\", \"step\");\n } else {\n button.removeAttribute(\"aria-current\");\n }\n });\n }\n\n /** Constrains an index to `[0, total-1]` (or `0` when there are no steps). */\n #clampIndex(index: number): number {\n const last = this.stepTargets.length - 1;\n if (last < 0 || !Number.isFinite(index)) return 0;\n return Math.min(last, Math.max(0, Math.trunc(index)));\n }\n}\n"]}
|
|
@@ -4,7 +4,7 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
4
4
|
* Headless, accessible switch (toggle) behavior.
|
|
5
5
|
*
|
|
6
6
|
* Markup contract (identifier: `stimeo--switch`):
|
|
7
|
-
* <button data-controller="stimeo--switch"
|
|
7
|
+
* <button type="button" data-controller="stimeo--switch"
|
|
8
8
|
* data-action="stimeo--switch#toggle keydown->stimeo--switch#onKeydown"
|
|
9
9
|
* role="switch" aria-checked="false">…</button>
|
|
10
10
|
*
|
|
@@ -14,9 +14,11 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
14
14
|
* @remarks
|
|
15
15
|
* Behavior only — the consumer owns all styling (typically keyed off the
|
|
16
16
|
* `[aria-checked="true"]` attribute). On a native `<button>` host, the browser
|
|
17
|
-
* already synthesizes a click for Space/Enter, so {@link onKeydown}
|
|
18
|
-
*
|
|
19
|
-
* such as `<div role="switch" tabindex="0"
|
|
17
|
+
* already synthesizes a click for Space/Enter, so {@link onKeydown} leaves the
|
|
18
|
+
* initial key to the browser and suppresses repeated keydowns. Non-button hosts
|
|
19
|
+
* such as `<div role="switch" tabindex="0">` are driven directly. Other native
|
|
20
|
+
* interactive hosts stand down because their checked/navigation semantics would
|
|
21
|
+
* conflict with `aria-checked` as this controller's sole source of truth.
|
|
20
22
|
*
|
|
21
23
|
* Behavior provided:
|
|
22
24
|
* - Click (or Space/Enter) toggles `aria-checked` between `"true"` and `"false"`.
|
|
@@ -29,13 +31,15 @@ declare class SwitchController extends Controller<HTMLElement> {
|
|
|
29
31
|
static events: readonly ["changed"];
|
|
30
32
|
/** Ensures the switch exposes a role and is keyboard-reachable. */
|
|
31
33
|
connect(): void;
|
|
34
|
+
/** Releases the explicit click guard and retained-element attribute observer. */
|
|
35
|
+
disconnect(): void;
|
|
32
36
|
/** Toggles the checked state. Bound via `data-action` (click). */
|
|
33
37
|
toggle(): void;
|
|
34
38
|
/**
|
|
35
|
-
* Activates the switch on Space/Enter for non-native hosts and
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
+
* Activates the switch on Space/Enter for non-native hosts and suppresses key
|
|
40
|
+
* repeat. Bound via `data-action` (keydown). A native `<button type="button">`
|
|
41
|
+
* owns the initial key-to-click synthesis; repeated keydowns are canceled before
|
|
42
|
+
* the browser can synthesize additional clicks.
|
|
39
43
|
*/
|
|
40
44
|
onKeydown(event: KeyboardEvent): void;
|
|
41
45
|
}
|
|
@@ -1,39 +1,181 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
3
|
// src/controllers/switch_controller.ts
|
|
4
|
+
|
|
5
|
+
// src/utils/default_attribute.ts
|
|
6
|
+
function setDefaultAttribute(element, name, value) {
|
|
7
|
+
if (element.hasAttribute(name)) return false;
|
|
8
|
+
element.setAttribute(name, value);
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// src/utils/focus_candidate.ts
|
|
13
|
+
function inheritsFieldsetDisabled(control) {
|
|
14
|
+
let fieldset = control.closest("fieldset[disabled]");
|
|
15
|
+
while (fieldset) {
|
|
16
|
+
const legend = Array.from(fieldset.children).find((child) => child.tagName === "LEGEND");
|
|
17
|
+
if (!legend?.contains(control)) return true;
|
|
18
|
+
fieldset = fieldset.parentElement?.closest("fieldset[disabled]") ?? null;
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/utils/interactive_host.ts
|
|
24
|
+
var INTERACTIVE_HOST_SELECTOR = "button, input, select, textarea, label, a[href], area[href], summary, details, audio[controls], video[controls], iframe, object, embed";
|
|
25
|
+
function isInteractiveHost(element) {
|
|
26
|
+
if (element.matches(INTERACTIVE_HOST_SELECTOR)) return true;
|
|
27
|
+
let current = element;
|
|
28
|
+
while (current) {
|
|
29
|
+
const raw = current.getAttribute("contenteditable");
|
|
30
|
+
if (raw !== null) {
|
|
31
|
+
const value = raw.trim().toLowerCase();
|
|
32
|
+
if (value === "false") return false;
|
|
33
|
+
if (value === "" || value === "true" || value === "plaintext-only") return true;
|
|
34
|
+
}
|
|
35
|
+
current = current.parentElement;
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/controllers/switch_controller.ts
|
|
41
|
+
var OBSERVED_ATTRIBUTES = [
|
|
42
|
+
"role",
|
|
43
|
+
"aria-checked",
|
|
44
|
+
"tabindex",
|
|
45
|
+
"type",
|
|
46
|
+
"href",
|
|
47
|
+
"contenteditable",
|
|
48
|
+
"controls"
|
|
49
|
+
];
|
|
50
|
+
var OBSERVED_ANCESTOR_ATTRIBUTES = ["contenteditable"];
|
|
4
51
|
var SwitchController = class extends Controller {
|
|
5
52
|
static actions = ["onKeydown", "toggle"];
|
|
6
53
|
static events = ["changed"];
|
|
54
|
+
/** Defaults this instance introduced and may therefore remove safely. */
|
|
55
|
+
#ownedDefaults = /* @__PURE__ */ new Set();
|
|
56
|
+
/** Controller writes that must not be mistaken for authored morph changes. */
|
|
57
|
+
#internalAttributeValues = /* @__PURE__ */ new Map();
|
|
58
|
+
#attributeObserver = new MutationObserver((records) => {
|
|
59
|
+
this.#releaseAuthoredDefaults(records);
|
|
60
|
+
this.#attributeObserver.disconnect();
|
|
61
|
+
this.#reconcileDefaults();
|
|
62
|
+
this.#observeAttributes();
|
|
63
|
+
});
|
|
64
|
+
/** Blocks disabled pointer/native-key activation before consumer click handlers. */
|
|
65
|
+
#onClickCapture = (event) => {
|
|
66
|
+
if (!this.#isSupportedHost || !this.#isActivationDisabled) return;
|
|
67
|
+
event.preventDefault();
|
|
68
|
+
event.stopImmediatePropagation();
|
|
69
|
+
};
|
|
7
70
|
/** Ensures the switch exposes a role and is keyboard-reachable. */
|
|
8
71
|
connect() {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
72
|
+
this.#reconcileDefaults();
|
|
73
|
+
this.element.addEventListener("click", this.#onClickCapture, true);
|
|
74
|
+
this.#observeAttributes();
|
|
75
|
+
}
|
|
76
|
+
/** Releases the explicit click guard and retained-element attribute observer. */
|
|
77
|
+
disconnect() {
|
|
78
|
+
this.element.removeEventListener("click", this.#onClickCapture, true);
|
|
79
|
+
this.#attributeObserver.disconnect();
|
|
80
|
+
this.#internalAttributeValues.clear();
|
|
18
81
|
}
|
|
19
82
|
/** Toggles the checked state. Bound via `data-action` (click). */
|
|
20
83
|
toggle() {
|
|
84
|
+
if (!this.#isSupportedHost || this.#isActivationDisabled) return;
|
|
21
85
|
this.#checked = !this.#checked;
|
|
22
86
|
}
|
|
23
87
|
/**
|
|
24
|
-
* Activates the switch on Space/Enter for non-native hosts and
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
88
|
+
* Activates the switch on Space/Enter for non-native hosts and suppresses key
|
|
89
|
+
* repeat. Bound via `data-action` (keydown). A native `<button type="button">`
|
|
90
|
+
* owns the initial key-to-click synthesis; repeated keydowns are canceled before
|
|
91
|
+
* the browser can synthesize additional clicks.
|
|
28
92
|
*/
|
|
29
93
|
onKeydown(event) {
|
|
30
94
|
if (event.defaultPrevented) return;
|
|
31
|
-
if (this
|
|
32
|
-
if (event.
|
|
33
|
-
if (
|
|
95
|
+
if (!this.#isSupportedHost) return;
|
|
96
|
+
if (event.key !== " " && event.key !== "Enter") return;
|
|
97
|
+
if (this.#isActivationDisabled) {
|
|
34
98
|
event.preventDefault();
|
|
35
|
-
|
|
99
|
+
event.stopImmediatePropagation();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (this.element instanceof HTMLButtonElement) {
|
|
103
|
+
if (event.repeat) event.preventDefault();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
event.preventDefault();
|
|
107
|
+
if (event.repeat) return;
|
|
108
|
+
this.toggle();
|
|
109
|
+
}
|
|
110
|
+
/** Re-adds only missing defaults, preserving every authored attribute value. */
|
|
111
|
+
#reconcileDefaults() {
|
|
112
|
+
if (!this.#isSupportedHost) {
|
|
113
|
+
this.#removeOwnedDefaults();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
this.#setOwnedDefault("role", "switch");
|
|
117
|
+
this.#setOwnedDefault("aria-checked", "false");
|
|
118
|
+
if (!(this.element instanceof HTMLButtonElement)) {
|
|
119
|
+
this.#setOwnedDefault("tabindex", "0");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** Records a newly introduced default without claiming authored markup. */
|
|
123
|
+
#setOwnedDefault(name, value) {
|
|
124
|
+
if (setDefaultAttribute(this.element, name, value)) this.#ownedDefaults.add(name);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Gives ownership back when a retained-element morph authors a value. Missing
|
|
128
|
+
* owned attributes stay owned so reconciliation can restore them.
|
|
129
|
+
*/
|
|
130
|
+
#releaseAuthoredDefaults(records) {
|
|
131
|
+
for (const record of records) {
|
|
132
|
+
if (record.target === this.element) {
|
|
133
|
+
const name = record.attributeName;
|
|
134
|
+
if (name && this.#ownedDefaults.has(name)) {
|
|
135
|
+
const value = this.element.getAttribute(name);
|
|
136
|
+
if (value !== null && this.#internalAttributeValues.get(name) !== value) {
|
|
137
|
+
this.#ownedDefaults.delete(name);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
this.#internalAttributeValues.clear();
|
|
143
|
+
}
|
|
144
|
+
/** Removes only defaults introduced by this instance when a host becomes invalid. */
|
|
145
|
+
#removeOwnedDefaults() {
|
|
146
|
+
for (const name of this.#ownedDefaults) this.element.removeAttribute(name);
|
|
147
|
+
this.#ownedDefaults.clear();
|
|
148
|
+
this.#internalAttributeValues.clear();
|
|
149
|
+
}
|
|
150
|
+
/** Watches retained host attributes that Turbo can morph without reconnecting. */
|
|
151
|
+
#observeAttributes() {
|
|
152
|
+
this.#attributeObserver.observe(this.element, {
|
|
153
|
+
attributes: true,
|
|
154
|
+
attributeFilter: OBSERVED_ATTRIBUTES
|
|
155
|
+
});
|
|
156
|
+
let ancestor = this.element.parentElement;
|
|
157
|
+
while (ancestor) {
|
|
158
|
+
this.#attributeObserver.observe(ancestor, {
|
|
159
|
+
attributes: true,
|
|
160
|
+
attributeFilter: OBSERVED_ANCESTOR_ATTRIBUTES
|
|
161
|
+
});
|
|
162
|
+
ancestor = ancestor.parentElement;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/** Whether the host has one activation model that this controller can own. */
|
|
166
|
+
get #isSupportedHost() {
|
|
167
|
+
if (this.element instanceof HTMLButtonElement) return this.element.type === "button";
|
|
168
|
+
return !isInteractiveHost(this.element);
|
|
169
|
+
}
|
|
170
|
+
/** Whether ARIA or native HTML semantics make the supported switch inoperable. */
|
|
171
|
+
get #isActivationDisabled() {
|
|
172
|
+
let current = this.element;
|
|
173
|
+
while (current) {
|
|
174
|
+
if (current.getAttribute("aria-disabled") === "true") return true;
|
|
175
|
+
current = current.parentElement;
|
|
36
176
|
}
|
|
177
|
+
if (!(this.element instanceof HTMLButtonElement)) return false;
|
|
178
|
+
return this.element.disabled || inheritsFieldsetDisabled(this.element);
|
|
37
179
|
}
|
|
38
180
|
/** Whether the switch is currently on. */
|
|
39
181
|
get #checked() {
|
|
@@ -41,7 +183,9 @@ var SwitchController = class extends Controller {
|
|
|
41
183
|
}
|
|
42
184
|
/** Reflects the new state on `aria-checked` and notifies listeners. */
|
|
43
185
|
set #checked(value) {
|
|
44
|
-
|
|
186
|
+
const reflected = value ? "true" : "false";
|
|
187
|
+
this.#internalAttributeValues.set("aria-checked", reflected);
|
|
188
|
+
this.element.setAttribute("aria-checked", reflected);
|
|
45
189
|
this.dispatch("changed", { detail: { checked: value } });
|
|
46
190
|
}
|
|
47
191
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/controllers/switch_controller.ts"],"names":[],"mappings":";;;AAyBO,IAAM,gBAAA,GAAN,cAA+B,UAAA,CAAwB;AAAA,EAC5D,OAAO,OAAA,GAAU,CAAC,WAAA,EAAa,QAAQ,CAAA;AAAA,EACvC,OAAO,MAAA,GAAS,CAAC,SAAS,CAAA;AAAA;AAAA,EAGjB,OAAA,GAAgB;AACvB,IAAA,IAAI,CAAC,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,MAAM,CAAA,EAAG;AACtC,MAAA,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,MAAA,EAAQ,QAAQ,CAAA;AAAA,IAC5C;AACA,IAAA,IAAI,CAAC,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,cAAc,CAAA,EAAG;AAC9C,MAAA,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,cAAA,EAAgB,OAAO,CAAA;AAAA,IACnD;AAIA,IAAA,IAAI,EAAE,KAAK,OAAA,YAAmB,iBAAA,CAAA,IAAsB,CAAC,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,UAAU,CAAA,EAAG;AAC1F,MAAA,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,UAAA,EAAY,GAAG,CAAA;AAAA,IAC3C;AAAA,EACF;AAAA;AAAA,EAGA,MAAA,GAAe;AACb,IAAA,IAAA,CAAK,QAAA,GAAW,CAAC,IAAA,CAAK,QAAA;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,UAAU,KAAA,EAA4B;AAGpC,IAAA,IAAI,MAAM,gBAAA,EAAkB;AAC5B,IAAA,IAAI,IAAA,CAAK,mBAAmB,iBAAA,EAAmB;AAC/C,IAAA,IAAI,MAAM,MAAA,EAAQ;AAClB,IAAA,IAAI,KAAA,CAAM,GAAA,KAAQ,GAAA,IAAO,KAAA,CAAM,QAAQ,OAAA,EAAS;AAC9C,MAAA,KAAA,CAAM,cAAA,EAAe;AACrB,MAAA,IAAA,CAAK,MAAA,EAAO;AAAA,IACd;AAAA,EACF;AAAA;AAAA,EAGA,IAAI,QAAA,GAAoB;AACtB,IAAA,OAAO,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,cAAc,CAAA,KAAM,MAAA;AAAA,EACvD;AAAA;AAAA,EAGA,IAAI,SAAS,KAAA,EAAgB;AAC3B,IAAA,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,cAAA,EAAgB,KAAA,GAAQ,SAAS,OAAO,CAAA;AAClE,IAAA,IAAA,CAAK,QAAA,CAAS,WAAW,EAAE,MAAA,EAAQ,EAAE,OAAA,EAAS,KAAA,IAAS,CAAA;AAAA,EACzD;AACF","file":"switch_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\n/**\n * Headless, accessible switch (toggle) behavior.\n *\n * Markup contract (identifier: `stimeo--switch`):\n * <button data-controller=\"stimeo--switch\"\n * data-action=\"stimeo--switch#toggle keydown->stimeo--switch#onKeydown\"\n * role=\"switch\" aria-checked=\"false\">…</button>\n *\n * Implements the WAI-ARIA APG **Switch** pattern. The controller element is the\n * switch itself; its on/off state is reflected solely through `aria-checked`.\n *\n * @remarks\n * Behavior only — the consumer owns all styling (typically keyed off the\n * `[aria-checked=\"true\"]` attribute). On a native `<button>` host, the browser\n * already synthesizes a click for Space/Enter, so {@link onKeydown} deliberately\n * does nothing there (to avoid a double toggle) and only drives non-button hosts\n * such as `<div role=\"switch\" tabindex=\"0\">`.\n *\n * Behavior provided:\n * - Click (or Space/Enter) toggles `aria-checked` between `\"true\"` and `\"false\"`.\n * - A `stimeo--switch:changed` event is dispatched on every toggle so the\n * consumer can react (its `detail.checked` carries the new boolean state).\n */\nexport class SwitchController extends Controller<HTMLElement> {\n static actions = [\"onKeydown\", \"toggle\"] as const;\n static events = [\"changed\"] as const;\n\n /** Ensures the switch exposes a role and is keyboard-reachable. */\n override connect(): void {\n if (!this.element.hasAttribute(\"role\")) {\n this.element.setAttribute(\"role\", \"switch\");\n }\n if (!this.element.hasAttribute(\"aria-checked\")) {\n this.element.setAttribute(\"aria-checked\", \"false\");\n }\n // Native <button> hosts are focusable already; a non-button host (e.g.\n // <div role=\"switch\">) needs an explicit tabindex to be keyboard-reachable,\n // otherwise the keyboard support below would be unreachable.\n if (!(this.element instanceof HTMLButtonElement) && !this.element.hasAttribute(\"tabindex\")) {\n this.element.setAttribute(\"tabindex\", \"0\");\n }\n }\n\n /** Toggles the checked state. Bound via `data-action` (click). */\n toggle(): void {\n this.#checked = !this.#checked;\n }\n\n /**\n * Activates the switch on Space/Enter for non-native hosts and prevents the\n * default Space scroll. Bound via `data-action` (keydown). Native `<button>`\n * hosts are skipped because the browser already turns Space/Enter into a click,\n * which would otherwise toggle the switch twice.\n */\n onKeydown(event: KeyboardEvent): void {\n // A descendant widget that already claimed the key must not ALSO toggle the switch —\n // composition depends on this yield.\n if (event.defaultPrevented) return;\n if (this.element instanceof HTMLButtonElement) return;\n if (event.repeat) return;\n if (event.key === \" \" || event.key === \"Enter\") {\n event.preventDefault();\n this.toggle();\n }\n }\n\n /** Whether the switch is currently on. */\n get #checked(): boolean {\n return this.element.getAttribute(\"aria-checked\") === \"true\";\n }\n\n /** Reflects the new state on `aria-checked` and notifies listeners. */\n set #checked(value: boolean) {\n this.element.setAttribute(\"aria-checked\", value ? \"true\" : \"false\");\n this.dispatch(\"changed\", { detail: { checked: value } });\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/default_attribute.ts","../../src/utils/focus_candidate.ts","../../src/utils/interactive_host.ts","../../src/controllers/switch_controller.ts"],"names":[],"mappings":"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","file":"switch_controller.js","sourcesContent":["/**\n * Adds an attribute default without displacing an authored value.\n *\n * Attribute presence — including an authored empty string — is the ownership\n * boundary. The return value lets a caller remember that it supplied the\n * default when that caller must later restore the authored state.\n *\n * @param element - Element that owns the attribute\n * @param name - Attribute name\n * @param value - Value to write only when the attribute is absent\n * @returns Whether this call added the attribute\n */\nexport function setDefaultAttribute(element: Element, name: string, value: string): boolean {\n if (element.hasAttribute(name)) return false;\n element.setAttribute(name, value);\n return true;\n}\n","/**\n * Whether a disabled `<fieldset>` ancestor actually reaches `control`.\n *\n * HTML exempts the contents of a fieldset's **first direct-child `<legend>`**, and\n * the exemption is per fieldset — a control legal in one legend can still be\n * disabled by a second, outer fieldset, so the walk continues upward.\n *\n * Exported for the consumer that needs this rule *without* the rest of\n * {@link canTakeFocus}: `toolbar` bounds its own `hidden` walk at the toolbar\n * root, which is a different rule, so it composes the two itself.\n */\nexport function inheritsFieldsetDisabled(control: HTMLElement): boolean {\n let fieldset: Element | null = control.closest(\"fieldset[disabled]\");\n while (fieldset) {\n const legend = Array.from(fieldset.children).find((child) => child.tagName === \"LEGEND\");\n if (!legend?.contains(control)) return true;\n fieldset = fieldset.parentElement?.closest(\"fieldset[disabled]\") ?? null;\n }\n return false;\n}\n\n/**\n * Whether an element can actually take focus, checked **before** `focus()` runs.\n *\n * A controller that must move focus off a control it is about to disable or hide\n * picks a destination and calls `focus()` on it. If that destination cannot take\n * focus, the call **fails silently**: `hidden` and natively `disabled` elements\n * swallow it, so the caret stays in the subtree that is disappearing and lands on\n * `<body>` a frame later — the exact outcome the rescue exists to prevent, minus\n * any signal that it happened.\n *\n * Testing after the fact is the obvious alternative and is deliberately not the\n * rule. Reading `document.activeElement` back only works in a real browser, and\n * looping over candidates that way performs a real focus move per failure —\n * observable to assistive technology. Checking first costs nothing and catches\n * the cases that actually occur.\n *\n * **`aria-disabled` is not disqualifying.** It is the attribute an author uses\n * for a control that must stay *discoverable*, and the roving contract keeps\n * such items reachable. Only the three conditions that make the platform refuse\n * focus are checked: `hidden` (an ancestor's counts), the native `disabled`\n * property, and `disabled` inherited from an ancestor `fieldset`. CSS-only\n * invisibility is not detectable here and stays the consumer's problem.\n *\n * Reading `:disabled` instead of walking the fieldset chain would be shorter, but\n * that pseudo-class is not evaluated consistently outside real browsers and this\n * has to be right headlessly too. happy-dom in particular focuses a `<button>`\n * inside a disabled fieldset where a real engine refuses, so the inheritance is\n * spelled out rather than delegated.\n *\n * What a consumer does when nothing survives is its own call: some fall back to\n * their landmark, while a widget whose caret already sits somewhere legitimate\n * refuses the move outright rather than relocating it.\n *\n * @example\n * ```ts\n * const target = candidates.find(canTakeFocus);\n * if (target) target.focus();\n * else {\n * this.#tabindex.lend(this.element); // nothing left: fall back to the landmark\n * this.element.focus();\n * }\n * ```\n *\n * @param element - the candidate destination\n */\nexport function canTakeFocus(element: HTMLElement): boolean {\n if (element.closest(\"[hidden]\")) return false;\n if (!(\"disabled\" in element)) return true;\n if ((element as HTMLElement & { disabled: boolean }).disabled) return false;\n return !inheritsFieldsetDisabled(element);\n}\n","/**\n * Native interactive elements whose built-in activation must not be replaced\n * wholesale by a controller. Deliberately excludes `[tabindex]`: a generic\n * element made focusable is the supported host for several headless patterns.\n * Contenteditable is handled by {@link isInteractiveHost} because its state is\n * inherited and its keywords are ASCII-case-insensitive.\n */\nexport const INTERACTIVE_HOST_SELECTOR =\n \"button, input, select, textarea, label, a[href], area[href], summary, details, audio[controls], video[controls], iframe, object, embed\";\n\n/**\n * Whether an element owns native activation or an inherited editing surface.\n *\n * The `contenteditable` missing and invalid states inherit. Walking explicitly\n * also keeps `contenteditable=\"false\"` as a real boundary inside an editable\n * ancestor instead of treating any ancestor attribute as decisive.\n */\nexport function isInteractiveHost(element: HTMLElement): boolean {\n if (element.matches(INTERACTIVE_HOST_SELECTOR)) return true;\n\n let current: HTMLElement | null = element;\n while (current) {\n const raw = current.getAttribute(\"contenteditable\");\n if (raw !== null) {\n const value = raw.trim().toLowerCase();\n if (value === \"false\") return false;\n if (value === \"\" || value === \"true\" || value === \"plaintext-only\") return true;\n }\n current = current.parentElement;\n }\n return false;\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { setDefaultAttribute } from \"../utils/default_attribute\";\nimport { inheritsFieldsetDisabled } from \"../utils/focus_candidate\";\nimport { isInteractiveHost } from \"../utils/interactive_host\";\n\n/** Attributes whose in-place changes can alter the host or its required defaults. */\nconst OBSERVED_ATTRIBUTES = [\n \"role\",\n \"aria-checked\",\n \"tabindex\",\n \"type\",\n \"href\",\n \"contenteditable\",\n \"controls\",\n];\n\n/** Ancestor attributes whose inherited state can change the supported host. */\nconst OBSERVED_ANCESTOR_ATTRIBUTES = [\"contenteditable\"];\n\n/**\n * Headless, accessible switch (toggle) behavior.\n *\n * Markup contract (identifier: `stimeo--switch`):\n * <button type=\"button\" data-controller=\"stimeo--switch\"\n * data-action=\"stimeo--switch#toggle keydown->stimeo--switch#onKeydown\"\n * role=\"switch\" aria-checked=\"false\">…</button>\n *\n * Implements the WAI-ARIA APG **Switch** pattern. The controller element is the\n * switch itself; its on/off state is reflected solely through `aria-checked`.\n *\n * @remarks\n * Behavior only — the consumer owns all styling (typically keyed off the\n * `[aria-checked=\"true\"]` attribute). On a native `<button>` host, the browser\n * already synthesizes a click for Space/Enter, so {@link onKeydown} leaves the\n * initial key to the browser and suppresses repeated keydowns. Non-button hosts\n * such as `<div role=\"switch\" tabindex=\"0\">` are driven directly. Other native\n * interactive hosts stand down because their checked/navigation semantics would\n * conflict with `aria-checked` as this controller's sole source of truth.\n *\n * Behavior provided:\n * - Click (or Space/Enter) toggles `aria-checked` between `\"true\"` and `\"false\"`.\n * - A `stimeo--switch:changed` event is dispatched on every toggle so the\n * consumer can react (its `detail.checked` carries the new boolean state).\n */\nexport class SwitchController extends Controller<HTMLElement> {\n static actions = [\"onKeydown\", \"toggle\"] as const;\n static events = [\"changed\"] as const;\n\n /** Defaults this instance introduced and may therefore remove safely. */\n readonly #ownedDefaults = new Set<string>();\n /** Controller writes that must not be mistaken for authored morph changes. */\n readonly #internalAttributeValues = new Map<string, string>();\n\n readonly #attributeObserver = new MutationObserver((records) => {\n this.#releaseAuthoredDefaults(records);\n // Stop observation while restoring defaults so the writes do not schedule a\n // second, empty callback batch. Re-observe synchronously before returning.\n this.#attributeObserver.disconnect();\n this.#reconcileDefaults();\n this.#observeAttributes();\n });\n\n /** Blocks disabled pointer/native-key activation before consumer click handlers. */\n readonly #onClickCapture = (event: MouseEvent): void => {\n if (!this.#isSupportedHost || !this.#isActivationDisabled) return;\n event.preventDefault();\n event.stopImmediatePropagation();\n };\n\n /** Ensures the switch exposes a role and is keyboard-reachable. */\n override connect(): void {\n this.#reconcileDefaults();\n this.element.addEventListener(\"click\", this.#onClickCapture, true);\n this.#observeAttributes();\n }\n\n /** Releases the explicit click guard and retained-element attribute observer. */\n override disconnect(): void {\n this.element.removeEventListener(\"click\", this.#onClickCapture, true);\n this.#attributeObserver.disconnect();\n this.#internalAttributeValues.clear();\n }\n\n /** Toggles the checked state. Bound via `data-action` (click). */\n toggle(): void {\n if (!this.#isSupportedHost || this.#isActivationDisabled) return;\n this.#checked = !this.#checked;\n }\n\n /**\n * Activates the switch on Space/Enter for non-native hosts and suppresses key\n * repeat. Bound via `data-action` (keydown). A native `<button type=\"button\">`\n * owns the initial key-to-click synthesis; repeated keydowns are canceled before\n * the browser can synthesize additional clicks.\n */\n onKeydown(event: KeyboardEvent): void {\n // A descendant widget that already claimed the key must not ALSO toggle the switch —\n // composition depends on this yield.\n if (event.defaultPrevented) return;\n if (!this.#isSupportedHost) return;\n if (event.key !== \" \" && event.key !== \"Enter\") return;\n if (this.#isActivationDisabled) {\n event.preventDefault();\n event.stopImmediatePropagation();\n return;\n }\n if (this.element instanceof HTMLButtonElement) {\n if (event.repeat) event.preventDefault();\n return;\n }\n event.preventDefault();\n if (event.repeat) return;\n this.toggle();\n }\n\n /** Re-adds only missing defaults, preserving every authored attribute value. */\n #reconcileDefaults(): void {\n if (!this.#isSupportedHost) {\n this.#removeOwnedDefaults();\n return;\n }\n this.#setOwnedDefault(\"role\", \"switch\");\n this.#setOwnedDefault(\"aria-checked\", \"false\");\n // Native buttons are focusable already; generic hosts need a Tab stop for\n // the keyboard action above to be reachable.\n if (!(this.element instanceof HTMLButtonElement)) {\n this.#setOwnedDefault(\"tabindex\", \"0\");\n }\n }\n\n /** Records a newly introduced default without claiming authored markup. */\n #setOwnedDefault(name: string, value: string): void {\n if (setDefaultAttribute(this.element, name, value)) this.#ownedDefaults.add(name);\n }\n\n /**\n * Gives ownership back when a retained-element morph authors a value. Missing\n * owned attributes stay owned so reconciliation can restore them.\n */\n #releaseAuthoredDefaults(records: MutationRecord[]): void {\n for (const record of records) {\n if (record.target === this.element) {\n const name = record.attributeName;\n if (name && this.#ownedDefaults.has(name)) {\n const value = this.element.getAttribute(name);\n if (value !== null && this.#internalAttributeValues.get(name) !== value) {\n this.#ownedDefaults.delete(name);\n }\n }\n }\n }\n this.#internalAttributeValues.clear();\n }\n\n /** Removes only defaults introduced by this instance when a host becomes invalid. */\n #removeOwnedDefaults(): void {\n for (const name of this.#ownedDefaults) this.element.removeAttribute(name);\n this.#ownedDefaults.clear();\n this.#internalAttributeValues.clear();\n }\n\n /** Watches retained host attributes that Turbo can morph without reconnecting. */\n #observeAttributes(): void {\n this.#attributeObserver.observe(this.element, {\n attributes: true,\n attributeFilter: OBSERVED_ATTRIBUTES,\n });\n let ancestor = this.element.parentElement;\n while (ancestor) {\n this.#attributeObserver.observe(ancestor, {\n attributes: true,\n attributeFilter: OBSERVED_ANCESTOR_ATTRIBUTES,\n });\n ancestor = ancestor.parentElement;\n }\n }\n\n /** Whether the host has one activation model that this controller can own. */\n get #isSupportedHost(): boolean {\n if (this.element instanceof HTMLButtonElement) return this.element.type === \"button\";\n return !isInteractiveHost(this.element);\n }\n\n /** Whether ARIA or native HTML semantics make the supported switch inoperable. */\n get #isActivationDisabled(): boolean {\n let current: HTMLElement | null = this.element;\n while (current) {\n if (current.getAttribute(\"aria-disabled\") === \"true\") return true;\n current = current.parentElement;\n }\n if (!(this.element instanceof HTMLButtonElement)) return false;\n return this.element.disabled || inheritsFieldsetDisabled(this.element);\n }\n\n /** Whether the switch is currently on. */\n get #checked(): boolean {\n return this.element.getAttribute(\"aria-checked\") === \"true\";\n }\n\n /** Reflects the new state on `aria-checked` and notifies listeners. */\n set #checked(value: boolean) {\n const reflected = value ? \"true\" : \"false\";\n this.#internalAttributeValues.set(\"aria-checked\", reflected);\n this.element.setAttribute(\"aria-checked\", reflected);\n this.dispatch(\"changed\", { detail: { checked: value } });\n }\n}\n"]}
|
|
@@ -5,19 +5,20 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
5
5
|
*
|
|
6
6
|
* Markup contract (identifier: `stimeo--tags-input`):
|
|
7
7
|
* <div data-controller="stimeo--tags-input"
|
|
8
|
-
* data-stimeo--tags-input-delimiter-value=","
|
|
8
|
+
* data-stimeo--tags-input-delimiter-value=","
|
|
9
|
+
* data-stimeo--tags-input-announce-text-value="{label} added; {count} total"
|
|
10
|
+
* data-stimeo--tags-input-announce-removed-text-value="{label} removed; {count} total">
|
|
9
11
|
* <ul role="list" aria-label="Tags" data-stimeo--tags-input-target="tags"></ul>
|
|
10
12
|
* <input type="text" aria-label="Add tag" aria-describedby="tags-help"
|
|
11
13
|
* data-stimeo--tags-input-target="input"
|
|
12
14
|
* data-action="keydown->stimeo--tags-input#onKeydown" />
|
|
13
|
-
* <span role="status" aria-live="polite" class="visually-hidden"
|
|
14
|
-
* data-stimeo--tags-input-target="status"></span>
|
|
15
15
|
* <div data-stimeo--tags-input-target="fields"></div>
|
|
16
16
|
* <template data-stimeo--tags-input-target="tagTemplate">
|
|
17
17
|
* <li role="listitem" data-stimeo--tags-input-target="tag">
|
|
18
|
-
* <span data-tags-input-
|
|
18
|
+
* <span data-stimeo--tags-input-target="label"></span>
|
|
19
19
|
* <!-- Removal is delegated on the tags container; no per-chip action needed. -->
|
|
20
|
-
* <button type="button" tabindex="-1"
|
|
20
|
+
* <button type="button" tabindex="-1" aria-label="Remove {label}"
|
|
21
|
+
* data-stimeo--tags-input-target="remove">×</button>
|
|
21
22
|
* </li>
|
|
22
23
|
* </template>
|
|
23
24
|
* </div>
|
|
@@ -31,13 +32,17 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
31
32
|
* - `Enter` or the configured delimiter commits the trimmed input as a tag.
|
|
32
33
|
* - Empty / duplicate / over-limit additions are rejected with
|
|
33
34
|
* `stimeo--tags-input:reject`; duplicates are allowed when `allowDuplicates`.
|
|
34
|
-
* - Tags render from the `tagTemplate
|
|
35
|
+
* - Tags render transactionally from the `tagTemplate`; its author-localized
|
|
36
|
+
* remove-button `aria-label` expands `{label}` / `{value}`. The
|
|
35
37
|
* `fields` container mirrors the tag set as `name`d hidden inputs for form
|
|
36
|
-
* submission
|
|
38
|
+
* submission. User changes dispatch `stimeo--tags-input:change`; DOM/Turbo tag
|
|
39
|
+
* changes dispatch `stimeo--tags-input:reconcile`; authored templates route
|
|
40
|
+
* localized addition/removal text to the shared announcer.
|
|
37
41
|
* - The remove buttons form one roving Tab stop: `ArrowLeft`/`ArrowRight` move
|
|
38
42
|
* between them (right past the end returns to the input), `Delete`/`Backspace`
|
|
39
43
|
* delete the focused tag, and `Backspace` on an empty input deletes the last.
|
|
40
|
-
* - Removing a
|
|
44
|
+
* - Removing from a chip moves focus to its neighbor (else the input); removing
|
|
45
|
+
* via empty-input `Backspace` keeps focus in the input for continued editing.
|
|
41
46
|
*/
|
|
42
47
|
declare class TagsInputController extends Controller<HTMLElement> {
|
|
43
48
|
#private;
|
|
@@ -59,23 +64,34 @@ declare class TagsInputController extends Controller<HTMLElement> {
|
|
|
59
64
|
type: StringConstructor;
|
|
60
65
|
default: string;
|
|
61
66
|
};
|
|
67
|
+
announceText: {
|
|
68
|
+
type: StringConstructor;
|
|
69
|
+
default: string;
|
|
70
|
+
};
|
|
71
|
+
announceRemovedText: {
|
|
72
|
+
type: StringConstructor;
|
|
73
|
+
default: string;
|
|
74
|
+
};
|
|
62
75
|
};
|
|
63
76
|
static actions: readonly ["onKeydown"];
|
|
64
|
-
static events: readonly ["change", "reject"];
|
|
77
|
+
static events: readonly ["change", "reconcile", "reject"];
|
|
65
78
|
readonly inputTarget: HTMLInputElement;
|
|
66
79
|
readonly tagsTarget: HTMLElement;
|
|
67
80
|
readonly tagTargets: HTMLElement[];
|
|
68
81
|
readonly tagTemplateTarget: HTMLTemplateElement;
|
|
69
|
-
readonly
|
|
82
|
+
readonly labelTargets: HTMLElement[];
|
|
83
|
+
readonly removeTargets: HTMLButtonElement[];
|
|
70
84
|
readonly fieldsTarget: HTMLElement;
|
|
71
|
-
readonly hasStatusTarget: boolean;
|
|
72
85
|
readonly hasFieldsTarget: boolean;
|
|
73
86
|
readonly hasTagTemplateTarget: boolean;
|
|
74
87
|
readonly hasInputTarget: boolean;
|
|
88
|
+
readonly hasTagsTarget: boolean;
|
|
75
89
|
delimiterValue: string;
|
|
76
90
|
maxValue: number;
|
|
77
91
|
allowDuplicatesValue: boolean;
|
|
78
92
|
nameValue: string;
|
|
93
|
+
announceTextValue: string;
|
|
94
|
+
announceRemovedTextValue: string;
|
|
79
95
|
/** Wires tag-list keyboard navigation and removal, and seeds the single Tab stop. */
|
|
80
96
|
connect(): void;
|
|
81
97
|
/** Releases the delegated listeners so no handler outlives the element. */
|
|
@@ -84,6 +100,20 @@ declare class TagsInputController extends Controller<HTMLElement> {
|
|
|
84
100
|
inputTargetConnected(input: HTMLInputElement): void;
|
|
85
101
|
/** Removes composition listeners when the active input is replaced or removed. */
|
|
86
102
|
inputTargetDisconnected(input: HTMLInputElement): void;
|
|
103
|
+
/** Rebinds delegation and derived state when the tags container is replaced. */
|
|
104
|
+
tagsTargetConnected(_tags: HTMLElement): void;
|
|
105
|
+
/** Releases only the row that disconnected, leaving a newer target intact. */
|
|
106
|
+
tagsTargetDisconnected(tags: HTMLElement): void;
|
|
107
|
+
/** Reconciles a tag inserted by Turbo, a server render, or another controller. */
|
|
108
|
+
tagTargetConnected(_tag: HTMLElement): void;
|
|
109
|
+
/** Reconciles a tag removed by Turbo, a server render, or another controller. */
|
|
110
|
+
tagTargetDisconnected(_tag: HTMLElement): void;
|
|
111
|
+
/** Seeds a fields target inserted after connect. */
|
|
112
|
+
fieldsTargetConnected(): void;
|
|
113
|
+
/** Rebuilds submitted fields when the public name changes at runtime. */
|
|
114
|
+
nameValueChanged(): void;
|
|
115
|
+
/** Recomputes the full hook when the cap changes at runtime. */
|
|
116
|
+
maxValueChanged(): void;
|
|
87
117
|
/** Commits on `Enter`/delimiter and deletes the last tag on empty `Backspace`. */
|
|
88
118
|
onKeydown(event: KeyboardEvent): void;
|
|
89
119
|
}
|