stimeo-ui 0.14.0 → 0.16.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 +212 -0
- package/README.md +120 -0
- package/dist/cable/index.js +123 -29
- package/dist/cable/index.js.map +1 -1
- package/dist/controllers/accordion_controller.d.ts +30 -3
- package/dist/controllers/accordion_controller.js +98 -9
- package/dist/controllers/accordion_controller.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.d.ts +8 -7
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- package/dist/controllers/announcer_controller.d.ts +3 -2
- package/dist/controllers/announcer_controller.js +96 -62
- package/dist/controllers/announcer_controller.js.map +1 -1
- package/dist/controllers/auto_submit_controller.js +83 -7
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.js +1 -1
- package/dist/controllers/avatar_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.d.ts +10 -7
- package/dist/controllers/breadcrumb_controller.js +38 -11
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/bulk_select_controller.d.ts +4 -4
- package/dist/controllers/bulk_select_controller.js +7 -6
- package/dist/controllers/bulk_select_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.d.ts +101 -23
- package/dist/controllers/calendar_controller.js +340 -123
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +67 -22
- package/dist/controllers/carousel_controller.js +263 -38
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/character_counter_controller.d.ts +1 -1
- package/dist/controllers/character_counter_controller.js +40 -2
- package/dist/controllers/character_counter_controller.js.map +1 -1
- package/dist/controllers/checkbox_controller.js +81 -12
- package/dist/controllers/checkbox_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.d.ts +28 -2
- package/dist/controllers/clipboard_controller.js +63 -5
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.d.ts +25 -1
- package/dist/controllers/collapsible_controller.js +99 -14
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +29 -8
- package/dist/controllers/color_picker_controller.js +80 -34
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.d.ts +10 -1
- package/dist/controllers/combobox_controller.js +106 -16
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.d.ts +3 -3
- package/dist/controllers/command_palette_controller.js +35 -3
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/conditional_fields_controller.js +85 -17
- package/dist/controllers/conditional_fields_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.js +3 -0
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +6 -0
- package/dist/controllers/context_menu_controller.js +32 -12
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/count_up_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +30 -1
- package/dist/controllers/countdown_controller.js +129 -26
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +77 -16
- package/dist/controllers/currency_input_controller.js +221 -67
- package/dist/controllers/currency_input_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.d.ts +63 -18
- package/dist/controllers/data_grid_controller.js +195 -29
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +41 -6
- package/dist/controllers/date_range_picker_controller.js +151 -30
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.d.ts +10 -3
- package/dist/controllers/dialog_controller.js +35 -8
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.js +22 -4
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/dirty_form_controller.d.ts +2 -2
- package/dist/controllers/dirty_form_controller.js +46 -13
- package/dist/controllers/dirty_form_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.js +1 -0
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.d.ts +18 -10
- package/dist/controllers/drawer_controller.js +54 -19
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/dropdown_controller.d.ts +9 -3
- package/dist/controllers/dropdown_controller.js +36 -9
- package/dist/controllers/dropdown_controller.js.map +1 -1
- package/dist/controllers/editable_controller.js +34 -0
- package/dist/controllers/editable_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.js +144 -51
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/filter_controller.d.ts +10 -4
- package/dist/controllers/filter_controller.js +20 -6
- package/dist/controllers/filter_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +26 -6
- package/dist/controllers/flash_controller.js +432 -71
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.js +1 -0
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.js +7 -5
- package/dist/controllers/form_field_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js +19 -13
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.js +45 -8
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.js +82 -25
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +10 -2
- package/dist/controllers/hover_card_controller.js +40 -14
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +16 -3
- package/dist/controllers/idle_controller.js +90 -5
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.d.ts +5 -2
- package/dist/controllers/input_mask_controller.js +65 -9
- package/dist/controllers/input_mask_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.js +3 -0
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.js +11 -2
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.d.ts +50 -8
- package/dist/controllers/listbox_controller.js +203 -45
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.js +10 -5
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +3 -3
- package/dist/controllers/masonry_controller.js +31 -15
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menu_controller.d.ts +9 -3
- package/dist/controllers/menu_controller.js +45 -16
- package/dist/controllers/menu_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.d.ts +11 -0
- package/dist/controllers/menubar_controller.js +58 -24
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.js +9 -5
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +18 -3
- package/dist/controllers/multi_select_controller.js +278 -104
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/navigation_menu_controller.d.ts +11 -0
- package/dist/controllers/navigation_menu_controller.js +48 -15
- package/dist/controllers/navigation_menu_controller.js.map +1 -1
- package/dist/controllers/nested_form_controller.js +37 -8
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.js +9 -1
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +36 -8
- package/dist/controllers/number_input_controller.js +124 -21
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/optimistic_controller.js +42 -5
- package/dist/controllers/optimistic_controller.js.map +1 -1
- package/dist/controllers/otp_controller.d.ts +22 -7
- package/dist/controllers/otp_controller.js +198 -55
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.d.ts +8 -12
- package/dist/controllers/overflow_indicator_controller.js +115 -21
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.d.ts +26 -6
- package/dist/controllers/overflow_menu_controller.js +141 -44
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.d.ts +19 -11
- package/dist/controllers/pagination_controller.js +74 -28
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +15 -1
- package/dist/controllers/password_reveal_controller.js +59 -2
- package/dist/controllers/password_reveal_controller.js.map +1 -1
- package/dist/controllers/persist_controller.js +30 -8
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.js +131 -52
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +9 -3
- package/dist/controllers/popover_controller.js +45 -11
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +1 -1
- package/dist/controllers/portal_controller.js +6 -2
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/preview_guard_controller.js +16 -1
- package/dist/controllers/preview_guard_controller.js.map +1 -1
- package/dist/controllers/progress_controller.js +8 -4
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.d.ts +6 -4
- package/dist/controllers/radio_group_controller.js +42 -17
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.d.ts +49 -1
- package/dist/controllers/range_slider_controller.js +88 -42
- package/dist/controllers/range_slider_controller.js.map +1 -1
- package/dist/controllers/rating_controller.d.ts +14 -3
- package/dist/controllers/rating_controller.js +39 -15
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/read_more_controller.d.ts +24 -2
- package/dist/controllers/read_more_controller.js +100 -7
- package/dist/controllers/read_more_controller.js.map +1 -1
- package/dist/controllers/reading_progress_controller.js +65 -19
- package/dist/controllers/reading_progress_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.js +10 -5
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/resizable_controller.d.ts +16 -2
- package/dist/controllers/resizable_controller.js +82 -22
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.js +75 -27
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_restore_controller.js +37 -16
- package/dist/controllers/scroll_restore_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.js +49 -30
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.d.ts +3 -2
- package/dist/controllers/scrollspy_controller.js +71 -26
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.d.ts +41 -14
- package/dist/controllers/separator_controller.js +66 -37
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.d.ts +20 -3
- package/dist/controllers/sidebar_controller.js +77 -18
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.js +6 -1
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +45 -7
- package/dist/controllers/slider_controller.js +82 -47
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/smart_sticky_header_controller.js +60 -26
- package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
- package/dist/controllers/sortable_controller.js +17 -2
- package/dist/controllers/sortable_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.js +10 -2
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +19 -17
- package/dist/controllers/step_indicator_controller.js +18 -17
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.d.ts +34 -9
- package/dist/controllers/stepper_controller.js +101 -19
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +19 -0
- package/dist/controllers/stick_to_bottom_controller.js +104 -8
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +3 -2
- package/dist/controllers/submit_once_controller.js +45 -9
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.d.ts +29 -7
- package/dist/controllers/switch_controller.js +101 -10
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.d.ts +12 -0
- package/dist/controllers/tabs_controller.js +21 -2
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +15 -3
- package/dist/controllers/tags_input_controller.js +209 -59
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.js +29 -3
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/theme_controller.d.ts +20 -3
- package/dist/controllers/theme_controller.js +64 -14
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.js +23 -8
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toast_controller.d.ts +55 -15
- package/dist/controllers/toast_controller.js +451 -105
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.d.ts +49 -7
- package/dist/controllers/toggle_group_controller.js +159 -23
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.js +32 -0
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +8 -0
- package/dist/controllers/tooltip_controller.js +39 -13
- package/dist/controllers/tooltip_controller.js.map +1 -1
- package/dist/controllers/transition_controller.js +4 -0
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +39 -8
- package/dist/controllers/tree_view_controller.js +169 -16
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.d.ts +28 -1
- package/dist/index.js +5002 -1911
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +72 -6
- package/dist/inspector/cli.js +262 -51
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +309 -51
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +40 -40
- package/dist/inspector/manifest.json +529 -61
- package/dist/positioning/index.js +2 -0
- package/dist/positioning/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -141,15 +141,17 @@ var MicrotaskCoalescer = class {
|
|
|
141
141
|
|
|
142
142
|
// src/controllers/form_field_controller.ts
|
|
143
143
|
var OBSERVED_ATTRIBUTES = ["hidden", "id"];
|
|
144
|
-
var FormFieldController = class
|
|
144
|
+
var FormFieldController = class extends Controller {
|
|
145
145
|
static targets = ["control", "description", "error"];
|
|
146
146
|
static values = {
|
|
147
147
|
focusOnError: { type: Boolean, default: false }
|
|
148
148
|
};
|
|
149
149
|
static actions = ["clearError", "setError"];
|
|
150
150
|
static events = ["validate"];
|
|
151
|
-
/** Root attribute (CSS hook) reflecting the invalid state. */
|
|
152
|
-
|
|
151
|
+
/** Root attribute (CSS hook) reflecting the invalid state, in this controller's namespace. */
|
|
152
|
+
get #invalidAttribute() {
|
|
153
|
+
return `data-${this.identifier}-invalid`;
|
|
154
|
+
}
|
|
153
155
|
/** Collapses one target/morph batch into one silent ARIA reconciliation. */
|
|
154
156
|
#reconcile = new MicrotaskCoalescer(() => this.#reconcileDom());
|
|
155
157
|
/** Returns borrowed control ARIA before Turbo snapshots the page. */
|
|
@@ -173,7 +175,7 @@ var FormFieldController = class _FormFieldController extends Controller {
|
|
|
173
175
|
this.#ensureAssociationIds();
|
|
174
176
|
this.#beforeCache.activate();
|
|
175
177
|
if (!this.#initialized) {
|
|
176
|
-
this.#explicitInvalid = this.element.hasAttribute(
|
|
178
|
+
this.#explicitInvalid = this.element.hasAttribute(this.#invalidAttribute) && this.#shownErrors().length === 0;
|
|
177
179
|
this.#initialized = true;
|
|
178
180
|
}
|
|
179
181
|
this.#reconcileDom();
|
|
@@ -270,7 +272,7 @@ var FormFieldController = class _FormFieldController extends Controller {
|
|
|
270
272
|
this.#adoptCurrentControl();
|
|
271
273
|
const shown = this.#shownErrors();
|
|
272
274
|
const invalid = this.#explicitInvalid || shown.length > 0;
|
|
273
|
-
this.element.toggleAttribute(
|
|
275
|
+
this.element.toggleAttribute(this.#invalidAttribute, invalid);
|
|
274
276
|
const control = this.#activeControl;
|
|
275
277
|
if (!control) return;
|
|
276
278
|
this.#ariaInvalid.write(control, invalid ? "true" : "false");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/announce.ts","../../src/utils/aria_ids.ts","../../src/utils/attribute_lease.ts","../../src/utils/before_cache_reset.ts","../../src/utils/event_owner.ts","../../src/utils/microtask_coalescer.ts","../../src/controllers/form_field_controller.ts"],"names":[],"mappings":"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","file":"form_field_controller.js","sourcesContent":["/**\n * Sends one message to the page's shared `stimeo--announcer`.\n *\n * A component that has to reach assistive tech does not carry a live region of its\n * own: a region only announces what changes *after* assistive tech already knows\n * about it, which a region that appears (or is un-hidden) with its message cannot\n * satisfy. The one region that can is the announcer sitting in the page from the\n * start, so state changes are handed to it as an event and it does the reading.\n *\n * The event goes to `window` because the announcer is usually a sibling high in the\n * document rather than an ancestor of the component dispatching it.\n *\n * Wording comes from the consumer — the library ships no English strings — so an\n * empty message is silently dropped and nothing is announced.\n *\n * @example\n * ```ts\n * announce(this.announceTextValue, { assertive: false });\n * ```\n */\nexport function announce(message: string, options: { assertive?: boolean } = {}): void {\n const text = message.trim();\n if (text.length === 0) return;\n window.dispatchEvent(\n new CustomEvent(\"stimeo--announcer:announce\", {\n detail: { message: text, assertive: options.assertive === true },\n }),\n );\n}\n\n/**\n * Fills `{name}` placeholders in an announcement template from `values`.\n *\n * The same substitution the value-text templates use, so a consumer writes\n * `\"{percent}% complete\"` in one attribute and gets the same rules everywhere. A\n * placeholder with no matching entry is left as authored rather than blanked, which\n * keeps a typo visible instead of silently swallowing the word.\n */\nexport function fillTemplate(template: string, values: Record<string, string | number>): string {\n return template.replace(/\\{([a-zA-Z][a-zA-Z0-9]*)\\}/g, (match, name: string) => {\n const replacement = values[name];\n return replacement === undefined ? match : String(replacement);\n });\n}\n","/**\n * Minimal id primitives for wiring ARIA relationships.\n *\n * Accessible relationships such as `aria-describedby`, `aria-errormessage`,\n * `aria-labelledby`, and `aria-activedescendant` reference their targets by DOM\n * `id`. When a controller must establish such a relationship but the consumer's\n * markup left the target without an `id`, it needs to mint one that is stable for\n * the page's lifetime and guaranteed not to collide with another generated id.\n *\n * These helpers own *only* id generation/assignment. The linking policy (which\n * attribute, what order, when to add or remove it) stays in each controller —\n * this deliberately avoids a premature generic \"ARIA linker\" abstraction.\n */\n\n/**\n * Monotonic counter backing {@link uniqueId}. Module-scoped so every generated\n * id is unique across all controller instances sharing this module in a document.\n */\nlet counter = 0;\n\n/**\n * Returns a unique, DOM-id-safe string of the form `` `${prefix}-${n}` `` where\n * `n` increases on each call.\n *\n * The counter alone guarantees uniqueness against other *generated* ids, but a\n * consumer-authored element could already own `` `${prefix}-${n}` ``. When a\n * `document` is available the candidate is therefore advanced until no element\n * with that id exists, so a generated id never collides with author markup\n * either — the minimal \"id registry\" guarantee these helpers exist to provide.\n *\n * @param prefix - Leading segment of the id (e.g. a controller's identifier).\n * Defaults to `\"stimeo\"`.\n */\nexport function uniqueId(prefix = \"stimeo\"): string {\n let candidate: string;\n do {\n counter += 1;\n candidate = `${prefix}-${counter}`;\n } while (typeof document !== \"undefined\" && document.getElementById(candidate) !== null);\n return candidate;\n}\n\n/**\n * Returns `element`'s existing `id`, or assigns it a freshly generated\n * {@link uniqueId} (with the given `prefix`) and returns that. Idempotent: an\n * element that already has an id keeps it, so consumer-authored ids are never\n * overwritten.\n *\n * @param element - The element to read or stamp an `id` onto.\n * @param prefix - Prefix forwarded to {@link uniqueId} when one must be generated.\n * @returns The element's id (existing or newly assigned).\n */\nexport function ensureId(element: Element, prefix = \"stimeo\"): string {\n if (element.id) return element.id;\n const id = uniqueId(prefix);\n element.id = id;\n return id;\n}\n","/** One temporarily controlled attribute value and the authored value it displaced. */\ninterface AttributeLeaseRecord {\n readonly original: string | null;\n written: string | null;\n}\n\n/**\n * Temporarily controls one attribute across a changing set of elements.\n *\n * The first write remembers the authored value, including the distinction between\n * an absent attribute and an authored empty string. Returning a lease restores that\n * value only while the attribute still matches the controller's last write. If a\n * consumer changed it in the meantime, the consumer owns the new value and teardown\n * leaves it alone.\n *\n * A `null` write deliberately removes the attribute while retaining the lease. This\n * is useful for derived ARIA whose valid absence is itself controller state, such as\n * an unbounded `aria-valuemin` or a blank spinbutton's `aria-valuenow`.\n *\n * The lease has no lifecycle of its own, so it never subscribes to document events.\n * A consumer that must not let a derived value become the authored baseline of a\n * cached page returns its leases from its own `turbo:before-cache` rewind, where the\n * matching `disconnect()` also releases the subscription.\n */\nexport class AttributeLease<T extends Element = Element> {\n readonly #attribute: string;\n readonly #records = new Map<T, AttributeLeaseRecord>();\n\n /** @param attribute - The attribute whose temporary values this lease owns. */\n constructor(attribute: string) {\n this.#attribute = attribute;\n }\n\n /** Writes or removes the leased attribute while preserving its authored value. */\n write(element: T, value: string | null): void {\n const existing = this.#records.get(element);\n if (existing) {\n existing.written = value;\n } else {\n this.#records.set(element, {\n original: element.getAttribute(this.#attribute),\n written: value,\n });\n }\n\n this.#reflect(element, value);\n }\n\n /** Returns one lease without overwriting a value subsequently authored by a consumer. */\n return(element: T): void {\n const record = this.#records.get(element);\n if (!record) return;\n this.#records.delete(element);\n const stillOwned = element.getAttribute(this.#attribute) === record.written;\n if (stillOwned) this.#reflect(element, record.original);\n }\n\n /** Reflects only a real value transition, avoiding self-triggered mutation work. */\n #reflect(element: T, value: string | null): void {\n if (element.getAttribute(this.#attribute) === value) return;\n if (value === null) element.removeAttribute(this.#attribute);\n else element.setAttribute(this.#attribute, value);\n }\n\n /** Returns every outstanding lease using the same ownership check as {@link return}. */\n returnAll(): void {\n for (const element of Array.from(this.#records.keys())) this.return(element);\n }\n}\n","/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way.\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","/**\n * Resolves which of a controller's elements owns a node.\n *\n * A delegated listener hears events from a whole subtree, so the handler's first\n * job is almost always the same question: which item, handle, or control does\n * this `event.target` belong to? The same question comes up for a\n * `MutationRecord.target`, for `document.activeElement`, and for a\n * `relatedTarget` on the way out of a hover region.\n *\n * The answer is `Node.contains()`, which is **inclusive** — an element contains\n * itself — so testing the candidate for identity as well would be redundant.\n *\n * **The guard is what makes this safe to call with a raw event target.**\n * `event.target` is typed `EventTarget | null`, and `contains()` takes a `Node?`:\n * browsers throw `TypeError` for anything else, and `window` — the everyday\n * `EventTarget` that is not a `Node` — is what an event dispatched at it carries.\n * Narrowing here means a caller never has to cast, and the rule cannot drift\n * between the places that ask the question.\n *\n * Scope stays with the caller. These helpers say *which candidate owns the node*,\n * not *whether the node belongs to this controller at all* — a component with\n * nested instances of itself decides that first (by comparing the closest\n * annotated ancestor) and passes the candidates it owns.\n */\n\n/**\n * Index in `candidates` of the first one that is, or contains, `node`.\n *\n * `-1` when none does, when `node` is absent, and when it is an `EventTarget`\n * that is not a `Node`. Candidates are tested in array order, so a nested pair\n * resolves to whichever the caller listed first.\n */\nexport function ownerIndex<T extends Element>(\n candidates: readonly T[],\n node: EventTarget | null | undefined,\n): number {\n if (!(node instanceof Node)) return -1;\n return candidates.findIndex((candidate) => candidate.contains(node));\n}\n\n/**\n * The first candidate that is, or contains, `node`; `null` when none does.\n *\n * A miss indexes the array at `-1`, which reads as `undefined` and lands on the\n * same `null` the absent cases produce.\n */\nexport function ownerOf<T extends Element>(\n candidates: readonly T[],\n node: EventTarget | null | undefined,\n): T | null {\n return candidates[ownerIndex(candidates, node)] ?? null;\n}\n","/**\n * Collapses many Stimulus lifecycle callbacks from one DOM mutation into a\n * single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element and\n * `<name>ValueChanged` once per changed attribute. Replacing a list of N options\n * or morphing several render Values therefore delivers N callbacks — but the\n * useful unit of work is \"reconcile against the resulting declarative input\",\n * once, after the batch has settled. Every controller with reconcilable targets\n * or render Values needs the same shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers initial target and Value callbacks\n * ahead of `connect()`. Reconciling there would compute output against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { announce } from \"../utils/announce\";\nimport { ensureId } from \"../utils/aria_ids\";\nimport { AttributeLease } from \"../utils/attribute_lease\";\nimport { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { ownerIndex } from \"../utils/event_owner\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\n\n/** Stimulus action params understood by {@link FormFieldController.setError}. */\ninterface SetErrorParams {\n /** The error message to display. */\n message?: string;\n}\n\n/** An action event carrying Stimulus `data-*-param` values. */\ntype ActionEvent = Event & { params?: SetErrorParams };\n\n/** Programmatic behavior switches for {@link FormFieldController.setError}. */\nexport interface FormFieldSetErrorOptions {\n /** Override the `focusOnError` Value for this call. */\n focus?: boolean;\n}\n\n/** Detail dispatched with `stimeo--form-field:validate`. */\nexport interface FormFieldValidateDetail {\n /** Whether the explicit validation action left the field valid. */\n valid: boolean;\n /** The first visible error message, or an empty string when valid/unavailable. */\n message: string;\n}\n\n/** Attributes on retained association targets that can change the ARIA graph. */\nconst OBSERVED_ATTRIBUTES = [\"hidden\", \"id\"];\n\n/**\n * Headless, accessible form-field association behavior.\n *\n * Markup contract (identifier: `stimeo--form-field`):\n * <div data-controller=\"stimeo--form-field\">\n * <label for=\"email\">Email</label>\n * <input id=\"email\" type=\"email\" aria-invalid=\"false\"\n * data-stimeo--form-field-target=\"control\" />\n * <p data-stimeo--form-field-target=\"description\">We'll send a confirmation.</p>\n * <p hidden data-stimeo--form-field-target=\"error\"></p>\n * </div>\n *\n * Not an APG widget pattern — this is the wiring substrate behind form controls:\n * it supports **Name, Role, Value** (WCAG 4.1.2) and **error identification**\n * (3.3.1 / 4.1.3) by composing the control's `aria-describedby`, toggling\n * `aria-invalid`, and pointing `aria-errormessage` at the visual error.\n *\n * `validate` dispatches `{ valid: boolean, message: string }`.\n *\n * @remarks\n * Behavior only — it sets semantic attributes and the `hidden` state of the\n * visual error; it never validates input (the consumer / server decides) and\n * never styles. Runtime errors are handed to a separately seated\n * `stimeo--announcer`, because showing a live region together with its first\n * message is not a reliable announcement primitive.\n *\n * Behavior provided:\n * - Assigns ids to description/error targets and composes the control's\n * `aria-describedby` from them in DOM order (preserving the control's authored\n * tokens and removing duplicates).\n * - Reflects server-rendered errors: an error target that is already visible and\n * non-empty at connect puts the field into the invalid state (progressive\n * enhancement).\n * - Reconciles added, removed, replaced, and retained/morphed targets without\n * emitting validation events or announcements.\n * - {@link setError} / {@link clearError} drive the invalid state at runtime and\n * dispatch `stimeo--form-field:validate`.\n */\nexport class FormFieldController extends Controller<HTMLElement> {\n static override targets = [\"control\", \"description\", \"error\"];\n static override values = {\n focusOnError: { type: Boolean, default: false },\n };\n static actions = [\"clearError\", \"setError\"] as const;\n static events = [\"validate\"] as const;\n\n declare readonly controlTarget: HTMLElement;\n declare readonly hasControlTarget: boolean;\n declare readonly descriptionTargets: HTMLElement[];\n declare readonly errorTargets: HTMLElement[];\n declare readonly hasErrorTarget: boolean;\n /** Whether an explicit {@link setError} call moves focus to the current control. */\n declare focusOnErrorValue: boolean;\n\n /** Root attribute (CSS hook) reflecting the invalid state. */\n static readonly #INVALID_ATTR = \"data-stimeo--form-field-invalid\";\n\n /** Collapses one target/morph batch into one silent ARIA reconciliation. */\n readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileDom());\n /** Returns borrowed control ARIA before Turbo snapshots the page. */\n readonly #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());\n readonly #ariaDescribedBy = new AttributeLease<HTMLElement>(\"aria-describedby\");\n readonly #ariaErrorMessage = new AttributeLease<HTMLElement>(\"aria-errormessage\");\n readonly #ariaInvalid = new AttributeLease<HTMLElement>(\"aria-invalid\");\n /** Watches retained target ids, error visibility, and error content. */\n readonly #observer = new MutationObserver((records) => {\n if (records.some((record) => this.#isRelevantMutation(record))) {\n this.#reconcile.schedule();\n }\n });\n\n #activeControl: HTMLElement | null = null;\n #baseDescribedBy: string[] = [];\n #explicitInvalid = false;\n #initialized = false;\n\n /** Wires the initial graph and starts retained-target reconciliation. */\n override connect(): void {\n this.#reconcile.activate();\n this.#ensureAssociationIds();\n this.#beforeCache.activate();\n\n // A restored DOM can contain the invalid hook with no surviving visual\n // message. Treat that one shape as the persisted explicit setError state;\n // visible server errors remain derived and can clear when their DOM clears.\n if (!this.#initialized) {\n this.#explicitInvalid =\n this.element.hasAttribute(FormFieldController.#INVALID_ATTR) &&\n this.#shownErrors().length === 0;\n this.#initialized = true;\n }\n\n this.#reconcileDom();\n this.#observer.observe(this.element, {\n attributes: true,\n attributeFilter: OBSERVED_ATTRIBUTES,\n characterData: true,\n childList: true,\n subtree: true,\n });\n }\n\n /** Releases observers, queued work, and ARIA borrowed on the current control. */\n override disconnect(): void {\n this.#reconcile.cancel();\n this.#observer.disconnect();\n this.#beforeCache.deactivate();\n this.#activeControl && this.#releaseControl(this.#activeControl);\n }\n\n /** Reconciles a control inserted or replaced at runtime. */\n controlTargetConnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Schedules restoration of authored ARIA when a control leaves this field. */\n controlTargetDisconnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Reconciles a description inserted or replaced at runtime. */\n descriptionTargetConnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Removes a departed description from the control's association graph. */\n descriptionTargetDisconnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Reconciles an error region inserted or replaced at runtime. */\n errorTargetConnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Removes a departed error from invalid state and the association graph. */\n errorTargetDisconnected(): void {\n this.#reconcile.schedule();\n }\n\n /**\n * Marks the field invalid and shows the error message. Bound via `data-action`\n * (`#setError`) or callable directly.\n *\n * A non-empty shown message is announced exactly once through the shared\n * assertive announcer. Initial/server reconciliation is deliberately silent.\n *\n * @param arg - Either the message string, or the action event whose\n * `data-stimeo--form-field-message-param` supplies it. When no message is\n * resolvable, any already-populated error targets are simply (re)shown.\n * @param options - Programmatic overrides. Stimulus actions use the Values.\n */\n setError(arg?: string | ActionEvent, options: FormFieldSetErrorOptions = {}): void {\n const message = this.#resolveMessage(arg);\n if (message !== null && this.hasErrorTarget) {\n this.errorTargets[0]?.replaceChildren(document.createTextNode(message));\n }\n for (const error of this.errorTargets) {\n error.hidden = (error.textContent ?? \"\").trim() === \"\";\n }\n\n // setError is an explicit invalid request: it remains invalid even if an\n // error target is absent or replaced before clearError is called.\n this.#explicitInvalid = true;\n this.#reconcileDom();\n const shownMessage = this.#shownMessage();\n const detail: FormFieldValidateDetail = { valid: false, message: shownMessage };\n this.dispatch(\"validate\", { detail });\n announce(shownMessage, { assertive: true });\n\n if (options.focus ?? this.focusOnErrorValue) this.#activeControl?.focus();\n }\n\n /**\n * Clears the error: empties and hides every error target and marks the field\n * valid. Bound via `data-action` (`#clearError`) or callable directly.\n *\n * Clearing is silent: the visual/ARIA state and validation event are sufficient,\n * while success wording remains an explicit consumer announcement.\n */\n clearError(): void {\n for (const error of this.errorTargets) {\n error.replaceChildren();\n error.hidden = true;\n }\n this.#explicitInvalid = false;\n this.#reconcileDom();\n const detail: FormFieldValidateDetail = { valid: true, message: \"\" };\n this.dispatch(\"validate\", { detail });\n }\n\n /**\n * Rebuilds ids and derived ARIA from the settled target graph without reporting\n * a user validation action.\n */\n #reconcileDom(): void {\n this.#ensureAssociationIds();\n this.#adoptCurrentControl();\n\n const shown = this.#shownErrors();\n const invalid = this.#explicitInvalid || shown.length > 0;\n this.element.toggleAttribute(FormFieldController.#INVALID_ATTR, invalid);\n\n const control = this.#activeControl;\n if (!control) return;\n\n this.#ariaInvalid.write(control, invalid ? \"true\" : \"false\");\n\n const primaryErrorId = shown[0]?.id ?? null;\n this.#ariaErrorMessage.write(control, primaryErrorId);\n\n const associationIds = this.#orderedElements([...this.descriptionTargets, ...shown]).map(\n (element) => element.id,\n );\n const describedBy = this.#uniqueTokens([...this.#baseDescribedBy, ...associationIds]);\n this.#ariaDescribedBy.write(control, describedBy.length > 0 ? describedBy.join(\" \") : null);\n }\n\n /** Assigns stable ids before either capture or reflection uses them. */\n #ensureAssociationIds(): void {\n for (const description of this.descriptionTargets) {\n ensureId(description, \"stimeo--form-field-desc\");\n }\n for (const error of this.errorTargets) {\n ensureId(error, \"stimeo--form-field-error\");\n }\n }\n\n /** Switches ARIA ownership when the singular current control target changes. */\n #adoptCurrentControl(): void {\n const next = this.hasControlTarget ? this.controlTarget : null;\n if (next === this.#activeControl) return;\n this.#activeControl && this.#releaseControl(this.#activeControl);\n this.#activeControl = next;\n this.#baseDescribedBy = next ? this.#externalDescribedByTokens(next) : [];\n }\n\n /** Restores one departed control without overwriting later consumer edits. */\n #releaseControl(control: HTMLElement): void {\n this.#ariaDescribedBy.return(control);\n this.#ariaErrorMessage.return(control);\n this.#ariaInvalid.return(control);\n if (control === this.#activeControl) {\n this.#activeControl = null;\n this.#baseDescribedBy = [];\n }\n }\n\n /** Error targets currently visible and non-empty, in document order. */\n #shownErrors(): HTMLElement[] {\n return this.#orderedElements(\n this.errorTargets.filter((error) => !error.hidden && (error.textContent ?? \"\").trim() !== \"\"),\n );\n }\n\n /** Text of the first shown error, for validation detail and announcement. */\n #shownMessage(): string {\n return (this.#shownErrors()[0]?.textContent ?? \"\").trim();\n }\n\n /** Resolves a message from a string argument or an action event's params. */\n #resolveMessage(arg?: string | ActionEvent): string | null {\n if (typeof arg === \"string\") return arg;\n const message = arg?.params?.message;\n return typeof message === \"string\" ? message : null;\n }\n\n /**\n * Tokens authored on a newly adopted control, excluding ids this controller\n * owns through its current description/error targets.\n */\n #externalDescribedByTokens(control: HTMLElement): string[] {\n const owned = new Set([\n ...this.descriptionTargets.map((description) => description.id),\n ...this.errorTargets.map((error) => error.id),\n ]);\n const existing = control.getAttribute(\"aria-describedby\") ?? \"\";\n return this.#uniqueTokens(\n existing.split(/\\s+/).filter((token) => token.length > 0 && !owned.has(token)),\n );\n }\n\n /** Deduplicates ARIA tokens without disturbing their first occurrence. */\n #uniqueTokens(tokens: string[]): string[] {\n const seen = new Set<string>();\n return tokens.filter((token) => {\n if (token.length === 0 || seen.has(token)) return false;\n seen.add(token);\n return true;\n });\n }\n\n /** Returns unique current targets in DOM order before serializing IDREF lists. */\n #orderedElements(elements: HTMLElement[]): HTMLElement[] {\n const candidates = new Set(elements);\n return [this.element, ...this.element.querySelectorAll<HTMLElement>(\"*\")].filter((element) =>\n candidates.has(element),\n );\n }\n\n /** Whether one retained-node mutation can alter this controller's derived state. */\n #isRelevantMutation(record: MutationRecord): boolean {\n const target = record.target;\n if (record.type === \"attributes\") {\n if (record.attributeName === \"hidden\")\n return this.errorTargets.includes(target as HTMLElement);\n return (\n record.attributeName === \"id\" &&\n [...this.descriptionTargets, ...this.errorTargets].includes(target as HTMLElement)\n );\n }\n\n return ownerIndex(this.errorTargets, target) !== -1;\n }\n /** Returns borrowed control ARIA before Turbo snapshots the page. */\n #rewindForCache(): void {\n this.#ariaDescribedBy.returnAll();\n this.#ariaErrorMessage.returnAll();\n this.#ariaInvalid.returnAll();\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/announce.ts","../../src/utils/aria_ids.ts","../../src/utils/attribute_lease.ts","../../src/utils/before_cache_reset.ts","../../src/utils/event_owner.ts","../../src/utils/microtask_coalescer.ts","../../src/controllers/form_field_controller.ts"],"names":[],"mappings":"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","file":"form_field_controller.js","sourcesContent":["/**\n * Sends one message to the page's shared `stimeo--announcer`.\n *\n * A component that has to reach assistive tech does not carry a live region of its\n * own: a region only announces what changes *after* assistive tech already knows\n * about it, which a region that appears (or is un-hidden) with its message cannot\n * satisfy. The one region that can is the announcer sitting in the page from the\n * start, so state changes are handed to it as an event and it does the reading.\n *\n * The event goes to `window` because the announcer is usually a sibling high in the\n * document rather than an ancestor of the component dispatching it.\n *\n * Wording comes from the consumer — the library ships no English strings — so an\n * empty message is silently dropped and nothing is announced.\n *\n * @example\n * ```ts\n * announce(this.announceTextValue, { assertive: false });\n * ```\n */\nexport function announce(message: string, options: { assertive?: boolean } = {}): void {\n const text = message.trim();\n if (text.length === 0) return;\n window.dispatchEvent(\n new CustomEvent(\"stimeo--announcer:announce\", {\n detail: { message: text, assertive: options.assertive === true },\n }),\n );\n}\n\n/**\n * Fills `{name}` placeholders in an announcement template from `values`.\n *\n * The same substitution the value-text templates use, so a consumer writes\n * `\"{percent}% complete\"` in one attribute and gets the same rules everywhere. A\n * placeholder with no matching entry is left as authored rather than blanked, which\n * keeps a typo visible instead of silently swallowing the word.\n */\nexport function fillTemplate(template: string, values: Record<string, string | number>): string {\n return template.replace(/\\{([a-zA-Z][a-zA-Z0-9]*)\\}/g, (match, name: string) => {\n const replacement = values[name];\n return replacement === undefined ? match : String(replacement);\n });\n}\n","/**\n * Minimal id primitives for wiring ARIA relationships.\n *\n * Accessible relationships such as `aria-describedby`, `aria-errormessage`,\n * `aria-labelledby`, and `aria-activedescendant` reference their targets by DOM\n * `id`. When a controller must establish such a relationship but the consumer's\n * markup left the target without an `id`, it needs to mint one that is stable for\n * the page's lifetime and guaranteed not to collide with another generated id.\n *\n * These helpers own *only* id generation/assignment. The linking policy (which\n * attribute, what order, when to add or remove it) stays in each controller —\n * this deliberately avoids a premature generic \"ARIA linker\" abstraction.\n */\n\n/**\n * Monotonic counter backing {@link uniqueId}. Module-scoped so every generated\n * id is unique across all controller instances sharing this module in a document.\n */\nlet counter = 0;\n\n/**\n * Returns a unique, DOM-id-safe string of the form `` `${prefix}-${n}` `` where\n * `n` increases on each call.\n *\n * The counter alone guarantees uniqueness against other *generated* ids, but a\n * consumer-authored element could already own `` `${prefix}-${n}` ``. When a\n * `document` is available the candidate is therefore advanced until no element\n * with that id exists, so a generated id never collides with author markup\n * either — the minimal \"id registry\" guarantee these helpers exist to provide.\n *\n * @param prefix - Leading segment of the id (e.g. a controller's identifier).\n * Defaults to `\"stimeo\"`.\n */\nexport function uniqueId(prefix = \"stimeo\"): string {\n let candidate: string;\n do {\n counter += 1;\n candidate = `${prefix}-${counter}`;\n } while (typeof document !== \"undefined\" && document.getElementById(candidate) !== null);\n return candidate;\n}\n\n/**\n * Returns `element`'s existing `id`, or assigns it a freshly generated\n * {@link uniqueId} (with the given `prefix`) and returns that. Idempotent: an\n * element that already has an id keeps it, so consumer-authored ids are never\n * overwritten.\n *\n * @param element - The element to read or stamp an `id` onto.\n * @param prefix - Prefix forwarded to {@link uniqueId} when one must be generated.\n * @returns The element's id (existing or newly assigned).\n */\nexport function ensureId(element: Element, prefix = \"stimeo\"): string {\n if (element.id) return element.id;\n const id = uniqueId(prefix);\n element.id = id;\n return id;\n}\n","/** One temporarily controlled attribute value and the authored value it displaced. */\ninterface AttributeLeaseRecord {\n readonly original: string | null;\n written: string | null;\n}\n\n/**\n * Temporarily controls one attribute across a changing set of elements.\n *\n * The first write remembers the authored value, including the distinction between\n * an absent attribute and an authored empty string. Returning a lease restores that\n * value only while the attribute still matches the controller's last write. If a\n * consumer changed it in the meantime, the consumer owns the new value and teardown\n * leaves it alone.\n *\n * A `null` write deliberately removes the attribute while retaining the lease. This\n * is useful for derived ARIA whose valid absence is itself controller state, such as\n * an unbounded `aria-valuemin` or a blank spinbutton's `aria-valuenow`.\n *\n * The lease has no lifecycle of its own, so it never subscribes to document events.\n * A consumer that must not let a derived value become the authored baseline of a\n * cached page returns its leases from its own `turbo:before-cache` rewind, where the\n * matching `disconnect()` also releases the subscription.\n */\nexport class AttributeLease<T extends Element = Element> {\n readonly #attribute: string;\n readonly #records = new Map<T, AttributeLeaseRecord>();\n\n /** @param attribute - The attribute whose temporary values this lease owns. */\n constructor(attribute: string) {\n this.#attribute = attribute;\n }\n\n /** Writes or removes the leased attribute while preserving its authored value. */\n write(element: T, value: string | null): void {\n const existing = this.#records.get(element);\n if (existing) {\n existing.written = value;\n } else {\n this.#records.set(element, {\n original: element.getAttribute(this.#attribute),\n written: value,\n });\n }\n\n this.#reflect(element, value);\n }\n\n /** Returns one lease without overwriting a value subsequently authored by a consumer. */\n return(element: T): void {\n const record = this.#records.get(element);\n if (!record) return;\n this.#records.delete(element);\n const stillOwned = element.getAttribute(this.#attribute) === record.written;\n if (stillOwned) this.#reflect(element, record.original);\n }\n\n /** Reflects only a real value transition, avoiding self-triggered mutation work. */\n #reflect(element: T, value: string | null): void {\n if (element.getAttribute(this.#attribute) === value) return;\n if (value === null) element.removeAttribute(this.#attribute);\n else element.setAttribute(this.#attribute, value);\n }\n\n /** Returns every outstanding lease using the same ownership check as {@link return}. */\n returnAll(): void {\n for (const element of Array.from(this.#records.keys())) this.return(element);\n }\n}\n","/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way.\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","/**\n * Resolves which of a controller's elements owns a node.\n *\n * A delegated listener hears events from a whole subtree, so the handler's first\n * job is almost always the same question: which item, handle, or control does\n * this `event.target` belong to? The same question comes up for a\n * `MutationRecord.target`, for `document.activeElement`, and for a\n * `relatedTarget` on the way out of a hover region.\n *\n * The answer is `Node.contains()`, which is **inclusive** — an element contains\n * itself — so testing the candidate for identity as well would be redundant.\n *\n * **The guard is what makes this safe to call with a raw event target.**\n * `event.target` is typed `EventTarget | null`, and `contains()` takes a `Node?`:\n * browsers throw `TypeError` for anything else, and `window` — the everyday\n * `EventTarget` that is not a `Node` — is what an event dispatched at it carries.\n * Narrowing here means a caller never has to cast, and the rule cannot drift\n * between the places that ask the question.\n *\n * Scope stays with the caller. These helpers say *which candidate owns the node*,\n * not *whether the node belongs to this controller at all* — a component with\n * nested instances of itself decides that first (by comparing the closest\n * annotated ancestor) and passes the candidates it owns.\n */\n\n/**\n * Index in `candidates` of the first one that is, or contains, `node`.\n *\n * `-1` when none does, when `node` is absent, and when it is an `EventTarget`\n * that is not a `Node`. Candidates are tested in array order, so a nested pair\n * resolves to whichever the caller listed first.\n */\nexport function ownerIndex<T extends Element>(\n candidates: readonly T[],\n node: EventTarget | null | undefined,\n): number {\n if (!(node instanceof Node)) return -1;\n return candidates.findIndex((candidate) => candidate.contains(node));\n}\n\n/**\n * The first candidate that is, or contains, `node`; `null` when none does.\n *\n * A miss indexes the array at `-1`, which reads as `undefined` and lands on the\n * same `null` the absent cases produce.\n */\nexport function ownerOf<T extends Element>(\n candidates: readonly T[],\n node: EventTarget | null | undefined,\n): T | null {\n return candidates[ownerIndex(candidates, node)] ?? null;\n}\n","/**\n * Collapses many Stimulus lifecycle callbacks from one DOM mutation into a\n * single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element and\n * `<name>ValueChanged` once per changed attribute. Replacing a list of N options\n * or morphing several render Values therefore delivers N callbacks — but the\n * useful unit of work is \"reconcile against the resulting declarative input\",\n * once, after the batch has settled. Every controller with reconcilable targets\n * or render Values needs the same shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers initial target and Value callbacks\n * ahead of `connect()`. Reconciling there would compute output against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { announce } from \"../utils/announce\";\nimport { ensureId } from \"../utils/aria_ids\";\nimport { AttributeLease } from \"../utils/attribute_lease\";\nimport { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { ownerIndex } from \"../utils/event_owner\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\n\n/** Stimulus action params understood by {@link FormFieldController.setError}. */\ninterface SetErrorParams {\n /** The error message to display. */\n message?: string;\n}\n\n/** An action event carrying Stimulus `data-*-param` values. */\ntype ActionEvent = Event & { params?: SetErrorParams };\n\n/** Programmatic behavior switches for {@link FormFieldController.setError}. */\nexport interface FormFieldSetErrorOptions {\n /** Override the `focusOnError` Value for this call. */\n focus?: boolean;\n}\n\n/** Detail dispatched with `stimeo--form-field:validate`. */\nexport interface FormFieldValidateDetail {\n /** Whether the explicit validation action left the field valid. */\n valid: boolean;\n /** The first visible error message, or an empty string when valid/unavailable. */\n message: string;\n}\n\n/** Attributes on retained association targets that can change the ARIA graph. */\nconst OBSERVED_ATTRIBUTES = [\"hidden\", \"id\"];\n\n/**\n * Headless, accessible form-field association behavior.\n *\n * Markup contract (identifier: `stimeo--form-field`):\n * <div data-controller=\"stimeo--form-field\">\n * <label for=\"email\">Email</label>\n * <input id=\"email\" type=\"email\" aria-invalid=\"false\"\n * data-stimeo--form-field-target=\"control\" />\n * <p data-stimeo--form-field-target=\"description\">We'll send a confirmation.</p>\n * <p hidden data-stimeo--form-field-target=\"error\"></p>\n * </div>\n *\n * Not an APG widget pattern — this is the wiring substrate behind form controls:\n * it supports **Name, Role, Value** (WCAG 4.1.2) and **error identification**\n * (3.3.1 / 4.1.3) by composing the control's `aria-describedby`, toggling\n * `aria-invalid`, and pointing `aria-errormessage` at the visual error.\n *\n * `validate` dispatches `{ valid: boolean, message: string }`.\n *\n * @remarks\n * Behavior only — it sets semantic attributes and the `hidden` state of the\n * visual error; it never validates input (the consumer / server decides) and\n * never styles. Runtime errors are handed to a separately seated\n * `stimeo--announcer`, because showing a live region together with its first\n * message is not a reliable announcement primitive.\n *\n * Behavior provided:\n * - Assigns ids to description/error targets and composes the control's\n * `aria-describedby` from them in DOM order (preserving the control's authored\n * tokens and removing duplicates).\n * - Reflects server-rendered errors: an error target that is already visible and\n * non-empty at connect puts the field into the invalid state (progressive\n * enhancement).\n * - Reconciles added, removed, replaced, and retained/morphed targets without\n * emitting validation events or announcements.\n * - {@link setError} / {@link clearError} drive the invalid state at runtime and\n * dispatch `stimeo--form-field:validate`.\n */\nexport class FormFieldController extends Controller<HTMLElement> {\n static override targets = [\"control\", \"description\", \"error\"];\n static override values = {\n focusOnError: { type: Boolean, default: false },\n };\n static actions = [\"clearError\", \"setError\"] as const;\n static events = [\"validate\"] as const;\n\n declare readonly controlTarget: HTMLElement;\n declare readonly hasControlTarget: boolean;\n declare readonly descriptionTargets: HTMLElement[];\n declare readonly errorTargets: HTMLElement[];\n declare readonly hasErrorTarget: boolean;\n /** Whether an explicit {@link setError} call moves focus to the current control. */\n declare focusOnErrorValue: boolean;\n\n /** Root attribute (CSS hook) reflecting the invalid state, in this controller's namespace. */\n get #invalidAttribute(): string {\n return `data-${this.identifier}-invalid`;\n }\n\n /** Collapses one target/morph batch into one silent ARIA reconciliation. */\n readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileDom());\n /** Returns borrowed control ARIA before Turbo snapshots the page. */\n readonly #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());\n readonly #ariaDescribedBy = new AttributeLease<HTMLElement>(\"aria-describedby\");\n readonly #ariaErrorMessage = new AttributeLease<HTMLElement>(\"aria-errormessage\");\n readonly #ariaInvalid = new AttributeLease<HTMLElement>(\"aria-invalid\");\n /** Watches retained target ids, error visibility, and error content. */\n readonly #observer = new MutationObserver((records) => {\n if (records.some((record) => this.#isRelevantMutation(record))) {\n this.#reconcile.schedule();\n }\n });\n\n #activeControl: HTMLElement | null = null;\n #baseDescribedBy: string[] = [];\n #explicitInvalid = false;\n #initialized = false;\n\n /** Wires the initial graph and starts retained-target reconciliation. */\n override connect(): void {\n this.#reconcile.activate();\n this.#ensureAssociationIds();\n this.#beforeCache.activate();\n\n // A restored DOM can contain the invalid hook with no surviving visual\n // message. Treat that one shape as the persisted explicit setError state;\n // visible server errors remain derived and can clear when their DOM clears.\n if (!this.#initialized) {\n this.#explicitInvalid =\n this.element.hasAttribute(this.#invalidAttribute) && this.#shownErrors().length === 0;\n this.#initialized = true;\n }\n\n this.#reconcileDom();\n this.#observer.observe(this.element, {\n attributes: true,\n attributeFilter: OBSERVED_ATTRIBUTES,\n characterData: true,\n childList: true,\n subtree: true,\n });\n }\n\n /** Releases observers, queued work, and ARIA borrowed on the current control. */\n override disconnect(): void {\n this.#reconcile.cancel();\n this.#observer.disconnect();\n this.#beforeCache.deactivate();\n this.#activeControl && this.#releaseControl(this.#activeControl);\n }\n\n /** Reconciles a control inserted or replaced at runtime. */\n controlTargetConnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Schedules restoration of authored ARIA when a control leaves this field. */\n controlTargetDisconnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Reconciles a description inserted or replaced at runtime. */\n descriptionTargetConnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Removes a departed description from the control's association graph. */\n descriptionTargetDisconnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Reconciles an error region inserted or replaced at runtime. */\n errorTargetConnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Removes a departed error from invalid state and the association graph. */\n errorTargetDisconnected(): void {\n this.#reconcile.schedule();\n }\n\n /**\n * Marks the field invalid and shows the error message. Bound via `data-action`\n * (`#setError`) or callable directly.\n *\n * A non-empty shown message is announced exactly once through the shared\n * assertive announcer. Initial/server reconciliation is deliberately silent.\n *\n * @param arg - Either the message string, or the action event whose\n * `data-stimeo--form-field-message-param` supplies it. When no message is\n * resolvable, any already-populated error targets are simply (re)shown.\n * @param options - Programmatic overrides. Stimulus actions use the Values.\n */\n setError(arg?: string | ActionEvent, options: FormFieldSetErrorOptions = {}): void {\n const message = this.#resolveMessage(arg);\n if (message !== null && this.hasErrorTarget) {\n this.errorTargets[0]?.replaceChildren(document.createTextNode(message));\n }\n for (const error of this.errorTargets) {\n error.hidden = (error.textContent ?? \"\").trim() === \"\";\n }\n\n // setError is an explicit invalid request: it remains invalid even if an\n // error target is absent or replaced before clearError is called.\n this.#explicitInvalid = true;\n this.#reconcileDom();\n const shownMessage = this.#shownMessage();\n const detail: FormFieldValidateDetail = { valid: false, message: shownMessage };\n this.dispatch(\"validate\", { detail });\n announce(shownMessage, { assertive: true });\n\n if (options.focus ?? this.focusOnErrorValue) this.#activeControl?.focus();\n }\n\n /**\n * Clears the error: empties and hides every error target and marks the field\n * valid. Bound via `data-action` (`#clearError`) or callable directly.\n *\n * Clearing is silent: the visual/ARIA state and validation event are sufficient,\n * while success wording remains an explicit consumer announcement.\n */\n clearError(): void {\n for (const error of this.errorTargets) {\n error.replaceChildren();\n error.hidden = true;\n }\n this.#explicitInvalid = false;\n this.#reconcileDom();\n const detail: FormFieldValidateDetail = { valid: true, message: \"\" };\n this.dispatch(\"validate\", { detail });\n }\n\n /**\n * Rebuilds ids and derived ARIA from the settled target graph without reporting\n * a user validation action.\n */\n #reconcileDom(): void {\n this.#ensureAssociationIds();\n this.#adoptCurrentControl();\n\n const shown = this.#shownErrors();\n const invalid = this.#explicitInvalid || shown.length > 0;\n this.element.toggleAttribute(this.#invalidAttribute, invalid);\n\n const control = this.#activeControl;\n if (!control) return;\n\n this.#ariaInvalid.write(control, invalid ? \"true\" : \"false\");\n\n const primaryErrorId = shown[0]?.id ?? null;\n this.#ariaErrorMessage.write(control, primaryErrorId);\n\n const associationIds = this.#orderedElements([...this.descriptionTargets, ...shown]).map(\n (element) => element.id,\n );\n const describedBy = this.#uniqueTokens([...this.#baseDescribedBy, ...associationIds]);\n this.#ariaDescribedBy.write(control, describedBy.length > 0 ? describedBy.join(\" \") : null);\n }\n\n /** Assigns stable ids before either capture or reflection uses them. */\n #ensureAssociationIds(): void {\n for (const description of this.descriptionTargets) {\n ensureId(description, \"stimeo--form-field-desc\");\n }\n for (const error of this.errorTargets) {\n ensureId(error, \"stimeo--form-field-error\");\n }\n }\n\n /** Switches ARIA ownership when the singular current control target changes. */\n #adoptCurrentControl(): void {\n const next = this.hasControlTarget ? this.controlTarget : null;\n if (next === this.#activeControl) return;\n this.#activeControl && this.#releaseControl(this.#activeControl);\n this.#activeControl = next;\n this.#baseDescribedBy = next ? this.#externalDescribedByTokens(next) : [];\n }\n\n /** Restores one departed control without overwriting later consumer edits. */\n #releaseControl(control: HTMLElement): void {\n this.#ariaDescribedBy.return(control);\n this.#ariaErrorMessage.return(control);\n this.#ariaInvalid.return(control);\n if (control === this.#activeControl) {\n this.#activeControl = null;\n this.#baseDescribedBy = [];\n }\n }\n\n /** Error targets currently visible and non-empty, in document order. */\n #shownErrors(): HTMLElement[] {\n return this.#orderedElements(\n this.errorTargets.filter((error) => !error.hidden && (error.textContent ?? \"\").trim() !== \"\"),\n );\n }\n\n /** Text of the first shown error, for validation detail and announcement. */\n #shownMessage(): string {\n return (this.#shownErrors()[0]?.textContent ?? \"\").trim();\n }\n\n /** Resolves a message from a string argument or an action event's params. */\n #resolveMessage(arg?: string | ActionEvent): string | null {\n if (typeof arg === \"string\") return arg;\n const message = arg?.params?.message;\n return typeof message === \"string\" ? message : null;\n }\n\n /**\n * Tokens authored on a newly adopted control, excluding ids this controller\n * owns through its current description/error targets.\n */\n #externalDescribedByTokens(control: HTMLElement): string[] {\n const owned = new Set([\n ...this.descriptionTargets.map((description) => description.id),\n ...this.errorTargets.map((error) => error.id),\n ]);\n const existing = control.getAttribute(\"aria-describedby\") ?? \"\";\n return this.#uniqueTokens(\n existing.split(/\\s+/).filter((token) => token.length > 0 && !owned.has(token)),\n );\n }\n\n /** Deduplicates ARIA tokens without disturbing their first occurrence. */\n #uniqueTokens(tokens: string[]): string[] {\n const seen = new Set<string>();\n return tokens.filter((token) => {\n if (token.length === 0 || seen.has(token)) return false;\n seen.add(token);\n return true;\n });\n }\n\n /** Returns unique current targets in DOM order before serializing IDREF lists. */\n #orderedElements(elements: HTMLElement[]): HTMLElement[] {\n const candidates = new Set(elements);\n return [this.element, ...this.element.querySelectorAll<HTMLElement>(\"*\")].filter((element) =>\n candidates.has(element),\n );\n }\n\n /** Whether one retained-node mutation can alter this controller's derived state. */\n #isRelevantMutation(record: MutationRecord): boolean {\n const target = record.target;\n if (record.type === \"attributes\") {\n if (record.attributeName === \"hidden\")\n return this.errorTargets.includes(target as HTMLElement);\n return (\n record.attributeName === \"id\" &&\n [...this.descriptionTargets, ...this.errorTargets].includes(target as HTMLElement)\n );\n }\n\n return ownerIndex(this.errorTargets, target) !== -1;\n }\n /** Returns borrowed control ARIA before Turbo snapshots the page. */\n #rewindForCache(): void {\n this.#ariaDescribedBy.returnAll();\n this.#ariaErrorMessage.returnAll();\n this.#ariaInvalid.returnAll();\n }\n}\n"]}
|
|
@@ -96,13 +96,19 @@ var CONSTRAINT_MESSAGE_KEYS = [
|
|
|
96
96
|
["stepMismatch", "step-mismatch"],
|
|
97
97
|
["badInput", "bad-input"]
|
|
98
98
|
];
|
|
99
|
-
var
|
|
100
|
-
var
|
|
101
|
-
var DISALLOW_ATTR = "data-stimeo--form-validation-disallow";
|
|
102
|
-
var DISALLOW_WHITESPACE_MESSAGE = `${MESSAGE_ATTR_PREFIX}whitespace`;
|
|
99
|
+
var MESSAGE_PART = "message";
|
|
100
|
+
var DISALLOW_PART = "disallow";
|
|
103
101
|
var DISALLOW_WHITESPACE_DEFAULT = "Please enter a value that is not only whitespace.";
|
|
104
102
|
var FORM_FIELD_INVALID_ATTR = "data-stimeo--form-field-invalid";
|
|
105
103
|
var FormValidationController = class _FormValidationController extends Controller {
|
|
104
|
+
/** The generic message override, in the namespace this controller is registered under. */
|
|
105
|
+
get #messageAttribute() {
|
|
106
|
+
return `data-${this.identifier}-${MESSAGE_PART}`;
|
|
107
|
+
}
|
|
108
|
+
/** Marks the `novalidate` this controller applied, so an authored one is left alone. */
|
|
109
|
+
get #novalidateMarker() {
|
|
110
|
+
return `data-${this.identifier}-${_FormValidationController.#NOVALIDATE_PART}`;
|
|
111
|
+
}
|
|
106
112
|
static outlets = ["stimeo--form-field"];
|
|
107
113
|
static values = {
|
|
108
114
|
validateOnBlur: { type: Boolean, default: true },
|
|
@@ -112,8 +118,8 @@ var FormValidationController = class _FormValidationController extends Controlle
|
|
|
112
118
|
};
|
|
113
119
|
static actions = ["validate"];
|
|
114
120
|
static events = ["valid", "invalid"];
|
|
115
|
-
/**
|
|
116
|
-
static #
|
|
121
|
+
/** Suffix of the marker recording that we added `novalidate`, so we only remove our own. */
|
|
122
|
+
static #NOVALIDATE_PART = "novalidate";
|
|
117
123
|
/** Object-backed groups already interacted with — the input revalidation gate. */
|
|
118
124
|
#touchedGroups = /* @__PURE__ */ new WeakSet();
|
|
119
125
|
/** String-backed fallback radio groups already interacted with. */
|
|
@@ -169,7 +175,7 @@ var FormValidationController = class _FormValidationController extends Controlle
|
|
|
169
175
|
/** Suppresses native bubbles and binds the submit / blur / input listeners. */
|
|
170
176
|
connect() {
|
|
171
177
|
if (setDefaultAttribute(this.element, "novalidate", "")) {
|
|
172
|
-
this.element.setAttribute(
|
|
178
|
+
this.element.setAttribute(this.#novalidateMarker, "");
|
|
173
179
|
}
|
|
174
180
|
this.#listenerDocument = this.element.ownerDocument;
|
|
175
181
|
this.#listenerDocument.addEventListener("submit", this.#onSubmit, true);
|
|
@@ -189,9 +195,9 @@ var FormValidationController = class _FormValidationController extends Controlle
|
|
|
189
195
|
}
|
|
190
196
|
this.#ownedCustomErrors.clear();
|
|
191
197
|
this.#touchedRadioGroups.clear();
|
|
192
|
-
if (this.element.hasAttribute(
|
|
198
|
+
if (this.element.hasAttribute(this.#novalidateMarker)) {
|
|
193
199
|
this.element.removeAttribute("novalidate");
|
|
194
|
-
this.element.removeAttribute(
|
|
200
|
+
this.element.removeAttribute(this.#novalidateMarker);
|
|
195
201
|
}
|
|
196
202
|
}
|
|
197
203
|
/**
|
|
@@ -283,10 +289,10 @@ var FormValidationController = class _FormValidationController extends Controlle
|
|
|
283
289
|
if (control.validity.customError) return control.validationMessage;
|
|
284
290
|
for (const [flag, key] of CONSTRAINT_MESSAGE_KEYS) {
|
|
285
291
|
if (control.validity[flag]) {
|
|
286
|
-
return this.#authoredMessage(control, `${
|
|
292
|
+
return this.#authoredMessage(control, `${this.#messageAttribute}-${key}`) ?? this.#authoredMessage(control, this.#messageAttribute) ?? control.validationMessage;
|
|
287
293
|
}
|
|
288
294
|
}
|
|
289
|
-
return control.validationMessage || this.#authoredMessage(control,
|
|
295
|
+
return control.validationMessage || this.#authoredMessage(control, this.#messageAttribute) || "";
|
|
290
296
|
}
|
|
291
297
|
/**
|
|
292
298
|
* Applies (or clears) a declarative custom constraint via `setCustomValidity`,
|
|
@@ -302,14 +308,14 @@ var FormValidationController = class _FormValidationController extends Controlle
|
|
|
302
308
|
#syncCustomValidity(control) {
|
|
303
309
|
const recorded = this.#ownedCustomErrors.get(control);
|
|
304
310
|
const ownsCurrent = recorded !== void 0 && control.validationMessage === recorded;
|
|
305
|
-
const violates = control.getAttribute(
|
|
311
|
+
const violates = control.getAttribute(`data-${this.identifier}-${DISALLOW_PART}`) === "whitespace" && control.value.length > 0 && control.value.trim() === "";
|
|
306
312
|
if (!violates) {
|
|
307
313
|
if (ownsCurrent) control.setCustomValidity("");
|
|
308
314
|
this.#ownedCustomErrors.delete(control);
|
|
309
315
|
return;
|
|
310
316
|
}
|
|
311
317
|
if (control.validity.customError && !ownsCurrent) return;
|
|
312
|
-
const message = this.#authoredMessage(control,
|
|
318
|
+
const message = this.#authoredMessage(control, `${this.#messageAttribute}-whitespace`) ?? this.#authoredMessage(control, this.#messageAttribute) ?? DISALLOW_WHITESPACE_DEFAULT;
|
|
313
319
|
if (!ownsCurrent || recorded !== message) control.setCustomValidity(message);
|
|
314
320
|
this.#ownedCustomErrors.set(control, message);
|
|
315
321
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/default_attribute.ts","../../src/utils/focus_candidate.ts","../../src/controllers/form_validation_controller.ts"],"names":[],"mappings":";;;;;AAYO,SAAS,mBAAA,CAAoB,OAAA,EAAkB,IAAA,EAAc,KAAA,EAAwB;AAC1F,EAAA,IAAI,OAAA,CAAQ,YAAA,CAAa,IAAI,CAAA,EAAG,OAAO,KAAA;AACvC,EAAA,OAAA,CAAQ,YAAA,CAAa,MAAM,KAAK,CAAA;AAChC,EAAA,OAAO,IAAA;AACT;;;ACLO,SAAS,yBAAyB,OAAA,EAA+B;AACtE,EAAA,IAAI,QAAA,GAA2B,OAAA,CAAQ,OAAA,CAAQ,oBAAoB,CAAA;AACnE,EAAA,OAAO,QAAA,EAAU;AACf,IAAA,MAAM,MAAA,GAAS,KAAA,CAAM,IAAA,CAAK,QAAA,CAAS,QAAQ,CAAA,CAAE,IAAA,CAAK,CAAC,KAAA,KAAU,KAAA,CAAM,OAAA,KAAY,QAAQ,CAAA;AACvF,IAAA,IAAI,CAAC,MAAA,EAAQ,QAAA,CAAS,OAAO,GAAG,OAAO,IAAA;AACvC,IAAA,QAAA,GAAW,QAAA,CAAS,aAAA,EAAe,OAAA,CAAQ,oBAAoB,CAAA,IAAK,IAAA;AAAA,EACtE;AACA,EAAA,OAAO,KAAA;AACT;AAgDO,SAAS,aAAa,OAAA,EAA+B;AAC1D,EAAA,IAAI,OAAA,CAAQ,OAAA,CAAQ,mBAAmB,CAAA,EAAG,OAAO,KAAA;AACjD,EAAA,IAAI,OAAA,YAAmB,gBAAA,IAAoB,OAAA,CAAQ,IAAA,KAAS,UAAU,OAAO,KAAA;AAC7E,EAAA,IAAI,EAAE,UAAA,IAAc,OAAA,CAAA,EAAU,OAAO,IAAA;AACrC,EAAA,IAAK,OAAA,CAAgD,UAAU,OAAO,KAAA;AACtE,EAAA,OAAO,CAAC,yBAAyB,OAAO,CAAA;AAC1C;AAGO,IAAM,2BAAA,GAA8B;AAAA,EACzC,SAAA;AAAA,EACA,YAAA;AAAA,EACA,QAAA;AAAA,EACA,OAAA;AAAA,EACA,QAAA;AAAA,EACA,UAAA;AAAA,EACA,SAAA;AAAA,EACA,QAAA;AAAA,EACA,iBAAA;AAAA,EACA,iBAAA;AAAA,EACA,YAAA;AAAA,EACA;AACF,CAAA,CAAE,KAAK,GAAG,CAAA;AAQH,SAAS,mBAAmB,OAAA,EAA+B;AAChE,EAAA,MAAM,QAAS,OAAA,CAA8C,eAAA;AAC7D,EAAA,OAAO,OAAO,KAAA,KAAU,UAAA,GAAa,KAAA,CAAM,IAAA,CAAK,SAAS,EAAE,kBAAA,EAAoB,IAAA,EAAM,CAAA,GAAI,IAAA;AAC3F;AAGA,SAAS,iBAAiB,OAAA,EAAqC;AAC7D,EAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,YAAA,CAAa,UAAU,CAAA;AAC7C,EAAA,IAAI,KAAA,KAAU,QAAQ,CAAC,YAAA,CAAa,KAAK,KAAA,CAAM,IAAA,EAAM,CAAA,EAAG,OAAO,IAAA;AAC/D,EAAA,OAAO,OAAO,KAAK,CAAA;AACrB;AAGA,SAAS,iBAAiB,OAAA,EAA+B;AACvD,EAAA,IAAI,OAAA,YAAmB,iBAAA,IAAqB,OAAA,YAAmB,eAAA,EAAiB;AAC9E,IAAA,OAAO,OAAA,CAAQ,aAAa,MAAM,CAAA;AAAA,EACpC;AACA,EAAA,IACE,OAAA,YAAmB,iBAAA,IACnB,OAAA,YAAmB,iBAAA,IACnB,mBAAmB,mBAAA,EACnB;AACA,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,IAAI,OAAA,YAAmB,gBAAA,EAAkB,OAAO,OAAA,CAAQ,IAAA,KAAS,QAAA;AACjE,EAAA,IAAI,OAAA,YAAmB,mBAAmB,OAAO,IAAA;AACjD,EAAA,IAAI,OAAA,CAAQ,OAAA,KAAY,OAAA,IAAW,OAAA,CAAQ,YAAY,OAAA,EAAS;AAC9D,IAAA,OAAO,OAAA,CAAQ,aAAa,UAAU,CAAA;AAAA,EACxC;AACA,EAAA,IAAI,OAAA,YAAmB,WAAA,IAAe,OAAA,CAAQ,OAAA,KAAY,SAAA,EAAW;AACnE,IAAA,MAAM,UAAU,OAAA,CAAQ,aAAA;AACxB,IAAA,OACE,OAAA,YAAmB,kBAAA,IACnB,KAAA,CAAM,IAAA,CAAK,OAAA,CAAQ,QAAQ,CAAA,CAAE,IAAA,CAAK,CAAC,KAAA,KAAU,KAAA,CAAM,OAAA,KAAY,SAAS,CAAA,KAAM,OAAA;AAAA,EAElF;AACA,EAAA,OAAO,KAAA;AACT;AAGA,SAAS,mBAAmB,OAAA,EAA+B;AACzD,EAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,YAAA,CAAa,iBAAiB,GAAG,WAAA,EAAY;AACnE,EAAA,OAAO,KAAA,KAAU,EAAA,IAAM,KAAA,KAAU,MAAA,IAAU,KAAA,KAAU,gBAAA;AACvD;AAUO,SAAS,UAAU,OAAA,EAA+B;AACvD,EAAA,IAAI,CAAC,aAAa,OAAO,CAAA,IAAK,CAAC,kBAAA,CAAmB,OAAO,GAAG,OAAO,KAAA;AAEnE,EAAA,MAAM,QAAA,GAAW,iBAAiB,OAAO,CAAA;AACzC,EAAA,IAAI,QAAA,KAAa,IAAA,EAAM,OAAO,QAAA,IAAY,CAAA;AAC1C,EAAA,OAAO,gBAAA,CAAiB,OAAO,CAAA,IAAK,kBAAA,CAAmB,OAAO,CAAA;AAChE;AAUO,SAAS,aAAa,IAAA,EAAsC;AACjE,EAAA,KAAA,MAAW,SAAA,IAAa,IAAA,CAAK,gBAAA,CAA8B,2BAA2B,CAAA,EAAG;AACvF,IAAA,IAAI,SAAA,CAAU,SAAS,CAAA,EAAG,OAAO,SAAA;AAAA,EACnC;AACA,EAAA,OAAO,IAAA;AACT;;;AC9JA,IAAM,uBAAA,GAAiF;AAAA,EACrF,CAAC,gBAAgB,eAAe,CAAA;AAAA,EAChC,CAAC,gBAAgB,eAAe,CAAA;AAAA,EAChC,CAAC,mBAAmB,kBAAkB,CAAA;AAAA,EACtC,CAAC,YAAY,WAAW,CAAA;AAAA,EACxB,CAAC,WAAW,UAAU,CAAA;AAAA,EACtB,CAAC,kBAAkB,iBAAiB,CAAA;AAAA,EACpC,CAAC,iBAAiB,gBAAgB,CAAA;AAAA,EAClC,CAAC,gBAAgB,eAAe,CAAA;AAAA,EAChC,CAAC,YAAY,WAAW;AAC1B,CAAA;AAGA,IAAM,mBAAA,GAAsB,uCAAA;AAE5B,IAAM,oBAAA,GAAuB,sCAAA;AAE7B,IAAM,aAAA,GAAgB,uCAAA;AAEtB,IAAM,2BAAA,GAA8B,GAAG,mBAAmB,CAAA,UAAA,CAAA;AAE1D,IAAM,2BAAA,GAA8B,mDAAA;AAGpC,IAAM,uBAAA,GAA0B,iCAAA;AAqGzB,IAAM,wBAAA,GAAN,MAAM,yBAAA,SAAiC,UAAA,CAA4B;AAAA,EACxE,OAAgB,OAAA,GAAU,CAAC,oBAAoB,CAAA;AAAA,EAC/C,OAAgB,MAAA,GAAS;AAAA,IACvB,cAAA,EAAgB,EAAE,IAAA,EAAM,OAAA,EAAS,SAAS,IAAA,EAAK;AAAA,IAC/C,gBAAA,EAAkB,EAAE,IAAA,EAAM,OAAA,EAAS,SAAS,IAAA,EAAK;AAAA,IACjD,iBAAA,EAAmB,EAAE,IAAA,EAAM,OAAA,EAAS,SAAS,IAAA,EAAK;AAAA,IAClD,YAAA,EAAc,EAAE,IAAA,EAAM,OAAA,EAAS,SAAS,IAAA;AAAK,GAC/C;AAAA,EACA,OAAO,OAAA,GAAU,CAAC,UAAU,CAAA;AAAA,EAC5B,OAAO,MAAA,GAAS,CAAC,OAAA,EAAS,SAAS,CAAA;AAAA;AAAA,EAWnC,OAAgB,kBAAA,GAAqB,yCAAA;AAAA;AAAA,EAG5B,cAAA,uBAAqB,OAAA,EAAkD;AAAA;AAAA,EAGvE,mBAAA,uBAA0B,GAAA,EAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQtC,kBAAA,uBAAyB,GAAA,EAAgC;AAAA;AAAA,EAGzD,eAAA,uBAAsB,OAAA,EAAqC;AAAA;AAAA,EAGpE,iBAAA,GAAqC,IAAA;AAAA,EAE5B,SAAA,GAAY,CAAC,KAAA,KAA6B;AACjD,IAAA,IAAI,KAAA,CAAM,MAAA,KAAW,IAAA,CAAK,OAAA,EAAS;AACnC,IAAA,MAAM,OAAA,GAAU,KAAK,YAAA,EAAa;AAClC,IAAA,IAAI,OAAA,CAAQ,WAAW,CAAA,EAAG;AACxB,MAAA,IAAA,CAAK,SAAS,OAAA,EAAS,EAAE,MAAA,EAAQ,IAAI,CAAA;AACrC,MAAA;AAAA,IACF;AAIA,IAAA,KAAA,CAAM,cAAA,EAAe;AACrB,IAAA,KAAA,CAAM,wBAAA,EAAyB;AAC/B,IAAA,MAAM,KAAA,GAAQ,QAAQ,CAAC,CAAA;AACvB,IAAA,IAAI,KAAK,iBAAA,IAAqB,KAAA,OAAY,eAAA,CAAgB,KAAK,GAAG,KAAA,EAAM;AACxE,IAAA,IAAA,CAAK,SAAS,SAAA,EAAW,EAAE,QAAQ,EAAE,OAAA,IAAW,CAAA;AAAA,EAClD,CAAA;AAAA,EAES,WAAA,GAAc,CAAC,KAAA,KAA4B;AAClD,IAAA,IAAI,CAAC,KAAK,mBAAA,EAAqB;AAC/B,IAAA,MAAM,QAAA,GAAW,KAAK,SAAA,EAAU;AAChC,IAAA,MAAM,KAAA,GAAQ,SAAS,cAAA,CAAe,GAAA,CAAI,KAAK,YAAA,CAAa,KAAA,CAAM,MAAM,CAAC,CAAA;AACzE,IAAA,IAAI,CAAC,KAAA,EAAO;AAGZ,IAAA,MAAM,UAAU,KAAA,CAAM,aAAA;AACtB,IAAA,IAAI,KAAA,CAAM,SAAS,OAAA,YAAmB,IAAA,IAAQ,MAAM,KAAA,CAAM,OAAA,CAAQ,QAAA,CAAS,OAAO,CAAA,EAAG;AACrF,IAAA,IAAI,QAAA,CAAS,cAAA,CAAe,GAAA,CAAI,IAAA,CAAK,YAAA,CAAa,OAAO,CAAC,CAAA,EAAG,GAAA,KAAQ,KAAA,CAAM,GAAA,EAAK;AAChF,IAAA,IAAA,CAAK,YAAA,CAAa,MAAM,GAAG,CAAA;AAC3B,IAAA,IAAA,CAAK,YAAY,KAAK,CAAA;AAAA,EACxB,CAAA;AAAA,EAES,QAAA,GAAW,CAAC,KAAA,KAAuB;AAC1C,IAAA,IAAI,CAAC,KAAK,sBAAA,EAAwB;AAClC,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,SAAA,EAAU,CAAE,cAAA,CAAe,IAAI,IAAA,CAAK,YAAA,CAAa,KAAA,CAAM,MAAM,CAAC,CAAA;AAGjF,IAAA,IAAI,CAAC,KAAA,IAAS,CAAC,KAAK,UAAA,CAAW,KAAA,CAAM,GAAG,CAAA,EAAG;AAC3C,IAAA,IAAA,CAAK,YAAY,KAAK,CAAA;AAAA,EACxB,CAAA;AAAA,EAES,SAAA,GAAY,CAAC,KAAA,KAAuB;AAC3C,IAAA,IAAI,CAAC,KAAK,qBAAA,EAAuB;AACjC,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,SAAA,EAAU,CAAE,cAAA,CAAe,IAAI,IAAA,CAAK,YAAA,CAAa,KAAA,CAAM,MAAM,CAAC,CAAA;AACjF,IAAA,IAAI,CAAC,KAAA,EAAO;AAGZ,IAAA,IAAA,CAAK,YAAA,CAAa,MAAM,GAAG,CAAA;AAC3B,IAAA,IAAA,CAAK,YAAY,KAAK,CAAA;AAAA,EACxB,CAAA;AAAA;AAAA,EAGS,OAAA,GAAgB;AACvB,IAAA,IAAI,mBAAA,CAAoB,IAAA,CAAK,OAAA,EAAS,YAAA,EAAc,EAAE,CAAA,EAAG;AACvD,MAAA,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,yBAAA,CAAyB,kBAAA,EAAoB,EAAE,CAAA;AAAA,IAC3E;AAIA,IAAA,IAAA,CAAK,iBAAA,GAAoB,KAAK,OAAA,CAAQ,aAAA;AACtC,IAAA,IAAA,CAAK,iBAAA,CAAkB,gBAAA,CAAiB,QAAA,EAAU,IAAA,CAAK,WAAW,IAAI,CAAA;AACtE,IAAA,IAAA,CAAK,iBAAA,CAAkB,gBAAA,CAAiB,UAAA,EAAY,IAAA,CAAK,WAAW,CAAA;AACpE,IAAA,IAAA,CAAK,iBAAA,CAAkB,gBAAA,CAAiB,OAAA,EAAS,IAAA,CAAK,QAAQ,CAAA;AAC9D,IAAA,IAAA,CAAK,iBAAA,CAAkB,gBAAA,CAAiB,QAAA,EAAU,IAAA,CAAK,SAAS,CAAA;AAAA,EAClE;AAAA;AAAA,EAGS,UAAA,GAAmB;AAC1B,IAAA,IAAA,CAAK,iBAAA,EAAmB,mBAAA,CAAoB,QAAA,EAAU,IAAA,CAAK,WAAW,IAAI,CAAA;AAC1E,IAAA,IAAA,CAAK,iBAAA,EAAmB,mBAAA,CAAoB,UAAA,EAAY,IAAA,CAAK,WAAW,CAAA;AACxE,IAAA,IAAA,CAAK,iBAAA,EAAmB,mBAAA,CAAoB,OAAA,EAAS,IAAA,CAAK,QAAQ,CAAA;AAClE,IAAA,IAAA,CAAK,iBAAA,EAAmB,mBAAA,CAAoB,QAAA,EAAU,IAAA,CAAK,SAAS,CAAA;AACpE,IAAA,IAAA,CAAK,iBAAA,GAAoB,IAAA;AACzB,IAAA,KAAA,MAAW,CAAC,OAAA,EAAS,OAAO,CAAA,IAAK,KAAK,kBAAA,EAAoB;AACxD,MAAA,IAAI,OAAA,CAAQ,iBAAA,KAAsB,OAAA,EAAS,OAAA,CAAQ,kBAAkB,EAAE,CAAA;AAAA,IACzE;AACA,IAAA,IAAA,CAAK,mBAAmB,KAAA,EAAM;AAC9B,IAAA,IAAA,CAAK,oBAAoB,KAAA,EAAM;AAC/B,IAAA,IAAI,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,yBAAA,CAAyB,kBAAkB,CAAA,EAAG;AAC1E,MAAA,IAAA,CAAK,OAAA,CAAQ,gBAAgB,YAAY,CAAA;AACzC,MAAA,IAAA,CAAK,OAAA,CAAQ,eAAA,CAAgB,yBAAA,CAAyB,kBAAkB,CAAA;AAAA,IAC1E;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,QAAA,GAAoB;AAClB,IAAA,OAAO,IAAA,CAAK,YAAA,EAAa,CAAE,MAAA,KAAW,CAAA;AAAA,EACxC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,YAAA,GAAqC;AACnC,IAAA,MAAM,UAAgC,EAAC;AACvC,IAAA,KAAA,MAAW,SAAS,IAAA,CAAK,SAAA,EAAU,CAAE,MAAA,CAAO,QAAO,EAAG;AACpD,MAAA,IAAA,CAAK,YAAA,CAAa,MAAM,GAAG,CAAA;AAC3B,MAAA,MAAM,YAAA,GAAe,IAAA,CAAK,WAAA,CAAY,KAAK,CAAA;AAC3C,MAAA,IAAI,YAAA,EAAc,OAAA,CAAQ,IAAA,CAAK,YAAY,CAAA;AAAA,IAC7C;AACA,IAAA,OAAO,OAAA;AAAA,EACT;AAAA;AAAA,EAGA,SAAA,GAAgC;AAC9B,IAAA,MAAM,MAAA,GAAS,KAAK,aAAA,EAAc;AAClC,IAAA,MAAM,MAAA,uBAAa,GAAA,EAA+B;AAClD,IAAA,MAAM,cAAA,uBAAqB,GAAA,EAA2C;AAEtE,IAAA,KAAA,MAAW,OAAA,IAAW,KAAK,SAAA,EAAW;AACpC,MAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,SAAA,CAAU,OAAA,EAAS,MAAM,CAAA;AAC5C,MAAA,MAAM,GAAA,GAAM,IAAA,CAAK,OAAA,CAAQ,OAAA,EAAS,KAAK,CAAA;AACvC,MAAA,IAAI,KAAA,GAAQ,MAAA,CAAO,GAAA,CAAI,GAAG,CAAA;AAC1B,MAAA,IAAI,CAAC,KAAA,EAAO;AACV,QAAA,KAAA,GAAQ,EAAE,GAAA,EAAK,KAAA,EAAO,QAAA,EAAU,EAAC,EAAE;AACnC,QAAA,MAAA,CAAO,GAAA,CAAI,KAAK,KAAK,CAAA;AAAA,MACvB;AACA,MAAA,KAAA,CAAM,QAAA,CAAS,KAAK,OAAO,CAAA;AAC3B,MAAA,cAAA,CAAe,GAAA,CAAI,SAAS,KAAK,CAAA;AAAA,IACnC;AAEA,IAAA,OAAO,EAAE,QAAQ,cAAA,EAAe;AAAA,EAClC;AAAA;AAAA,EAGA,aAAa,GAAA,EAA0B;AACrC,IAAA,IAAI,OAAO,QAAQ,QAAA,EAAU;AAC3B,MAAA,IAAA,CAAK,mBAAA,CAAoB,IAAI,GAAG,CAAA;AAAA,IAClC,CAAA,MAAO;AACL,MAAA,IAAA,CAAK,cAAA,CAAe,IAAI,GAAG,CAAA;AAAA,IAC7B;AAAA,EACF;AAAA,EAEA,WAAW,GAAA,EAA6B;AACtC,IAAA,OAAO,OAAO,GAAA,KAAQ,QAAA,GAClB,IAAA,CAAK,mBAAA,CAAoB,GAAA,CAAI,GAAG,CAAA,GAChC,IAAA,CAAK,cAAA,CAAe,GAAA,CAAI,GAAG,CAAA;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,YAAY,KAAA,EAA8C;AAGxD,IAAA,KAAA,MAAW,OAAA,IAAW,KAAA,CAAM,QAAA,EAAU,IAAA,CAAK,oBAAoB,OAAO,CAAA;AACtE,IAAA,MAAM,YAAA,GAAe,KAAA,CAAM,QAAA,CAAS,IAAA,CAAK,CAAC,YAAY,CAAC,OAAA,CAAQ,aAAA,EAAe,CAAA,IAAK,IAAA;AACnF,IAAA,IAAI,MAAM,KAAA,EAAO;AACf,MAAA,IAAI,YAAA,EAAc;AAChB,QAAA,MAAM,OAAA,GAAU,IAAA,CAAK,WAAA,CAAY,YAAY,CAAA;AAC7C,QAAA,MAAM,eAAA,GACJ,KAAA,CAAM,KAAA,CAAM,OAAA,CAAQ,YAAA,CAAa,uBAAuB,CAAA,IACxD,IAAA,CAAK,eAAA,CAAgB,GAAA,CAAI,KAAA,CAAM,KAAK,CAAA,KAAM,OAAA;AAC5C,QAAA,IAAI,CAAC,iBAAiB,KAAA,CAAM,KAAA,CAAM,SAAS,OAAA,EAAS,EAAE,KAAA,EAAO,KAAA,EAAO,CAAA;AACpE,QAAA,IAAA,CAAK,eAAA,CAAgB,GAAA,CAAI,KAAA,CAAM,KAAA,EAAO,OAAO,CAAA;AAAA,MAC/C,CAAA,MAAA,IACE,KAAA,CAAM,KAAA,CAAM,OAAA,CAAQ,YAAA,CAAa,uBAAuB,CAAA,IACxD,IAAA,CAAK,eAAA,CAAgB,GAAA,CAAI,KAAA,CAAM,KAAK,CAAA,EACpC;AACA,QAAA,KAAA,CAAM,MAAM,UAAA,EAAW;AACvB,QAAA,IAAA,CAAK,eAAA,CAAgB,MAAA,CAAO,KAAA,CAAM,KAAK,CAAA;AAAA,MACzC;AAAA,IACF;AACA,IAAA,OAAO,YAAA;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,YAAY,OAAA,EAAqC;AAG/C,IAAA,IAAI,OAAA,CAAQ,QAAA,CAAS,WAAA,EAAa,OAAO,OAAA,CAAQ,iBAAA;AACjD,IAAA,KAAA,MAAW,CAAC,IAAA,EAAM,GAAG,CAAA,IAAK,uBAAA,EAAyB;AACjD,MAAA,IAAI,OAAA,CAAQ,QAAA,CAAS,IAAI,CAAA,EAAG;AAC1B,QAAA,OACE,IAAA,CAAK,gBAAA,CAAiB,OAAA,EAAS,CAAA,EAAG,mBAAmB,CAAA,EAAG,GAAG,CAAA,CAAE,CAAA,IAC7D,IAAA,CAAK,gBAAA,CAAiB,OAAA,EAAS,oBAAoB,KACnD,OAAA,CAAQ,iBAAA;AAAA,MAEZ;AAAA,IACF;AACA,IAAA,OAAO,QAAQ,iBAAA,IAAqB,IAAA,CAAK,gBAAA,CAAiB,OAAA,EAAS,oBAAoB,CAAA,IAAK,EAAA;AAAA,EAC9F;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,oBAAoB,OAAA,EAAmC;AACrD,IAAA,MAAM,QAAA,GAAW,IAAA,CAAK,kBAAA,CAAmB,GAAA,CAAI,OAAO,CAAA;AACpD,IAAA,MAAM,WAAA,GAAc,QAAA,KAAa,MAAA,IAAa,OAAA,CAAQ,iBAAA,KAAsB,QAAA;AAE5E,IAAA,MAAM,QAAA,GACJ,OAAA,CAAQ,YAAA,CAAa,aAAa,CAAA,KAAM,YAAA,IACxC,OAAA,CAAQ,KAAA,CAAM,MAAA,GAAS,CAAA,IACvB,OAAA,CAAQ,KAAA,CAAM,MAAK,KAAM,EAAA;AAC3B,IAAA,IAAI,CAAC,QAAA,EAAU;AACb,MAAA,IAAI,WAAA,EAAa,OAAA,CAAQ,iBAAA,CAAkB,EAAE,CAAA;AAC7C,MAAA,IAAA,CAAK,kBAAA,CAAmB,OAAO,OAAO,CAAA;AACtC,MAAA;AAAA,IACF;AAIA,IAAA,IAAI,OAAA,CAAQ,QAAA,CAAS,WAAA,IAAe,CAAC,WAAA,EAAa;AAElD,IAAA,MAAM,OAAA,GACJ,IAAA,CAAK,gBAAA,CAAiB,OAAA,EAAS,2BAA2B,KAC1D,IAAA,CAAK,gBAAA,CAAiB,OAAA,EAAS,oBAAoB,CAAA,IACnD,2BAAA;AACF,IAAA,IAAI,CAAC,WAAA,IAAe,QAAA,KAAa,OAAA,EAAS,OAAA,CAAQ,kBAAkB,OAAO,CAAA;AAC3E,IAAA,IAAA,CAAK,kBAAA,CAAmB,GAAA,CAAI,OAAA,EAAS,OAAO,CAAA;AAAA,EAC9C;AAAA;AAAA,EAGA,gBAAA,CAAiB,SAA6B,SAAA,EAAkC;AAC9E,IAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,YAAA,CAAa,SAAS,CAAA;AAC9C,IAAA,OAAO,WAAW,OAAA,CAAQ,IAAA,EAAK,CAAE,MAAA,GAAS,IAAI,OAAA,GAAU,IAAA;AAAA,EAC1D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,OAAA,CAAQ,SAA6B,KAAA,EAAuD;AAC1F,IAAA,IAAI,OAAO,OAAO,KAAA;AAClB,IAAA,IAAI,mBAAmB,gBAAA,IAAoB,OAAA,CAAQ,IAAA,KAAS,OAAA,IAAW,QAAQ,IAAA,EAAM;AACnF,MAAA,OAAO,CAAA,MAAA,EAAS,QAAQ,IAAI,CAAA,CAAA;AAAA,IAC9B;AACA,IAAA,OAAO,OAAA;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,gBAAgB,OAAA,EAAiD;AAC/D,IAAA,IAAI,YAAA,CAAa,OAAO,CAAA,EAAG,OAAO,OAAA;AAClC,IAAA,MAAM,QAAQ,IAAA,CAAK,SAAA,CAAU,OAAA,EAAS,IAAA,CAAK,eAAe,CAAA;AAC1D,IAAA,IAAI,CAAC,KAAA,EAAO,gBAAA,EAAkB,OAAO,IAAA;AACrC,IAAA,MAAM,OAAO,KAAA,CAAM,aAAA;AACnB,IAAA,IAAI,SAAA,CAAU,IAAI,CAAA,EAAG,OAAO,IAAA;AAC5B,IAAA,OAAO,aAAa,IAAI,CAAA;AAAA,EAC1B;AAAA;AAAA,EAGA,aAAA,GAAuD;AACrD,IAAA,MAAM,MAAA,uBAAa,GAAA,EAAsC;AACzD,IAAA,MAAM,WAAW,IAAA,CAAK,6BAAA;AACtB,IAAA,MAAM,UAAU,IAAA,CAAK,sBAAA;AACrB,IAAA,KAAA,IAAS,KAAA,GAAQ,CAAA,EAAG,KAAA,GAAQ,QAAA,CAAS,QAAQ,KAAA,EAAA,EAAS;AACpD,MAAA,MAAM,OAAA,GAAU,SAAS,KAAK,CAAA;AAC9B,MAAA,MAAM,MAAA,GAAS,QAAQ,KAAK,CAAA;AAC5B,MAAA,IAAI,OAAA,IAAW,MAAA,EAAQ,MAAA,CAAO,GAAA,CAAI,SAAS,MAAM,CAAA;AAAA,IACnD;AACA,IAAA,OAAO,MAAA;AAAA,EACT;AAAA;AAAA,EAGA,SAAA,CACE,SACA,MAAA,EACiC;AACjC,IAAA,IAAI,QAAA,GAA+B,OAAA;AACnC,IAAA,OAAO,QAAA,EAAU;AACf,MAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,GAAA,CAAI,QAAQ,CAAA;AACjC,MAAA,IAAI,OAAO,OAAO,KAAA;AAClB,MAAA,QAAA,GAAW,QAAA,CAAS,aAAA;AAAA,IACtB;AACA,IAAA,OAAO,MAAA;AAAA,EACT;AAAA;AAAA,EAGA,IAAI,SAAA,GAAkC;AACpC,IAAA,MAAM,WAAiC,EAAC;AACxC,IAAA,KAAA,MAAW,WAAW,KAAA,CAAM,IAAA,CAAK,IAAA,CAAK,OAAA,CAAQ,QAAQ,CAAA,EAAG;AACvD,MAAA,IAAI,KAAK,cAAA,CAAe,OAAO,CAAA,EAAG,QAAA,CAAS,KAAK,OAAO,CAAA;AAAA,IACzD;AACA,IAAA,OAAO,QAAA;AAAA,EACT;AAAA;AAAA,EAGA,aAAa,MAAA,EAAuD;AAClE,IAAA,OAAO,MAAA,YAAkB,OAAA,IAAW,IAAA,CAAK,cAAA,CAAe,MAAM,KAAK,MAAA,CAAO,IAAA,KAAS,IAAA,CAAK,OAAA,GACpF,MAAA,GACA,IAAA;AAAA,EACN;AAAA,EAEA,eAAe,OAAA,EAAiD;AAC9D,IAAA,OAAA,CACG,OAAA,YAAmB,gBAAA,IAClB,OAAA,YAAmB,iBAAA,IACnB,OAAA,YAAmB,mBAAA;AAAA;AAAA;AAAA,IAIrB,OAAA,CAAQ,YAAA;AAAA,EAEZ;AACF","file":"form_validation_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 on its own for callers that need the disabled-inheritance rule without\n * the rest of {@link canTakeFocus} — a control's availability check, or a `hidden`\n * walk bounded at a widget root, which is a different rule to compose with.\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 four conditions that make the platform refuse\n * focus are checked: `hidden` / `inert` (an ancestor's counts),\n * `input[type=\"hidden\"]`, the native `disabled` property, and `disabled`\n * inherited from an ancestor `fieldset`. CSS-only invisibility is handled by\n * {@link isRenderedForFocus} when a consumer needs sequential-focus semantics.\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], [inert]\")) return false;\n if (element instanceof HTMLInputElement && element.type === \"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/** Elements whose semantics or authored attributes can place them in sequential focus order. */\nexport const TAB_STOP_CANDIDATE_SELECTOR = [\n \"a[href]\",\n \"area[href]\",\n \"button\",\n \"input\",\n \"select\",\n \"textarea\",\n \"summary\",\n \"iframe\",\n \"audio[controls]\",\n \"video[controls]\",\n \"[tabindex]\",\n \"[contenteditable]\",\n].join(\",\");\n\n/** Optional browser visibility API used to exclude CSS-hidden candidates. */\ninterface VisibilityCheckable {\n checkVisibility?: (options?: { visibilityProperty?: boolean }) => boolean;\n}\n\n/** Whether CSS visibility allows an otherwise eligible element to participate in focus order. */\nexport function isRenderedForFocus(element: HTMLElement): boolean {\n const check = (element as HTMLElement & VisibilityCheckable).checkVisibility;\n return typeof check === \"function\" ? check.call(element, { visibilityProperty: true }) : true;\n}\n\n/** Parses an authored `tabindex`; invalid syntax has no explicit focus-order meaning. */\nfunction authoredTabindex(element: HTMLElement): number | null {\n const value = element.getAttribute(\"tabindex\");\n if (value === null || !/^[+-]?\\d+$/.test(value.trim())) return null;\n return Number(value);\n}\n\n/** Whether the element's native semantics place it in sequential focus order. */\nfunction hasNativeTabStop(element: HTMLElement): boolean {\n if (element instanceof HTMLAnchorElement || element instanceof HTMLAreaElement) {\n return element.hasAttribute(\"href\");\n }\n if (\n element instanceof HTMLButtonElement ||\n element instanceof HTMLSelectElement ||\n element instanceof HTMLTextAreaElement\n ) {\n return true;\n }\n if (element instanceof HTMLInputElement) return element.type !== \"hidden\";\n if (element instanceof HTMLIFrameElement) return true;\n if (element.tagName === \"AUDIO\" || element.tagName === \"VIDEO\") {\n return element.hasAttribute(\"controls\");\n }\n if (element instanceof HTMLElement && element.tagName === \"SUMMARY\") {\n const details = element.parentElement;\n return (\n details instanceof HTMLDetailsElement &&\n Array.from(details.children).find((child) => child.tagName === \"SUMMARY\") === element\n );\n }\n return false;\n}\n\n/** Whether an explicit `contenteditable` value creates an editable tab stop. */\nfunction hasEditableTabStop(element: HTMLElement): boolean {\n const value = element.getAttribute(\"contenteditable\")?.toLowerCase();\n return value === \"\" || value === \"true\" || value === \"plaintext-only\";\n}\n\n/**\n * Whether an element is a usable sequential Tab stop right now.\n *\n * Native semantics, authored `tabindex`, editable hosts, inherited disabled state,\n * HTML `hidden`/`inert`, and CSS visibility are evaluated together. `aria-disabled`\n * remains focusable because it communicates unavailability without removing the\n * control from discovery order.\n */\nexport function isTabStop(element: HTMLElement): boolean {\n if (!canTakeFocus(element) || !isRenderedForFocus(element)) return false;\n\n const tabindex = authoredTabindex(element);\n if (tabindex !== null) return tabindex >= 0;\n return hasNativeTabStop(element) || hasEditableTabStop(element);\n}\n\n/** Returns every usable sequential Tab stop below `root` in document order. */\nexport function tabStopsWithin(root: ParentNode): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(TAB_STOP_CANDIDATE_SELECTOR)).filter(\n isTabStop,\n );\n}\n\n/** Returns the first usable sequential Tab stop below `root`, if one exists. */\nexport function firstTabStop(root: ParentNode): HTMLElement | null {\n for (const candidate of root.querySelectorAll<HTMLElement>(TAB_STOP_CANDIDATE_SELECTOR)) {\n if (isTabStop(candidate)) return candidate;\n }\n return null;\n}\n\n/** Whether `root` contains at least one usable sequential Tab stop. */\nexport function hasTabStop(root: ParentNode): boolean {\n return firstTabStop(root) !== null;\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { setDefaultAttribute } from \"../utils/default_attribute\";\nimport { canTakeFocus, firstTabStop, isTabStop } from \"../utils/focus_candidate\";\nimport type { FormFieldController } from \"./form_field_controller\";\n\n/** Native form controls that participate in constraint validation. */\ntype ValidatableControl = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;\n\n/**\n * Maps each `ValidityState` flag (in priority order) to the kebab-case suffix of\n * its per-constraint message-override attribute. The first flag that is `true`\n * wins, so e.g. `valueMissing` is reported before a stale `patternMismatch`.\n */\nconst CONSTRAINT_MESSAGE_KEYS: ReadonlyArray<readonly [keyof ValidityState, string]> = [\n [\"valueMissing\", \"value-missing\"],\n [\"typeMismatch\", \"type-mismatch\"],\n [\"patternMismatch\", \"pattern-mismatch\"],\n [\"tooShort\", \"too-short\"],\n [\"tooLong\", \"too-long\"],\n [\"rangeUnderflow\", \"range-underflow\"],\n [\"rangeOverflow\", \"range-overflow\"],\n [\"stepMismatch\", \"step-mismatch\"],\n [\"badInput\", \"bad-input\"],\n];\n\n/** Attribute prefix for a per-constraint message override, authored on the control. */\nconst MESSAGE_ATTR_PREFIX = \"data-stimeo--form-validation-message-\";\n/** Attribute for a generic message override applied to any failing constraint. */\nconst MESSAGE_ATTR_GENERIC = \"data-stimeo--form-validation-message\";\n/** Attribute opting a control into a declarative custom rule (`\"whitespace\"`). */\nconst DISALLOW_ATTR = \"data-stimeo--form-validation-disallow\";\n/** Message override dedicated to the declarative whitespace rule. */\nconst DISALLOW_WHITESPACE_MESSAGE = `${MESSAGE_ATTR_PREFIX}whitespace`;\n/** Default message when `disallow=\"whitespace\"` fails and no override is given. */\nconst DISALLOW_WHITESPACE_DEFAULT = \"Please enter a value that is not only whitespace.\";\n\n/** Public invalid-state hook owned by `stimeo--form-field`. */\nconst FORM_FIELD_INVALID_ATTR = \"data-stimeo--form-field-invalid\";\n\n/** Identity used to collect controls into one validation field. */\ntype FieldGroupKey = FormFieldController | ValidatableControl | string;\n\n/** A field's validatable controls, grouped so the whole field validates together. */\ninterface FieldGroup {\n readonly key: FieldGroupKey;\n readonly field: FormFieldController | undefined;\n readonly controls: ValidatableControl[];\n}\n\n/** Live form grouping resolved once for a validation operation. */\ninterface ValidationSnapshot {\n readonly groups: Map<FieldGroupKey, FieldGroup>;\n /** `null` is a deliberate miss key so delegated non-control events need one guard. */\n readonly groupByControl: Map<ValidatableControl | null, FieldGroup>;\n}\n\n/**\n * Headless, accessible **form-validation orchestration**.\n *\n * Markup contract (identifier: `stimeo--form-validation`):\n * <form data-controller=\"stimeo--form-validation\"\n * data-stimeo--form-validation-stimeo--form-field-outlet=\"[data-controller~='stimeo--form-field']\">\n * <div data-controller=\"stimeo--form-field\">\n * <label for=\"email\">Email</label>\n * <input id=\"email\" type=\"email\" required\n * data-stimeo--form-field-target=\"control\" />\n * <p hidden data-stimeo--form-field-target=\"error\"></p>\n * </div>\n * <button type=\"submit\">Save</button>\n * </form>\n *\n * Not an APG widget pattern — this is the *timing* layer for **error\n * identification** (WCAG 3.3.1) and **error suggestion** (3.3.3): it decides\n * *when* each control is checked and routes the browser's native\n * `validationMessage` into the field's {@link FormFieldController} error region.\n * The per-field ARIA wiring (`aria-invalid` / `aria-errormessage` /\n * `aria-describedby`) therefore lives in exactly one place — `stimeo--form-field`,\n * reached through a Stimulus **outlet** — and is never re-implemented here.\n *\n * `valid` dispatches `{}`; `invalid` dispatches `{ invalid: HTMLElement[] }`.\n *\n * @remarks\n * Behavior only — validation **rules** stay in the markup (native HTML\n * constraints: `required`, `type`, `pattern`, `min`/`max`, …) or in the consumer's\n * own `setCustomValidity()` calls, which `checkValidity()` surfaces transparently.\n * It sets the form's `novalidate` so it can replace the browser's default error\n * bubbles with accessible in-page visual errors plus the shared Announcer, and\n * restores the attribute on disconnect.\n *\n * Two declarative escape hatches let a field **exceed** native validation with no\n * consumer JS (author them on the control):\n * - **Per-constraint messages** — `data-stimeo--form-validation-message-<constraint>`\n * (`value-missing`, `too-short`, `too-long`, `pattern-mismatch`, `type-mismatch`,\n * `range-overflow`, `range-underflow`, `step-mismatch`, `bad-input`), or a generic\n * `data-stimeo--form-validation-message` fallback, override the shown text per failing\n * `ValidityState` flag — controlled, localizable wording that also fixes headless\n * browsers returning an empty native `validationMessage`. Falls back to native.\n * - **`data-stimeo--form-validation-disallow=\"whitespace\"`** — a built-in custom rule\n * rejecting a value that is blank after trimming (which slips past `required` /\n * `minlength`), wired through `setCustomValidity` so it blocks submit like any\n * native constraint.\n *\n * Behavior provided:\n * - On connect, suppresses native bubbles (`novalidate`, restored on disconnect)\n * and intercepts the form's `submit` in the **capture phase** so an invalid form\n * is cancelled before any other submit handler (e.g. `stimeo--submit-once`)\n * reacts to a submission that will never happen.\n * - On submit, validates every control; if any is invalid it blocks submission,\n * moves focus to the first invalid control (unless `focusInvalid` is `false`),\n * and dispatches `stimeo--form-validation:invalid`. An all-valid form dispatches\n * `:valid` and submits normally.\n * - Validates a field on blur once it has been interacted with (`validateOnBlur`),\n * and re-validates it on input while it is already touched\n * (`revalidateOnInput`) so a shown message clears the moment the value becomes\n * valid — but a pristine field is never eagerly flagged mid-typing.\n *\n * A control with no owning `stimeo--form-field` outlet is still validated (it can\n * block submit and receive focus) but renders no message.\n *\n * Radio groups work unchanged: point the field's `control` target at the\n * `role=\"radiogroup\"` container so the invalid state lands on the group, and the\n * group is reported as a single invalid entry (not one per radio).\n *\n * Rich widgets (listbox, time-picker, …) that keep their committed value in a\n * hidden holder participate by making that holder a **validatable mirror**:\n * `<input type=\"text\" hidden required>` — the `hidden` *attribute*, not\n * `type=\"hidden\"`, which is barred from constraint validation. Native\n * constraints then govern the widget's value with no extra JavaScript. The\n * widget dispatches a bubbling `change` on the mirror when a value is committed\n * (a completed interaction, so it validates immediately), and focus for an\n * invalid mirror is delegated to the field's visible `control` target — the\n * target itself when focusable, else its first focusable descendant.\n *\n * No-JS caveat: a `required` mirror also gates the browser's own pre-Stimulus\n * validation, which cannot surface UI on an invisible control. When the no-JS\n * fallback matters, author `novalidate` on the form (this controller preserves\n * an author-set attribute) so the submission reaches the server's validation.\n */\nexport class FormValidationController extends Controller<HTMLFormElement> {\n static override outlets = [\"stimeo--form-field\"];\n static override values = {\n validateOnBlur: { type: Boolean, default: true },\n validateOnChange: { type: Boolean, default: true },\n revalidateOnInput: { type: Boolean, default: true },\n focusInvalid: { type: Boolean, default: true },\n };\n static actions = [\"validate\"] as const;\n static events = [\"valid\", \"invalid\"] as const;\n\n declare readonly stimeoFormFieldOutlets: FormFieldController[];\n declare readonly stimeoFormFieldOutletElements: HTMLElement[];\n\n declare validateOnBlurValue: boolean;\n declare validateOnChangeValue: boolean;\n declare revalidateOnInputValue: boolean;\n declare focusInvalidValue: boolean;\n\n /** Marker recording that we added `novalidate`, so we only remove our own. */\n static readonly #NOVALIDATE_MARKER = \"data-stimeo--form-validation-novalidate\";\n\n /** Object-backed groups already interacted with — the input revalidation gate. */\n readonly #touchedGroups = new WeakSet<FormFieldController | ValidatableControl>();\n\n /** String-backed fallback radio groups already interacted with. */\n readonly #touchedRadioGroups = new Set<string>();\n\n /**\n * Last message this controller wrote through `setCustomValidity`, kept as a\n * two-layer ownership ledger: a value is cleared only while the live message\n * still equals the recorded write. Iterable so disconnect can release every\n * surviving loan without touching a consumer's later custom error.\n */\n readonly #ownedCustomErrors = new Map<ValidatableControl, string>();\n\n /** Last invalid message routed to each field, suppressing duplicate reports. */\n readonly #reportedErrors = new WeakMap<FormFieldController, string>();\n\n /** Exact document that owns the delegated listeners for this connection. */\n #listenerDocument: Document | null = null;\n\n readonly #onSubmit = (event: SubmitEvent): void => {\n if (event.target !== this.element) return;\n const invalid = this.#validateAll();\n if (invalid.length === 0) {\n this.dispatch(\"valid\", { detail: {} });\n return;\n }\n // Cancel the whole submit: preventDefault stops native/Turbo navigation;\n // stopImmediatePropagation keeps later submit handlers (e.g. submit-once's\n // busy state) from acting on a submission that will never happen.\n event.preventDefault();\n event.stopImmediatePropagation();\n const first = invalid[0];\n if (this.focusInvalidValue && first) this.#focusTargetFor(first)?.focus();\n this.dispatch(\"invalid\", { detail: { invalid } });\n };\n\n readonly #onFocusOut = (event: FocusEvent): void => {\n if (!this.validateOnBlurValue) return;\n const snapshot = this.#snapshot();\n const group = snapshot.groupByControl.get(this.#controlFrom(event.target));\n if (!group) return;\n // Focus moving *within* the same field (e.g. between members of a radio\n // group) is not leaving it — defer validation until focus actually exits.\n const related = event.relatedTarget;\n if (group.field && related instanceof Node && group.field.element.contains(related)) return;\n if (snapshot.groupByControl.get(this.#controlFrom(related))?.key === group.key) return;\n this.#markTouched(group.key);\n this.#applyGroup(group);\n };\n\n readonly #onInput = (event: Event): void => {\n if (!this.revalidateOnInputValue) return;\n const group = this.#snapshot().groupByControl.get(this.#controlFrom(event.target));\n // Only re-validate a field the user has already left once, so the first\n // keystroke never eagerly flags a control they are still filling in.\n if (!group || !this.#isTouched(group.key)) return;\n this.#applyGroup(group);\n };\n\n readonly #onChange = (event: Event): void => {\n if (!this.validateOnChangeValue) return;\n const group = this.#snapshot().groupByControl.get(this.#controlFrom(event.target));\n if (!group) return;\n // change marks a *committed* interaction (a picked option, a toggled box, a\n // widget writing its mirror), so unlike input it both touches and validates.\n this.#markTouched(group.key);\n this.#applyGroup(group);\n };\n\n /** Suppresses native bubbles and binds the submit / blur / input listeners. */\n override connect(): void {\n if (setDefaultAttribute(this.element, \"novalidate\", \"\")) {\n this.element.setAttribute(FormValidationController.#NOVALIDATE_MARKER, \"\");\n }\n // Capture submit before form-bound actions. The other bubbling events live on\n // the same document so controls associated through `form=\"id\"` participate too;\n // #controlFrom rejects every control owned by a different form.\n this.#listenerDocument = this.element.ownerDocument;\n this.#listenerDocument.addEventListener(\"submit\", this.#onSubmit, true);\n this.#listenerDocument.addEventListener(\"focusout\", this.#onFocusOut);\n this.#listenerDocument.addEventListener(\"input\", this.#onInput);\n this.#listenerDocument.addEventListener(\"change\", this.#onChange);\n }\n\n /** Tears down listeners and restores `novalidate` if we added it. */\n override disconnect(): void {\n this.#listenerDocument?.removeEventListener(\"submit\", this.#onSubmit, true);\n this.#listenerDocument?.removeEventListener(\"focusout\", this.#onFocusOut);\n this.#listenerDocument?.removeEventListener(\"input\", this.#onInput);\n this.#listenerDocument?.removeEventListener(\"change\", this.#onChange);\n this.#listenerDocument = null;\n for (const [control, message] of this.#ownedCustomErrors) {\n if (control.validationMessage === message) control.setCustomValidity(\"\");\n }\n this.#ownedCustomErrors.clear();\n this.#touchedRadioGroups.clear();\n if (this.element.hasAttribute(FormValidationController.#NOVALIDATE_MARKER)) {\n this.element.removeAttribute(\"novalidate\");\n this.element.removeAttribute(FormValidationController.#NOVALIDATE_MARKER);\n }\n }\n\n /**\n * Validates every control now, rendering or clearing each field's message, and\n * returns whether the whole form is valid. Marks every field/group touched so\n * a later input from any sibling re-validates it. Bound via `data-action`\n * (`#validate`) or callable directly (e.g. before a programmatic submit).\n */\n validate(): boolean {\n return this.#validateAll().length === 0;\n }\n\n /**\n * Validates every control and returns one invalid control per field. Controls\n * are grouped by field first (see {@link #keyFor}) so a field with several\n * controls — a radio group, or a mirror plus its visible control — reflects\n * *all* of them: a valid sibling must never clear an invalid one's message.\n * Each group's first invalid control supplies the message and the focus target.\n */\n #validateAll(): ValidatableControl[] {\n const invalid: ValidatableControl[] = [];\n for (const group of this.#snapshot().groups.values()) {\n this.#markTouched(group.key);\n const firstInvalid = this.#applyGroup(group);\n if (firstInvalid) invalid.push(firstInvalid);\n }\n return invalid;\n }\n\n /** Builds the current field table in one pass over form controls and DOM depth. */\n #snapshot(): ValidationSnapshot {\n const fields = this.#fieldOutlets();\n const groups = new Map<FieldGroupKey, FieldGroup>();\n const groupByControl = new Map<ValidatableControl | null, FieldGroup>();\n\n for (const control of this.#controls) {\n const field = this.#fieldFor(control, fields);\n const key = this.#keyFor(control, field);\n let group = groups.get(key);\n if (!group) {\n group = { key, field, controls: [] };\n groups.set(key, group);\n }\n group.controls.push(control);\n groupByControl.set(control, group);\n }\n\n return { groups, groupByControl };\n }\n\n /** Records that a whole field/group, rather than one sibling control, was visited. */\n #markTouched(key: FieldGroupKey): void {\n if (typeof key === \"string\") {\n this.#touchedRadioGroups.add(key);\n } else {\n this.#touchedGroups.add(key);\n }\n }\n\n #isTouched(key: FieldGroupKey): boolean {\n return typeof key === \"string\"\n ? this.#touchedRadioGroups.has(key)\n : this.#touchedGroups.has(key);\n }\n\n /**\n * Runs native constraint validation across a field's controls and routes the\n * result to its `stimeo--form-field` outlet: the first invalid control's\n * `validationMessage` is shown, an all-valid field is cleared. Returns the\n * first invalid control (for the invalid list / focus), or `null` when valid.\n * Routing goes through the outlet, so the ARIA wiring is never duplicated here.\n */\n #applyGroup(group: FieldGroup): ValidatableControl | null {\n // Apply declarative custom rules (e.g. disallow=\"whitespace\") before reading\n // validity so they participate in checkValidity() like a native constraint.\n for (const control of group.controls) this.#syncCustomValidity(control);\n const firstInvalid = group.controls.find((control) => !control.checkValidity()) ?? null;\n if (group.field) {\n if (firstInvalid) {\n const message = this.#messageFor(firstInvalid);\n const alreadyReported =\n group.field.element.hasAttribute(FORM_FIELD_INVALID_ATTR) &&\n this.#reportedErrors.get(group.field) === message;\n if (!alreadyReported) group.field.setError(message, { focus: false });\n this.#reportedErrors.set(group.field, message);\n } else if (\n group.field.element.hasAttribute(FORM_FIELD_INVALID_ATTR) ||\n this.#reportedErrors.has(group.field)\n ) {\n group.field.clearError();\n this.#reportedErrors.delete(group.field);\n }\n }\n return firstInvalid;\n }\n\n /**\n * Resolves the message to show for an invalid control: a per-constraint\n * override (`data-stimeo--form-validation-message-<constraint>`) for the first failing\n * `ValidityState` flag, then a generic `data-stimeo--form-validation-message`\n * override, then the browser's native `validationMessage`. Authoring an override\n * gives controlled, localizable, theme-able wording with **no consumer JS** —\n * and sidesteps headless browsers that return an empty native message.\n */\n #messageFor(control: ValidatableControl): string {\n // `setCustomValidity()` owns its wording. In particular, a consumer's custom\n // error must not be replaced by a simultaneous native-constraint override.\n if (control.validity.customError) return control.validationMessage;\n for (const [flag, key] of CONSTRAINT_MESSAGE_KEYS) {\n if (control.validity[flag]) {\n return (\n this.#authoredMessage(control, `${MESSAGE_ATTR_PREFIX}${key}`) ??\n this.#authoredMessage(control, MESSAGE_ATTR_GENERIC) ??\n control.validationMessage\n );\n }\n }\n return control.validationMessage || this.#authoredMessage(control, MESSAGE_ATTR_GENERIC) || \"\";\n }\n\n /**\n * Applies (or clears) a declarative custom constraint via `setCustomValidity`,\n * for controls that opt in with `data-stimeo--form-validation-disallow`. The supported\n * rule is `\"whitespace\"` — a value that is non-empty but blank\n * after trimming (which slips past `required` / `minlength`); its message follows\n * the whitespace-specific → generic → default chain.\n *\n * Don't-clobber-authored-state: an unknown/absent rule is never written, an\n * existing consumer custom error wins, and a controller error is only cleared\n * while the live message still equals the recorded controller write.\n */\n #syncCustomValidity(control: ValidatableControl): void {\n const recorded = this.#ownedCustomErrors.get(control);\n const ownsCurrent = recorded !== undefined && control.validationMessage === recorded;\n\n const violates =\n control.getAttribute(DISALLOW_ATTR) === \"whitespace\" &&\n control.value.length > 0 &&\n control.value.trim() === \"\";\n if (!violates) {\n if (ownsCurrent) control.setCustomValidity(\"\");\n this.#ownedCustomErrors.delete(control);\n return;\n }\n\n // One custom-validity slot exists per control. Preserve a consumer's live\n // entry; once they clear it, the next validation applies the declarative rule.\n if (control.validity.customError && !ownsCurrent) return;\n\n const message =\n this.#authoredMessage(control, DISALLOW_WHITESPACE_MESSAGE) ??\n this.#authoredMessage(control, MESSAGE_ATTR_GENERIC) ??\n DISALLOW_WHITESPACE_DEFAULT;\n if (!ownsCurrent || recorded !== message) control.setCustomValidity(message);\n this.#ownedCustomErrors.set(control, message);\n }\n\n /** Returns a non-blank authored message, otherwise falls through to a fallback. */\n #authoredMessage(control: ValidatableControl, attribute: string): string | null {\n const message = control.getAttribute(attribute);\n return message && message.trim().length > 0 ? message : null;\n }\n\n /**\n * A grouping key that collects controls belonging to the same field: the owning\n * `stimeo--form-field` when present, else a radio group's shared `name`, else\n * the control itself (always distinct).\n */\n #keyFor(control: ValidatableControl, field: FormFieldController | undefined): FieldGroupKey {\n if (field) return field;\n if (control instanceof HTMLInputElement && control.type === \"radio\" && control.name) {\n return `radio:${control.name}`;\n }\n return control;\n }\n\n /**\n * Where focus should land for an invalid control. A visible control is focused\n * directly — the case for native fields and radios. A validatable mirror\n * (the `hidden` attribute) cannot receive focus, so focus is delegated to the\n * visible widget: the owning field's `control` target when it is itself\n * focusable, else its first focusable descendant (e.g. a roving-tabindex\n * member). Resolved structurally — never by probing `focus()` — so behavior\n * is deterministic and CSS-independent.\n */\n #focusTargetFor(control: ValidatableControl): HTMLElement | null {\n if (canTakeFocus(control)) return control;\n const field = this.#fieldFor(control, this.#fieldOutlets());\n if (!field?.hasControlTarget) return null;\n const root = field.controlTarget;\n if (isTabStop(root)) return root;\n return firstTabStop(root);\n }\n\n /** Pairs each live outlet element with its controller once per operation. */\n #fieldOutlets(): Map<HTMLElement, FormFieldController> {\n const fields = new Map<HTMLElement, FormFieldController>();\n const elements = this.stimeoFormFieldOutletElements;\n const outlets = this.stimeoFormFieldOutlets;\n for (let index = 0; index < elements.length; index++) {\n const element = elements[index];\n const outlet = outlets[index];\n if (element && outlet) fields.set(element, outlet);\n }\n return fields;\n }\n\n /** The nearest configured field ancestor of `control`, if any. */\n #fieldFor(\n control: ValidatableControl,\n fields: ReadonlyMap<HTMLElement, FormFieldController>,\n ): FormFieldController | undefined {\n let ancestor: HTMLElement | null = control;\n while (ancestor) {\n const field = fields.get(ancestor);\n if (field) return field;\n ancestor = ancestor.parentElement;\n }\n return undefined;\n }\n\n /** This form's native controls that participate in constraint validation. */\n get #controls(): ValidatableControl[] {\n const controls: ValidatableControl[] = [];\n for (const element of Array.from(this.element.elements)) {\n if (this.#isValidatable(element)) controls.push(element);\n }\n return controls;\n }\n\n /** Narrows an event target to a validatable control. */\n #controlFrom(target: EventTarget | null): ValidatableControl | null {\n return target instanceof Element && this.#isValidatable(target) && target.form === this.element\n ? target\n : null;\n }\n\n #isValidatable(element: Element): element is ValidatableControl {\n return (\n (element instanceof HTMLInputElement ||\n element instanceof HTMLSelectElement ||\n element instanceof HTMLTextAreaElement) &&\n // `willValidate` already excludes disabled, read-only, input[type=hidden],\n // and button-type controls. The `hidden` attribute intentionally does not\n // bar a text mirror from constraint validation.\n element.willValidate\n );\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/default_attribute.ts","../../src/utils/focus_candidate.ts","../../src/controllers/form_validation_controller.ts"],"names":[],"mappings":"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","file":"form_validation_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 on its own for callers that need the disabled-inheritance rule without\n * the rest of {@link canTakeFocus} — a control's availability check, or a `hidden`\n * walk bounded at a widget root, which is a different rule to compose with.\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 four conditions that make the platform refuse\n * focus are checked: `hidden` / `inert` (an ancestor's counts),\n * `input[type=\"hidden\"]`, the native `disabled` property, and `disabled`\n * inherited from an ancestor `fieldset`. CSS-only invisibility is handled by\n * {@link isRenderedForFocus} when a consumer needs sequential-focus semantics.\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], [inert]\")) return false;\n if (element instanceof HTMLInputElement && element.type === \"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/** Elements whose semantics or authored attributes can place them in sequential focus order. */\nexport const TAB_STOP_CANDIDATE_SELECTOR = [\n \"a[href]\",\n \"area[href]\",\n \"button\",\n \"input\",\n \"select\",\n \"textarea\",\n \"summary\",\n \"iframe\",\n \"audio[controls]\",\n \"video[controls]\",\n \"[tabindex]\",\n \"[contenteditable]\",\n].join(\",\");\n\n/** Optional browser visibility API used to exclude CSS-hidden candidates. */\ninterface VisibilityCheckable {\n checkVisibility?: (options?: { visibilityProperty?: boolean }) => boolean;\n}\n\n/** Whether CSS visibility allows an otherwise eligible element to participate in focus order. */\nexport function isRenderedForFocus(element: HTMLElement): boolean {\n const check = (element as HTMLElement & VisibilityCheckable).checkVisibility;\n return typeof check === \"function\" ? check.call(element, { visibilityProperty: true }) : true;\n}\n\n/** Parses an authored `tabindex`; invalid syntax has no explicit focus-order meaning. */\nfunction authoredTabindex(element: HTMLElement): number | null {\n const value = element.getAttribute(\"tabindex\");\n if (value === null || !/^[+-]?\\d+$/.test(value.trim())) return null;\n return Number(value);\n}\n\n/** Whether the element's native semantics place it in sequential focus order. */\nfunction hasNativeTabStop(element: HTMLElement): boolean {\n if (element instanceof HTMLAnchorElement || element instanceof HTMLAreaElement) {\n return element.hasAttribute(\"href\");\n }\n if (\n element instanceof HTMLButtonElement ||\n element instanceof HTMLSelectElement ||\n element instanceof HTMLTextAreaElement\n ) {\n return true;\n }\n if (element instanceof HTMLInputElement) return element.type !== \"hidden\";\n if (element instanceof HTMLIFrameElement) return true;\n if (element.tagName === \"AUDIO\" || element.tagName === \"VIDEO\") {\n return element.hasAttribute(\"controls\");\n }\n if (element instanceof HTMLElement && element.tagName === \"SUMMARY\") {\n const details = element.parentElement;\n return (\n details instanceof HTMLDetailsElement &&\n Array.from(details.children).find((child) => child.tagName === \"SUMMARY\") === element\n );\n }\n return false;\n}\n\n/** Whether an explicit `contenteditable` value creates an editable tab stop. */\nfunction hasEditableTabStop(element: HTMLElement): boolean {\n const value = element.getAttribute(\"contenteditable\")?.toLowerCase();\n return value === \"\" || value === \"true\" || value === \"plaintext-only\";\n}\n\n/**\n * Whether an element is a usable sequential Tab stop right now.\n *\n * Native semantics, authored `tabindex`, editable hosts, inherited disabled state,\n * HTML `hidden`/`inert`, and CSS visibility are evaluated together. `aria-disabled`\n * remains focusable because it communicates unavailability without removing the\n * control from discovery order.\n */\nexport function isTabStop(element: HTMLElement): boolean {\n if (!canTakeFocus(element) || !isRenderedForFocus(element)) return false;\n\n const tabindex = authoredTabindex(element);\n if (tabindex !== null) return tabindex >= 0;\n return hasNativeTabStop(element) || hasEditableTabStop(element);\n}\n\n/** Returns every usable sequential Tab stop below `root` in document order. */\nexport function tabStopsWithin(root: ParentNode): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(TAB_STOP_CANDIDATE_SELECTOR)).filter(\n isTabStop,\n );\n}\n\n/** Returns the first usable sequential Tab stop below `root`, if one exists. */\nexport function firstTabStop(root: ParentNode): HTMLElement | null {\n for (const candidate of root.querySelectorAll<HTMLElement>(TAB_STOP_CANDIDATE_SELECTOR)) {\n if (isTabStop(candidate)) return candidate;\n }\n return null;\n}\n\n/** Whether `root` contains at least one usable sequential Tab stop. */\nexport function hasTabStop(root: ParentNode): boolean {\n return firstTabStop(root) !== null;\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { setDefaultAttribute } from \"../utils/default_attribute\";\nimport { canTakeFocus, firstTabStop, isTabStop } from \"../utils/focus_candidate\";\nimport type { FormFieldController } from \"./form_field_controller\";\n\n/** Native form controls that participate in constraint validation. */\ntype ValidatableControl = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;\n\n/**\n * Maps each `ValidityState` flag (in priority order) to the kebab-case suffix of\n * its per-constraint message-override attribute. The first flag that is `true`\n * wins, so e.g. `valueMissing` is reported before a stale `patternMismatch`.\n */\nconst CONSTRAINT_MESSAGE_KEYS: ReadonlyArray<readonly [keyof ValidityState, string]> = [\n [\"valueMissing\", \"value-missing\"],\n [\"typeMismatch\", \"type-mismatch\"],\n [\"patternMismatch\", \"pattern-mismatch\"],\n [\"tooShort\", \"too-short\"],\n [\"tooLong\", \"too-long\"],\n [\"rangeUnderflow\", \"range-underflow\"],\n [\"rangeOverflow\", \"range-overflow\"],\n [\"stepMismatch\", \"step-mismatch\"],\n [\"badInput\", \"bad-input\"],\n];\n\n/** Suffix of a generic message override applied to any failing constraint. */\nconst MESSAGE_PART = \"message\";\n/** Suffix opting a control into a declarative custom rule (`\"whitespace\"`). */\nconst DISALLOW_PART = \"disallow\";\n/** Default message when `disallow=\"whitespace\"` fails and no override is given. */\nconst DISALLOW_WHITESPACE_DEFAULT = \"Please enter a value that is not only whitespace.\";\n\n/** Public invalid-state hook owned by `stimeo--form-field`. */\nconst FORM_FIELD_INVALID_ATTR = \"data-stimeo--form-field-invalid\";\n\n/** Identity used to collect controls into one validation field. */\ntype FieldGroupKey = FormFieldController | ValidatableControl | string;\n\n/** A field's validatable controls, grouped so the whole field validates together. */\ninterface FieldGroup {\n readonly key: FieldGroupKey;\n readonly field: FormFieldController | undefined;\n readonly controls: ValidatableControl[];\n}\n\n/** Live form grouping resolved once for a validation operation. */\ninterface ValidationSnapshot {\n readonly groups: Map<FieldGroupKey, FieldGroup>;\n /** `null` is a deliberate miss key so delegated non-control events need one guard. */\n readonly groupByControl: Map<ValidatableControl | null, FieldGroup>;\n}\n\n/**\n * Headless, accessible **form-validation orchestration**.\n *\n * Markup contract (identifier: `stimeo--form-validation`):\n * <form data-controller=\"stimeo--form-validation\"\n * data-stimeo--form-validation-stimeo--form-field-outlet=\"[data-controller~='stimeo--form-field']\">\n * <div data-controller=\"stimeo--form-field\">\n * <label for=\"email\">Email</label>\n * <input id=\"email\" type=\"email\" required\n * data-stimeo--form-field-target=\"control\" />\n * <p hidden data-stimeo--form-field-target=\"error\"></p>\n * </div>\n * <button type=\"submit\">Save</button>\n * </form>\n *\n * Not an APG widget pattern — this is the *timing* layer for **error\n * identification** (WCAG 3.3.1) and **error suggestion** (3.3.3): it decides\n * *when* each control is checked and routes the browser's native\n * `validationMessage` into the field's {@link FormFieldController} error region.\n * The per-field ARIA wiring (`aria-invalid` / `aria-errormessage` /\n * `aria-describedby`) therefore lives in exactly one place — `stimeo--form-field`,\n * reached through a Stimulus **outlet** — and is never re-implemented here.\n *\n * `valid` dispatches `{}`; `invalid` dispatches `{ invalid: HTMLElement[] }`.\n *\n * @remarks\n * Behavior only — validation **rules** stay in the markup (native HTML\n * constraints: `required`, `type`, `pattern`, `min`/`max`, …) or in the consumer's\n * own `setCustomValidity()` calls, which `checkValidity()` surfaces transparently.\n * It sets the form's `novalidate` so it can replace the browser's default error\n * bubbles with accessible in-page visual errors plus the shared Announcer, and\n * restores the attribute on disconnect.\n *\n * Two declarative escape hatches let a field **exceed** native validation with no\n * consumer JS (author them on the control):\n * - **Per-constraint messages** — `data-stimeo--form-validation-message-<constraint>`\n * (`value-missing`, `too-short`, `too-long`, `pattern-mismatch`, `type-mismatch`,\n * `range-overflow`, `range-underflow`, `step-mismatch`, `bad-input`), or a generic\n * `data-stimeo--form-validation-message` fallback, override the shown text per failing\n * `ValidityState` flag — controlled, localizable wording that also fixes headless\n * browsers returning an empty native `validationMessage`. Falls back to native.\n * - **`data-stimeo--form-validation-disallow=\"whitespace\"`** — a built-in custom rule\n * rejecting a value that is blank after trimming (which slips past `required` /\n * `minlength`), wired through `setCustomValidity` so it blocks submit like any\n * native constraint.\n *\n * Behavior provided:\n * - On connect, suppresses native bubbles (`novalidate`, restored on disconnect)\n * and intercepts the form's `submit` in the **capture phase** so an invalid form\n * is cancelled before any other submit handler (e.g. `stimeo--submit-once`)\n * reacts to a submission that will never happen.\n * - On submit, validates every control; if any is invalid it blocks submission,\n * moves focus to the first invalid control (unless `focusInvalid` is `false`),\n * and dispatches `stimeo--form-validation:invalid`. An all-valid form dispatches\n * `:valid` and submits normally.\n * - Validates a field on blur once it has been interacted with (`validateOnBlur`),\n * and re-validates it on input while it is already touched\n * (`revalidateOnInput`) so a shown message clears the moment the value becomes\n * valid — but a pristine field is never eagerly flagged mid-typing.\n *\n * A control with no owning `stimeo--form-field` outlet is still validated (it can\n * block submit and receive focus) but renders no message.\n *\n * Radio groups work unchanged: point the field's `control` target at the\n * `role=\"radiogroup\"` container so the invalid state lands on the group, and the\n * group is reported as a single invalid entry (not one per radio).\n *\n * Rich widgets (listbox, time-picker, …) that keep their committed value in a\n * hidden holder participate by making that holder a **validatable mirror**:\n * `<input type=\"text\" hidden required>` — the `hidden` *attribute*, not\n * `type=\"hidden\"`, which is barred from constraint validation. Native\n * constraints then govern the widget's value with no extra JavaScript. The\n * widget dispatches a bubbling `change` on the mirror when a value is committed\n * (a completed interaction, so it validates immediately), and focus for an\n * invalid mirror is delegated to the field's visible `control` target — the\n * target itself when focusable, else its first focusable descendant.\n *\n * No-JS caveat: a `required` mirror also gates the browser's own pre-Stimulus\n * validation, which cannot surface UI on an invisible control. When the no-JS\n * fallback matters, author `novalidate` on the form (this controller preserves\n * an author-set attribute) so the submission reaches the server's validation.\n */\nexport class FormValidationController extends Controller<HTMLFormElement> {\n /** The generic message override, in the namespace this controller is registered under. */\n get #messageAttribute(): string {\n return `data-${this.identifier}-${MESSAGE_PART}`;\n }\n\n /** Marks the `novalidate` this controller applied, so an authored one is left alone. */\n get #novalidateMarker(): string {\n return `data-${this.identifier}-${FormValidationController.#NOVALIDATE_PART}`;\n }\n\n static override outlets = [\"stimeo--form-field\"];\n static override values = {\n validateOnBlur: { type: Boolean, default: true },\n validateOnChange: { type: Boolean, default: true },\n revalidateOnInput: { type: Boolean, default: true },\n focusInvalid: { type: Boolean, default: true },\n };\n static actions = [\"validate\"] as const;\n static events = [\"valid\", \"invalid\"] as const;\n\n declare readonly stimeoFormFieldOutlets: FormFieldController[];\n declare readonly stimeoFormFieldOutletElements: HTMLElement[];\n\n declare validateOnBlurValue: boolean;\n declare validateOnChangeValue: boolean;\n declare revalidateOnInputValue: boolean;\n declare focusInvalidValue: boolean;\n\n /** Suffix of the marker recording that we added `novalidate`, so we only remove our own. */\n static readonly #NOVALIDATE_PART = \"novalidate\";\n\n /** Object-backed groups already interacted with — the input revalidation gate. */\n readonly #touchedGroups = new WeakSet<FormFieldController | ValidatableControl>();\n\n /** String-backed fallback radio groups already interacted with. */\n readonly #touchedRadioGroups = new Set<string>();\n\n /**\n * Last message this controller wrote through `setCustomValidity`, kept as a\n * two-layer ownership ledger: a value is cleared only while the live message\n * still equals the recorded write. Iterable so disconnect can release every\n * surviving loan without touching a consumer's later custom error.\n */\n readonly #ownedCustomErrors = new Map<ValidatableControl, string>();\n\n /** Last invalid message routed to each field, suppressing duplicate reports. */\n readonly #reportedErrors = new WeakMap<FormFieldController, string>();\n\n /** Exact document that owns the delegated listeners for this connection. */\n #listenerDocument: Document | null = null;\n\n readonly #onSubmit = (event: SubmitEvent): void => {\n if (event.target !== this.element) return;\n const invalid = this.#validateAll();\n if (invalid.length === 0) {\n this.dispatch(\"valid\", { detail: {} });\n return;\n }\n // Cancel the whole submit: preventDefault stops native/Turbo navigation;\n // stopImmediatePropagation keeps later submit handlers (e.g. submit-once's\n // busy state) from acting on a submission that will never happen.\n event.preventDefault();\n event.stopImmediatePropagation();\n const first = invalid[0];\n if (this.focusInvalidValue && first) this.#focusTargetFor(first)?.focus();\n this.dispatch(\"invalid\", { detail: { invalid } });\n };\n\n readonly #onFocusOut = (event: FocusEvent): void => {\n if (!this.validateOnBlurValue) return;\n const snapshot = this.#snapshot();\n const group = snapshot.groupByControl.get(this.#controlFrom(event.target));\n if (!group) return;\n // Focus moving *within* the same field (e.g. between members of a radio\n // group) is not leaving it — defer validation until focus actually exits.\n const related = event.relatedTarget;\n if (group.field && related instanceof Node && group.field.element.contains(related)) return;\n if (snapshot.groupByControl.get(this.#controlFrom(related))?.key === group.key) return;\n this.#markTouched(group.key);\n this.#applyGroup(group);\n };\n\n readonly #onInput = (event: Event): void => {\n if (!this.revalidateOnInputValue) return;\n const group = this.#snapshot().groupByControl.get(this.#controlFrom(event.target));\n // Only re-validate a field the user has already left once, so the first\n // keystroke never eagerly flags a control they are still filling in.\n if (!group || !this.#isTouched(group.key)) return;\n this.#applyGroup(group);\n };\n\n readonly #onChange = (event: Event): void => {\n if (!this.validateOnChangeValue) return;\n const group = this.#snapshot().groupByControl.get(this.#controlFrom(event.target));\n if (!group) return;\n // change marks a *committed* interaction (a picked option, a toggled box, a\n // widget writing its mirror), so unlike input it both touches and validates.\n this.#markTouched(group.key);\n this.#applyGroup(group);\n };\n\n /** Suppresses native bubbles and binds the submit / blur / input listeners. */\n override connect(): void {\n if (setDefaultAttribute(this.element, \"novalidate\", \"\")) {\n this.element.setAttribute(this.#novalidateMarker, \"\");\n }\n // Capture submit before form-bound actions. The other bubbling events live on\n // the same document so controls associated through `form=\"id\"` participate too;\n // #controlFrom rejects every control owned by a different form.\n this.#listenerDocument = this.element.ownerDocument;\n this.#listenerDocument.addEventListener(\"submit\", this.#onSubmit, true);\n this.#listenerDocument.addEventListener(\"focusout\", this.#onFocusOut);\n this.#listenerDocument.addEventListener(\"input\", this.#onInput);\n this.#listenerDocument.addEventListener(\"change\", this.#onChange);\n }\n\n /** Tears down listeners and restores `novalidate` if we added it. */\n override disconnect(): void {\n this.#listenerDocument?.removeEventListener(\"submit\", this.#onSubmit, true);\n this.#listenerDocument?.removeEventListener(\"focusout\", this.#onFocusOut);\n this.#listenerDocument?.removeEventListener(\"input\", this.#onInput);\n this.#listenerDocument?.removeEventListener(\"change\", this.#onChange);\n this.#listenerDocument = null;\n for (const [control, message] of this.#ownedCustomErrors) {\n if (control.validationMessage === message) control.setCustomValidity(\"\");\n }\n this.#ownedCustomErrors.clear();\n this.#touchedRadioGroups.clear();\n if (this.element.hasAttribute(this.#novalidateMarker)) {\n this.element.removeAttribute(\"novalidate\");\n this.element.removeAttribute(this.#novalidateMarker);\n }\n }\n\n /**\n * Validates every control now, rendering or clearing each field's message, and\n * returns whether the whole form is valid. Marks every field/group touched so\n * a later input from any sibling re-validates it. Bound via `data-action`\n * (`#validate`) or callable directly (e.g. before a programmatic submit).\n */\n validate(): boolean {\n return this.#validateAll().length === 0;\n }\n\n /**\n * Validates every control and returns one invalid control per field. Controls\n * are grouped by field first (see {@link #keyFor}) so a field with several\n * controls — a radio group, or a mirror plus its visible control — reflects\n * *all* of them: a valid sibling must never clear an invalid one's message.\n * Each group's first invalid control supplies the message and the focus target.\n */\n #validateAll(): ValidatableControl[] {\n const invalid: ValidatableControl[] = [];\n for (const group of this.#snapshot().groups.values()) {\n this.#markTouched(group.key);\n const firstInvalid = this.#applyGroup(group);\n if (firstInvalid) invalid.push(firstInvalid);\n }\n return invalid;\n }\n\n /** Builds the current field table in one pass over form controls and DOM depth. */\n #snapshot(): ValidationSnapshot {\n const fields = this.#fieldOutlets();\n const groups = new Map<FieldGroupKey, FieldGroup>();\n const groupByControl = new Map<ValidatableControl | null, FieldGroup>();\n\n for (const control of this.#controls) {\n const field = this.#fieldFor(control, fields);\n const key = this.#keyFor(control, field);\n let group = groups.get(key);\n if (!group) {\n group = { key, field, controls: [] };\n groups.set(key, group);\n }\n group.controls.push(control);\n groupByControl.set(control, group);\n }\n\n return { groups, groupByControl };\n }\n\n /** Records that a whole field/group, rather than one sibling control, was visited. */\n #markTouched(key: FieldGroupKey): void {\n if (typeof key === \"string\") {\n this.#touchedRadioGroups.add(key);\n } else {\n this.#touchedGroups.add(key);\n }\n }\n\n #isTouched(key: FieldGroupKey): boolean {\n return typeof key === \"string\"\n ? this.#touchedRadioGroups.has(key)\n : this.#touchedGroups.has(key);\n }\n\n /**\n * Runs native constraint validation across a field's controls and routes the\n * result to its `stimeo--form-field` outlet: the first invalid control's\n * `validationMessage` is shown, an all-valid field is cleared. Returns the\n * first invalid control (for the invalid list / focus), or `null` when valid.\n * Routing goes through the outlet, so the ARIA wiring is never duplicated here.\n */\n #applyGroup(group: FieldGroup): ValidatableControl | null {\n // Apply declarative custom rules (e.g. disallow=\"whitespace\") before reading\n // validity so they participate in checkValidity() like a native constraint.\n for (const control of group.controls) this.#syncCustomValidity(control);\n const firstInvalid = group.controls.find((control) => !control.checkValidity()) ?? null;\n if (group.field) {\n if (firstInvalid) {\n const message = this.#messageFor(firstInvalid);\n const alreadyReported =\n group.field.element.hasAttribute(FORM_FIELD_INVALID_ATTR) &&\n this.#reportedErrors.get(group.field) === message;\n if (!alreadyReported) group.field.setError(message, { focus: false });\n this.#reportedErrors.set(group.field, message);\n } else if (\n group.field.element.hasAttribute(FORM_FIELD_INVALID_ATTR) ||\n this.#reportedErrors.has(group.field)\n ) {\n group.field.clearError();\n this.#reportedErrors.delete(group.field);\n }\n }\n return firstInvalid;\n }\n\n /**\n * Resolves the message to show for an invalid control: a per-constraint\n * override (`data-stimeo--form-validation-message-<constraint>`) for the first failing\n * `ValidityState` flag, then a generic `data-stimeo--form-validation-message`\n * override, then the browser's native `validationMessage`. Authoring an override\n * gives controlled, localizable, theme-able wording with **no consumer JS** —\n * and sidesteps headless browsers that return an empty native message.\n */\n #messageFor(control: ValidatableControl): string {\n // `setCustomValidity()` owns its wording. In particular, a consumer's custom\n // error must not be replaced by a simultaneous native-constraint override.\n if (control.validity.customError) return control.validationMessage;\n for (const [flag, key] of CONSTRAINT_MESSAGE_KEYS) {\n if (control.validity[flag]) {\n return (\n this.#authoredMessage(control, `${this.#messageAttribute}-${key}`) ??\n this.#authoredMessage(control, this.#messageAttribute) ??\n control.validationMessage\n );\n }\n }\n return (\n control.validationMessage || this.#authoredMessage(control, this.#messageAttribute) || \"\"\n );\n }\n\n /**\n * Applies (or clears) a declarative custom constraint via `setCustomValidity`,\n * for controls that opt in with `data-stimeo--form-validation-disallow`. The supported\n * rule is `\"whitespace\"` — a value that is non-empty but blank\n * after trimming (which slips past `required` / `minlength`); its message follows\n * the whitespace-specific → generic → default chain.\n *\n * Don't-clobber-authored-state: an unknown/absent rule is never written, an\n * existing consumer custom error wins, and a controller error is only cleared\n * while the live message still equals the recorded controller write.\n */\n #syncCustomValidity(control: ValidatableControl): void {\n const recorded = this.#ownedCustomErrors.get(control);\n const ownsCurrent = recorded !== undefined && control.validationMessage === recorded;\n\n const violates =\n control.getAttribute(`data-${this.identifier}-${DISALLOW_PART}`) === \"whitespace\" &&\n control.value.length > 0 &&\n control.value.trim() === \"\";\n if (!violates) {\n if (ownsCurrent) control.setCustomValidity(\"\");\n this.#ownedCustomErrors.delete(control);\n return;\n }\n\n // One custom-validity slot exists per control. Preserve a consumer's live\n // entry; once they clear it, the next validation applies the declarative rule.\n if (control.validity.customError && !ownsCurrent) return;\n\n const message =\n this.#authoredMessage(control, `${this.#messageAttribute}-whitespace`) ??\n this.#authoredMessage(control, this.#messageAttribute) ??\n DISALLOW_WHITESPACE_DEFAULT;\n if (!ownsCurrent || recorded !== message) control.setCustomValidity(message);\n this.#ownedCustomErrors.set(control, message);\n }\n\n /** Returns a non-blank authored message, otherwise falls through to a fallback. */\n #authoredMessage(control: ValidatableControl, attribute: string): string | null {\n const message = control.getAttribute(attribute);\n return message && message.trim().length > 0 ? message : null;\n }\n\n /**\n * A grouping key that collects controls belonging to the same field: the owning\n * `stimeo--form-field` when present, else a radio group's shared `name`, else\n * the control itself (always distinct).\n */\n #keyFor(control: ValidatableControl, field: FormFieldController | undefined): FieldGroupKey {\n if (field) return field;\n if (control instanceof HTMLInputElement && control.type === \"radio\" && control.name) {\n return `radio:${control.name}`;\n }\n return control;\n }\n\n /**\n * Where focus should land for an invalid control. A visible control is focused\n * directly — the case for native fields and radios. A validatable mirror\n * (the `hidden` attribute) cannot receive focus, so focus is delegated to the\n * visible widget: the owning field's `control` target when it is itself\n * focusable, else its first focusable descendant (e.g. a roving-tabindex\n * member). Resolved structurally — never by probing `focus()` — so behavior\n * is deterministic and CSS-independent.\n */\n #focusTargetFor(control: ValidatableControl): HTMLElement | null {\n if (canTakeFocus(control)) return control;\n const field = this.#fieldFor(control, this.#fieldOutlets());\n if (!field?.hasControlTarget) return null;\n const root = field.controlTarget;\n if (isTabStop(root)) return root;\n return firstTabStop(root);\n }\n\n /** Pairs each live outlet element with its controller once per operation. */\n #fieldOutlets(): Map<HTMLElement, FormFieldController> {\n const fields = new Map<HTMLElement, FormFieldController>();\n const elements = this.stimeoFormFieldOutletElements;\n const outlets = this.stimeoFormFieldOutlets;\n for (let index = 0; index < elements.length; index++) {\n const element = elements[index];\n const outlet = outlets[index];\n if (element && outlet) fields.set(element, outlet);\n }\n return fields;\n }\n\n /** The nearest configured field ancestor of `control`, if any. */\n #fieldFor(\n control: ValidatableControl,\n fields: ReadonlyMap<HTMLElement, FormFieldController>,\n ): FormFieldController | undefined {\n let ancestor: HTMLElement | null = control;\n while (ancestor) {\n const field = fields.get(ancestor);\n if (field) return field;\n ancestor = ancestor.parentElement;\n }\n return undefined;\n }\n\n /** This form's native controls that participate in constraint validation. */\n get #controls(): ValidatableControl[] {\n const controls: ValidatableControl[] = [];\n for (const element of Array.from(this.element.elements)) {\n if (this.#isValidatable(element)) controls.push(element);\n }\n return controls;\n }\n\n /** Narrows an event target to a validatable control. */\n #controlFrom(target: EventTarget | null): ValidatableControl | null {\n return target instanceof Element && this.#isValidatable(target) && target.form === this.element\n ? target\n : null;\n }\n\n #isValidatable(element: Element): element is ValidatableControl {\n return (\n (element instanceof HTMLInputElement ||\n element instanceof HTMLSelectElement ||\n element instanceof HTMLTextAreaElement) &&\n // `willValidate` already excludes disabled, read-only, input[type=hidden],\n // and button-type controls. The `hidden` attribute intentionally does not\n // bar a text mirror from constraint validation.\n element.willValidate\n );\n }\n}\n"]}
|