stimeo-ui 0.12.0 → 0.13.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 +37 -0
- package/dist/cable/index.d.ts +67 -13
- package/dist/cable/index.js +438 -152
- package/dist/cable/index.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.d.ts +1 -1
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- package/dist/controllers/announcer_controller.d.ts +1 -1
- package/dist/controllers/announcer_controller.js.map +1 -1
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.d.ts +1 -1
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.d.ts +2 -1
- package/dist/controllers/calendar_controller.js +10 -2
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.js +1 -1
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/character_counter_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.d.ts +1 -1
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +1 -1
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.d.ts +3 -3
- package/dist/controllers/command_palette_controller.js +2 -2
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/conditional_fields_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.d.ts +2 -2
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +1 -1
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +1 -1
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.d.ts +1 -1
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.js +10 -10
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.d.ts +1 -1
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/dirty_form_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.d.ts +2 -2
- package/dist/controllers/drawer_controller.js +2 -2
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/dropdown_controller.d.ts +1 -1
- package/dist/controllers/dropdown_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +2 -2
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +3 -3
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +1 -1
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +1 -1
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.d.ts +1 -1
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +1 -1
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menu_controller.d.ts +1 -1
- package/dist/controllers/menu_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.d.ts +2 -2
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/navigation_menu_controller.d.ts +1 -1
- package/dist/controllers/navigation_menu_controller.js.map +1 -1
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/optimistic_controller.d.ts +23 -10
- package/dist/controllers/optimistic_controller.js +141 -20
- package/dist/controllers/optimistic_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.d.ts +1 -1
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.js.map +1 -1
- package/dist/controllers/password_strength_controller.d.ts +2 -1
- package/dist/controllers/password_strength_controller.js.map +1 -1
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +1 -1
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +2 -2
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.d.ts +1 -1
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.d.ts +3 -3
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/roving_controller.d.ts +1 -1
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.d.ts +1 -1
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.d.ts +2 -2
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.d.ts +3 -3
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/sticky_observer_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +3 -3
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.d.ts +1 -1
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/theme_controller.d.ts +3 -2
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/toast_controller.js +1 -1
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.d.ts +1 -1
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +1 -1
- package/dist/controllers/tooltip_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +1 -1
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +4 -3
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.js +101 -38
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +1 -1
- package/dist/inspector/cli.js +1 -1
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +1 -1
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +1 -1
- package/dist/inspector/manifest.json +6 -3
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/default_attribute.ts","../../src/utils/focus_candidate.ts","../../src/utils/interactive_host.ts","../../src/controllers/switch_controller.ts"],"names":[],"mappings":";;;;;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;;;ACZO,IAAM,yBAAA,GACX,wIAAA;AASK,SAAS,kBAAkB,OAAA,EAA+B;AAC/D,EAAA,IAAI,OAAA,CAAQ,OAAA,CAAQ,yBAAyB,CAAA,EAAG,OAAO,IAAA;AAEvD,EAAA,IAAI,OAAA,GAA8B,OAAA;AAClC,EAAA,OAAO,OAAA,EAAS;AACd,IAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,YAAA,CAAa,iBAAiB,CAAA;AAClD,IAAA,IAAI,QAAQ,IAAA,EAAM;AAChB,MAAA,MAAM,KAAA,GAAQ,GAAA,CAAI,IAAA,EAAK,CAAE,WAAA,EAAY;AACrC,MAAA,IAAI,KAAA,KAAU,SAAS,OAAO,KAAA;AAC9B,MAAA,IAAI,UAAU,EAAA,IAAM,KAAA,KAAU,MAAA,IAAU,KAAA,KAAU,kBAAkB,OAAO,IAAA;AAAA,IAC7E;AACA,IAAA,OAAA,GAAU,OAAA,CAAQ,aAAA;AAAA,EACpB;AACA,EAAA,OAAO,KAAA;AACT;;;ACzBA,IAAM,mBAAA,GAAsB;AAAA,EAC1B,MAAA;AAAA,EACA,cAAA;AAAA,EACA,UAAA;AAAA,EACA,MAAA;AAAA,EACA,MAAA;AAAA,EACA,iBAAA;AAAA,EACA;AACF,CAAA;AAGA,IAAM,4BAAA,GAA+B,CAAC,iBAAiB,CAAA;AA6BhD,IAAM,gBAAA,GAAN,cAA+B,UAAA,CAAwB;AAAA,EAC5D,OAAO,OAAA,GAAU,CAAC,WAAA,EAAa,QAAQ,CAAA;AAAA,EACvC,OAAO,MAAA,GAAS,CAAC,SAAS,CAAA;AAAA;AAAA,EAGjB,cAAA,uBAAqB,GAAA,EAAY;AAAA;AAAA,EAEjC,wBAAA,uBAA+B,GAAA,EAAoB;AAAA,EAEnD,kBAAA,GAAqB,IAAI,gBAAA,CAAiB,CAAC,OAAA,KAAY;AAC9D,IAAA,IAAA,CAAK,yBAAyB,OAAO,CAAA;AAGrC,IAAA,IAAA,CAAK,mBAAmB,UAAA,EAAW;AACnC,IAAA,IAAA,CAAK,kBAAA,EAAmB;AACxB,IAAA,IAAA,CAAK,kBAAA,EAAmB;AAAA,EAC1B,CAAC,CAAA;AAAA;AAAA,EAGQ,eAAA,GAAkB,CAAC,KAAA,KAA4B;AACtD,IAAA,IAAI,CAAC,IAAA,CAAK,gBAAA,IAAoB,CAAC,KAAK,qBAAA,EAAuB;AAC3D,IAAA,KAAA,CAAM,cAAA,EAAe;AACrB,IAAA,KAAA,CAAM,wBAAA,EAAyB;AAAA,EACjC,CAAA;AAAA;AAAA,EAGS,OAAA,GAAgB;AACvB,IAAA,IAAA,CAAK,kBAAA,EAAmB;AACxB,IAAA,IAAA,CAAK,OAAA,CAAQ,gBAAA,CAAiB,OAAA,EAAS,IAAA,CAAK,iBAAiB,IAAI,CAAA;AACjE,IAAA,IAAA,CAAK,kBAAA,EAAmB;AAAA,EAC1B;AAAA;AAAA,EAGS,UAAA,GAAmB;AAC1B,IAAA,IAAA,CAAK,OAAA,CAAQ,mBAAA,CAAoB,OAAA,EAAS,IAAA,CAAK,iBAAiB,IAAI,CAAA;AACpE,IAAA,IAAA,CAAK,mBAAmB,UAAA,EAAW;AACnC,IAAA,IAAA,CAAK,yBAAyB,KAAA,EAAM;AAAA,EACtC;AAAA;AAAA,EAGA,MAAA,GAAe;AACb,IAAA,IAAI,CAAC,IAAA,CAAK,gBAAA,IAAoB,IAAA,CAAK,qBAAA,EAAuB;AAC1D,IAAA,IAAA,CAAK,QAAA,GAAW,CAAC,IAAA,CAAK,QAAA;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,UAAU,KAAA,EAA4B;AAGpC,IAAA,IAAI,MAAM,gBAAA,EAAkB;AAC5B,IAAA,IAAI,CAAC,KAAK,gBAAA,EAAkB;AAC5B,IAAA,IAAI,KAAA,CAAM,GAAA,KAAQ,GAAA,IAAO,KAAA,CAAM,QAAQ,OAAA,EAAS;AAChD,IAAA,IAAI,KAAK,qBAAA,EAAuB;AAC9B,MAAA,KAAA,CAAM,cAAA,EAAe;AACrB,MAAA,KAAA,CAAM,wBAAA,EAAyB;AAC/B,MAAA;AAAA,IACF;AACA,IAAA,IAAI,IAAA,CAAK,mBAAmB,iBAAA,EAAmB;AAC7C,MAAA,IAAI,KAAA,CAAM,MAAA,EAAQ,KAAA,CAAM,cAAA,EAAe;AACvC,MAAA;AAAA,IACF;AACA,IAAA,KAAA,CAAM,cAAA,EAAe;AACrB,IAAA,IAAI,MAAM,MAAA,EAAQ;AAClB,IAAA,IAAA,CAAK,MAAA,EAAO;AAAA,EACd;AAAA;AAAA,EAGA,kBAAA,GAA2B;AACzB,IAAA,IAAI,CAAC,KAAK,gBAAA,EAAkB;AAC1B,MAAA,IAAA,CAAK,oBAAA,EAAqB;AAC1B,MAAA;AAAA,IACF;AACA,IAAA,IAAA,CAAK,gBAAA,CAAiB,QAAQ,QAAQ,CAAA;AACtC,IAAA,IAAA,CAAK,gBAAA,CAAiB,gBAAgB,OAAO,CAAA;AAG7C,IAAA,IAAI,EAAE,IAAA,CAAK,OAAA,YAAmB,iBAAA,CAAA,EAAoB;AAChD,MAAA,IAAA,CAAK,gBAAA,CAAiB,YAAY,GAAG,CAAA;AAAA,IACvC;AAAA,EACF;AAAA;AAAA,EAGA,gBAAA,CAAiB,MAAc,KAAA,EAAqB;AAClD,IAAA,IAAI,mBAAA,CAAoB,KAAK,OAAA,EAAS,IAAA,EAAM,KAAK,CAAA,EAAG,IAAA,CAAK,cAAA,CAAe,GAAA,CAAI,IAAI,CAAA;AAAA,EAClF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,yBAAyB,OAAA,EAAiC;AACxD,IAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,MAAA,IAAI,MAAA,CAAO,MAAA,KAAW,IAAA,CAAK,OAAA,EAAS;AAClC,QAAA,MAAM,OAAO,MAAA,CAAO,aAAA;AACpB,QAAA,IAAI,IAAA,IAAQ,IAAA,CAAK,cAAA,CAAe,GAAA,CAAI,IAAI,CAAA,EAAG;AACzC,UAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,IAAI,CAAA;AAC5C,UAAA,IAAI,UAAU,IAAA,IAAQ,IAAA,CAAK,yBAAyB,GAAA,CAAI,IAAI,MAAM,KAAA,EAAO;AACvE,YAAA,IAAA,CAAK,cAAA,CAAe,OAAO,IAAI,CAAA;AAAA,UACjC;AAAA,QACF;AAAA,MACF;AAAA,IACF;AACA,IAAA,IAAA,CAAK,yBAAyB,KAAA,EAAM;AAAA,EACtC;AAAA;AAAA,EAGA,oBAAA,GAA6B;AAC3B,IAAA,KAAA,MAAW,QAAQ,IAAA,CAAK,cAAA,EAAgB,IAAA,CAAK,OAAA,CAAQ,gBAAgB,IAAI,CAAA;AACzE,IAAA,IAAA,CAAK,eAAe,KAAA,EAAM;AAC1B,IAAA,IAAA,CAAK,yBAAyB,KAAA,EAAM;AAAA,EACtC;AAAA;AAAA,EAGA,kBAAA,GAA2B;AACzB,IAAA,IAAA,CAAK,kBAAA,CAAmB,OAAA,CAAQ,IAAA,CAAK,OAAA,EAAS;AAAA,MAC5C,UAAA,EAAY,IAAA;AAAA,MACZ,eAAA,EAAiB;AAAA,KAClB,CAAA;AACD,IAAA,IAAI,QAAA,GAAW,KAAK,OAAA,CAAQ,aAAA;AAC5B,IAAA,OAAO,QAAA,EAAU;AACf,MAAA,IAAA,CAAK,kBAAA,CAAmB,QAAQ,QAAA,EAAU;AAAA,QACxC,UAAA,EAAY,IAAA;AAAA,QACZ,eAAA,EAAiB;AAAA,OAClB,CAAA;AACD,MAAA,QAAA,GAAW,QAAA,CAAS,aAAA;AAAA,IACtB;AAAA,EACF;AAAA;AAAA,EAGA,IAAI,gBAAA,GAA4B;AAC9B,IAAA,IAAI,KAAK,OAAA,YAAmB,iBAAA,EAAmB,OAAO,IAAA,CAAK,QAAQ,IAAA,KAAS,QAAA;AAC5E,IAAA,OAAO,CAAC,iBAAA,CAAkB,IAAA,CAAK,OAAO,CAAA;AAAA,EACxC;AAAA;AAAA,EAGA,IAAI,qBAAA,GAAiC;AACnC,IAAA,IAAI,UAA8B,IAAA,CAAK,OAAA;AACvC,IAAA,OAAO,OAAA,EAAS;AACd,MAAA,IAAI,OAAA,CAAQ,YAAA,CAAa,eAAe,CAAA,KAAM,QAAQ,OAAO,IAAA;AAC7D,MAAA,OAAA,GAAU,OAAA,CAAQ,aAAA;AAAA,IACpB;AACA,IAAA,IAAI,EAAE,IAAA,CAAK,OAAA,YAAmB,iBAAA,CAAA,EAAoB,OAAO,KAAA;AACzD,IAAA,OAAO,IAAA,CAAK,OAAA,CAAQ,QAAA,IAAY,wBAAA,CAAyB,KAAK,OAAO,CAAA;AAAA,EACvE;AAAA;AAAA,EAGA,IAAI,QAAA,GAAoB;AACtB,IAAA,OAAO,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,cAAc,CAAA,KAAM,MAAA;AAAA,EACvD;AAAA;AAAA,EAGA,IAAI,SAAS,KAAA,EAAgB;AAC3B,IAAA,MAAM,SAAA,GAAY,QAAQ,MAAA,GAAS,OAAA;AACnC,IAAA,IAAA,CAAK,wBAAA,CAAyB,GAAA,CAAI,cAAA,EAAgB,SAAS,CAAA;AAC3D,IAAA,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,cAAA,EAAgB,SAAS,CAAA;AACnD,IAAA,IAAA,CAAK,QAAA,CAAS,WAAW,EAAE,MAAA,EAAQ,EAAE,OAAA,EAAS,KAAA,IAAS,CAAA;AAAA,EACzD;AACF","file":"switch_controller.js","sourcesContent":["/**\n * Adds an attribute default without displacing an authored value.\n *\n * Attribute presence — including an authored empty string — is the ownership\n * boundary. The return value lets a caller remember that it supplied the\n * default when that caller must later restore the authored state.\n *\n * @param element - Element that owns the attribute\n * @param name - Attribute name\n * @param value - Value to write only when the attribute is absent\n * @returns Whether this call added the attribute\n */\nexport function setDefaultAttribute(element: Element, name: string, value: string): boolean {\n if (element.hasAttribute(name)) return false;\n element.setAttribute(name, value);\n return true;\n}\n","/**\n * Whether a disabled `<fieldset>` ancestor actually reaches `control`.\n *\n * HTML exempts the contents of a fieldset's **first direct-child `<legend>`**, and\n * the exemption is per fieldset — a control legal in one legend can still be\n * disabled by a second, outer fieldset, so the walk continues upward.\n *\n * Exported 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 three 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","/**\n * Native interactive elements whose built-in activation must not be replaced\n * wholesale by a controller. Deliberately excludes `[tabindex]`: a generic\n * element made focusable is the supported host for several headless patterns.\n * Contenteditable is handled by {@link isInteractiveHost} because its state is\n * inherited and its keywords are ASCII-case-insensitive.\n */\nexport const INTERACTIVE_HOST_SELECTOR =\n \"button, input, select, textarea, label, a[href], area[href], summary, details, audio[controls], video[controls], iframe, object, embed\";\n\n/**\n * Whether an element owns native activation or an inherited editing surface.\n *\n * The `contenteditable` missing and invalid states inherit. Walking explicitly\n * also keeps `contenteditable=\"false\"` as a real boundary inside an editable\n * ancestor instead of treating any ancestor attribute as decisive.\n */\nexport function isInteractiveHost(element: HTMLElement): boolean {\n if (element.matches(INTERACTIVE_HOST_SELECTOR)) return true;\n\n let current: HTMLElement | null = element;\n while (current) {\n const raw = current.getAttribute(\"contenteditable\");\n if (raw !== null) {\n const value = raw.trim().toLowerCase();\n if (value === \"false\") return false;\n if (value === \"\" || value === \"true\" || value === \"plaintext-only\") return true;\n }\n current = current.parentElement;\n }\n return false;\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { setDefaultAttribute } from \"../utils/default_attribute\";\nimport { inheritsFieldsetDisabled } from \"../utils/focus_candidate\";\nimport { isInteractiveHost } from \"../utils/interactive_host\";\n\n/** Attributes whose in-place changes can alter the host or its required defaults. */\nconst OBSERVED_ATTRIBUTES = [\n \"role\",\n \"aria-checked\",\n \"tabindex\",\n \"type\",\n \"href\",\n \"contenteditable\",\n \"controls\",\n];\n\n/** Ancestor attributes whose inherited state can change the supported host. */\nconst OBSERVED_ANCESTOR_ATTRIBUTES = [\"contenteditable\"];\n\n/**\n * Headless, accessible switch (toggle) behavior.\n *\n * Markup contract (identifier: `stimeo--switch`):\n * <button type=\"button\" data-controller=\"stimeo--switch\"\n * data-action=\"stimeo--switch#toggle keydown->stimeo--switch#onKeydown\"\n * role=\"switch\" aria-checked=\"false\">…</button>\n *\n * Implements the WAI-ARIA APG **Switch** pattern. The controller element is the\n * switch itself; its on/off state is reflected solely through `aria-checked`.\n *\n * `changed` dispatches `{ checked: boolean }`.\n *\n * @remarks\n * Behavior only — the consumer owns all styling (typically keyed off the\n * `[aria-checked=\"true\"]` attribute). On a native `<button>` host, the browser\n * already synthesizes a click for Space/Enter, so {@link onKeydown} leaves the\n * initial key to the browser and suppresses repeated keydowns. Non-button hosts\n * such as `<div role=\"switch\" tabindex=\"0\">` are driven directly. Other native\n * interactive hosts stand down because their checked/navigation semantics would\n * conflict with `aria-checked` as this controller's sole source of truth.\n *\n * Behavior provided:\n * - Click (or Space/Enter) toggles `aria-checked` between `\"true\"` and `\"false\"`.\n * - A `stimeo--switch:changed` event is dispatched on every toggle so the\n * consumer can react (its `detail.checked` carries the new boolean state).\n */\nexport class SwitchController extends Controller<HTMLElement> {\n static actions = [\"onKeydown\", \"toggle\"] as const;\n static events = [\"changed\"] as const;\n\n /** Defaults this instance introduced and may therefore remove safely. */\n readonly #ownedDefaults = new Set<string>();\n /** Controller writes that must not be mistaken for authored morph changes. */\n readonly #internalAttributeValues = new Map<string, string>();\n\n readonly #attributeObserver = new MutationObserver((records) => {\n this.#releaseAuthoredDefaults(records);\n // Stop observation while restoring defaults so the writes do not schedule a\n // second, empty callback batch. Re-observe synchronously before returning.\n this.#attributeObserver.disconnect();\n this.#reconcileDefaults();\n this.#observeAttributes();\n });\n\n /** Blocks disabled pointer/native-key activation before consumer click handlers. */\n readonly #onClickCapture = (event: MouseEvent): void => {\n if (!this.#isSupportedHost || !this.#isActivationDisabled) return;\n event.preventDefault();\n event.stopImmediatePropagation();\n };\n\n /** Ensures the switch exposes a role and is keyboard-reachable. */\n override connect(): void {\n this.#reconcileDefaults();\n this.element.addEventListener(\"click\", this.#onClickCapture, true);\n this.#observeAttributes();\n }\n\n /** Releases the explicit click guard and retained-element attribute observer. */\n override disconnect(): void {\n this.element.removeEventListener(\"click\", this.#onClickCapture, true);\n this.#attributeObserver.disconnect();\n this.#internalAttributeValues.clear();\n }\n\n /** Toggles the checked state. Bound via `data-action` (click). */\n toggle(): void {\n if (!this.#isSupportedHost || this.#isActivationDisabled) return;\n this.#checked = !this.#checked;\n }\n\n /**\n * Activates the switch on Space/Enter for non-native hosts and suppresses key\n * repeat. Bound via `data-action` (keydown). A native `<button type=\"button\">`\n * owns the initial key-to-click synthesis; repeated keydowns are canceled before\n * the browser can synthesize additional clicks.\n */\n onKeydown(event: KeyboardEvent): void {\n // A descendant widget that already claimed the key must not ALSO toggle the switch —\n // composition depends on this yield.\n if (event.defaultPrevented) return;\n if (!this.#isSupportedHost) return;\n if (event.key !== \" \" && event.key !== \"Enter\") return;\n if (this.#isActivationDisabled) {\n event.preventDefault();\n event.stopImmediatePropagation();\n return;\n }\n if (this.element instanceof HTMLButtonElement) {\n if (event.repeat) event.preventDefault();\n return;\n }\n event.preventDefault();\n if (event.repeat) return;\n this.toggle();\n }\n\n /** Re-adds only missing defaults, preserving every authored attribute value. */\n #reconcileDefaults(): void {\n if (!this.#isSupportedHost) {\n this.#removeOwnedDefaults();\n return;\n }\n this.#setOwnedDefault(\"role\", \"switch\");\n this.#setOwnedDefault(\"aria-checked\", \"false\");\n // Native buttons are focusable already; generic hosts need a Tab stop for\n // the keyboard action above to be reachable.\n if (!(this.element instanceof HTMLButtonElement)) {\n this.#setOwnedDefault(\"tabindex\", \"0\");\n }\n }\n\n /** Records a newly introduced default without claiming authored markup. */\n #setOwnedDefault(name: string, value: string): void {\n if (setDefaultAttribute(this.element, name, value)) this.#ownedDefaults.add(name);\n }\n\n /**\n * Gives ownership back when a retained-element morph authors a value. Missing\n * owned attributes stay owned so reconciliation can restore them.\n */\n #releaseAuthoredDefaults(records: MutationRecord[]): void {\n for (const record of records) {\n if (record.target === this.element) {\n const name = record.attributeName;\n if (name && this.#ownedDefaults.has(name)) {\n const value = this.element.getAttribute(name);\n if (value !== null && this.#internalAttributeValues.get(name) !== value) {\n this.#ownedDefaults.delete(name);\n }\n }\n }\n }\n this.#internalAttributeValues.clear();\n }\n\n /** Removes only defaults introduced by this instance when a host becomes invalid. */\n #removeOwnedDefaults(): void {\n for (const name of this.#ownedDefaults) this.element.removeAttribute(name);\n this.#ownedDefaults.clear();\n this.#internalAttributeValues.clear();\n }\n\n /** Watches retained host attributes that Turbo can morph without reconnecting. */\n #observeAttributes(): void {\n this.#attributeObserver.observe(this.element, {\n attributes: true,\n attributeFilter: OBSERVED_ATTRIBUTES,\n });\n let ancestor = this.element.parentElement;\n while (ancestor) {\n this.#attributeObserver.observe(ancestor, {\n attributes: true,\n attributeFilter: OBSERVED_ANCESTOR_ATTRIBUTES,\n });\n ancestor = ancestor.parentElement;\n }\n }\n\n /** Whether the host has one activation model that this controller can own. */\n get #isSupportedHost(): boolean {\n if (this.element instanceof HTMLButtonElement) return this.element.type === \"button\";\n return !isInteractiveHost(this.element);\n }\n\n /** Whether ARIA or native HTML semantics make the supported switch inoperable. */\n get #isActivationDisabled(): boolean {\n let current: HTMLElement | null = this.element;\n while (current) {\n if (current.getAttribute(\"aria-disabled\") === \"true\") return true;\n current = current.parentElement;\n }\n if (!(this.element instanceof HTMLButtonElement)) return false;\n return this.element.disabled || inheritsFieldsetDisabled(this.element);\n }\n\n /** Whether the switch is currently on. */\n get #checked(): boolean {\n return this.element.getAttribute(\"aria-checked\") === \"true\";\n }\n\n /** Reflects the new state on `aria-checked` and notifies listeners. */\n set #checked(value: boolean) {\n const reflected = value ? \"true\" : \"false\";\n this.#internalAttributeValues.set(\"aria-checked\", reflected);\n this.element.setAttribute(\"aria-checked\", reflected);\n this.dispatch(\"changed\", { detail: { checked: value } });\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/default_attribute.ts","../../src/utils/focus_candidate.ts","../../src/utils/interactive_host.ts","../../src/controllers/switch_controller.ts"],"names":[],"mappings":"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","file":"switch_controller.js","sourcesContent":["/**\n * Adds an attribute default without displacing an authored value.\n *\n * Attribute presence — including an authored empty string — is the ownership\n * boundary. The return value lets a caller remember that it supplied the\n * default when that caller must later restore the authored state.\n *\n * @param element - Element that owns the attribute\n * @param name - Attribute name\n * @param value - Value to write only when the attribute is absent\n * @returns Whether this call added the attribute\n */\nexport function setDefaultAttribute(element: Element, name: string, value: string): boolean {\n if (element.hasAttribute(name)) return false;\n element.setAttribute(name, value);\n return true;\n}\n","/**\n * Whether a disabled `<fieldset>` ancestor actually reaches `control`.\n *\n * HTML exempts the contents of a fieldset's **first direct-child `<legend>`**, and\n * the exemption is per fieldset — a control legal in one legend can still be\n * disabled by a second, outer fieldset, so the walk continues upward.\n *\n * Exported 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","/**\n * Native interactive elements whose built-in activation must not be replaced\n * wholesale by a controller. Deliberately excludes `[tabindex]`: a generic\n * element made focusable is the supported host for several headless patterns.\n * Contenteditable is handled by {@link isInteractiveHost} because its state is\n * inherited and its keywords are ASCII-case-insensitive.\n */\nexport const INTERACTIVE_HOST_SELECTOR =\n \"button, input, select, textarea, label, a[href], area[href], summary, details, audio[controls], video[controls], iframe, object, embed\";\n\n/**\n * Whether an element owns native activation or an inherited editing surface.\n *\n * The `contenteditable` missing and invalid states inherit. Walking explicitly\n * also keeps `contenteditable=\"false\"` as a real boundary inside an editable\n * ancestor instead of treating any ancestor attribute as decisive.\n */\nexport function isInteractiveHost(element: HTMLElement): boolean {\n if (element.matches(INTERACTIVE_HOST_SELECTOR)) return true;\n\n let current: HTMLElement | null = element;\n while (current) {\n const raw = current.getAttribute(\"contenteditable\");\n if (raw !== null) {\n const value = raw.trim().toLowerCase();\n if (value === \"false\") return false;\n if (value === \"\" || value === \"true\" || value === \"plaintext-only\") return true;\n }\n current = current.parentElement;\n }\n return false;\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { setDefaultAttribute } from \"../utils/default_attribute\";\nimport { inheritsFieldsetDisabled } from \"../utils/focus_candidate\";\nimport { isInteractiveHost } from \"../utils/interactive_host\";\n\n/** Attributes whose in-place changes can alter the host or its required defaults. */\nconst OBSERVED_ATTRIBUTES = [\n \"role\",\n \"aria-checked\",\n \"tabindex\",\n \"type\",\n \"href\",\n \"contenteditable\",\n \"controls\",\n];\n\n/** Ancestor attributes whose inherited state can change the supported host. */\nconst OBSERVED_ANCESTOR_ATTRIBUTES = [\"contenteditable\"];\n\n/**\n * Headless, accessible switch (toggle) behavior.\n *\n * Markup contract (identifier: `stimeo--switch`):\n * <button type=\"button\" data-controller=\"stimeo--switch\"\n * data-action=\"stimeo--switch#toggle keydown->stimeo--switch#onKeydown\"\n * role=\"switch\" aria-checked=\"false\">…</button>\n *\n * Implements the WAI-ARIA APG **Switch** pattern. The controller element is the\n * switch itself; its on/off state is reflected solely through `aria-checked`.\n *\n * `changed` dispatches `{ checked: boolean }`.\n *\n * @remarks\n * Behavior only — the consumer owns all styling (typically keyed off the\n * `[aria-checked=\"true\"]` attribute). On a native `<button>` host, the browser\n * already synthesizes a click for Space/Enter, so {@link onKeydown} leaves the\n * initial key to the browser and suppresses repeated keydowns. Non-button hosts\n * such as `<div role=\"switch\" tabindex=\"0\">` are driven directly. Other native\n * interactive hosts stand down because their checked/navigation semantics would\n * conflict with `aria-checked` as this controller's sole source of truth.\n *\n * Behavior provided:\n * - Click (or Space/Enter) toggles `aria-checked` between `\"true\"` and `\"false\"`.\n * - A `stimeo--switch:changed` event is dispatched on every toggle so the\n * consumer can react (its `detail.checked` carries the new boolean state).\n */\nexport class SwitchController extends Controller<HTMLElement> {\n static actions = [\"onKeydown\", \"toggle\"] as const;\n static events = [\"changed\"] as const;\n\n /** Defaults this instance introduced and may therefore remove safely. */\n readonly #ownedDefaults = new Set<string>();\n /** Controller writes that must not be mistaken for authored morph changes. */\n readonly #internalAttributeValues = new Map<string, string>();\n\n readonly #attributeObserver = new MutationObserver((records) => {\n this.#releaseAuthoredDefaults(records);\n // Stop observation while restoring defaults so the writes do not schedule a\n // second, empty callback batch. Re-observe synchronously before returning.\n this.#attributeObserver.disconnect();\n this.#reconcileDefaults();\n this.#observeAttributes();\n });\n\n /** Blocks disabled pointer/native-key activation before consumer click handlers. */\n readonly #onClickCapture = (event: MouseEvent): void => {\n if (!this.#isSupportedHost || !this.#isActivationDisabled) return;\n event.preventDefault();\n event.stopImmediatePropagation();\n };\n\n /** Ensures the switch exposes a role and is keyboard-reachable. */\n override connect(): void {\n this.#reconcileDefaults();\n this.element.addEventListener(\"click\", this.#onClickCapture, true);\n this.#observeAttributes();\n }\n\n /** Releases the explicit click guard and retained-element attribute observer. */\n override disconnect(): void {\n this.element.removeEventListener(\"click\", this.#onClickCapture, true);\n this.#attributeObserver.disconnect();\n this.#internalAttributeValues.clear();\n }\n\n /** Toggles the checked state. Bound via `data-action` (click). */\n toggle(): void {\n if (!this.#isSupportedHost || this.#isActivationDisabled) return;\n this.#checked = !this.#checked;\n }\n\n /**\n * Activates the switch on Space/Enter for non-native hosts and suppresses key\n * repeat. Bound via `data-action` (keydown). A native `<button type=\"button\">`\n * owns the initial key-to-click synthesis; repeated keydowns are canceled before\n * the browser can synthesize additional clicks.\n */\n onKeydown(event: KeyboardEvent): void {\n // A descendant widget that already claimed the key must not ALSO toggle the switch —\n // composition depends on this yield.\n if (event.defaultPrevented) return;\n if (!this.#isSupportedHost) return;\n if (event.key !== \" \" && event.key !== \"Enter\") return;\n if (this.#isActivationDisabled) {\n event.preventDefault();\n event.stopImmediatePropagation();\n return;\n }\n if (this.element instanceof HTMLButtonElement) {\n if (event.repeat) event.preventDefault();\n return;\n }\n event.preventDefault();\n if (event.repeat) return;\n this.toggle();\n }\n\n /** Re-adds only missing defaults, preserving every authored attribute value. */\n #reconcileDefaults(): void {\n if (!this.#isSupportedHost) {\n this.#removeOwnedDefaults();\n return;\n }\n this.#setOwnedDefault(\"role\", \"switch\");\n this.#setOwnedDefault(\"aria-checked\", \"false\");\n // Native buttons are focusable already; generic hosts need a Tab stop for\n // the keyboard action above to be reachable.\n if (!(this.element instanceof HTMLButtonElement)) {\n this.#setOwnedDefault(\"tabindex\", \"0\");\n }\n }\n\n /** Records a newly introduced default without claiming authored markup. */\n #setOwnedDefault(name: string, value: string): void {\n if (setDefaultAttribute(this.element, name, value)) this.#ownedDefaults.add(name);\n }\n\n /**\n * Gives ownership back when a retained-element morph authors a value. Missing\n * owned attributes stay owned so reconciliation can restore them.\n */\n #releaseAuthoredDefaults(records: MutationRecord[]): void {\n for (const record of records) {\n if (record.target === this.element) {\n const name = record.attributeName;\n if (name && this.#ownedDefaults.has(name)) {\n const value = this.element.getAttribute(name);\n if (value !== null && this.#internalAttributeValues.get(name) !== value) {\n this.#ownedDefaults.delete(name);\n }\n }\n }\n }\n this.#internalAttributeValues.clear();\n }\n\n /** Removes only defaults introduced by this instance when a host becomes invalid. */\n #removeOwnedDefaults(): void {\n for (const name of this.#ownedDefaults) this.element.removeAttribute(name);\n this.#ownedDefaults.clear();\n this.#internalAttributeValues.clear();\n }\n\n /** Watches retained host attributes that Turbo can morph without reconnecting. */\n #observeAttributes(): void {\n this.#attributeObserver.observe(this.element, {\n attributes: true,\n attributeFilter: OBSERVED_ATTRIBUTES,\n });\n let ancestor = this.element.parentElement;\n while (ancestor) {\n this.#attributeObserver.observe(ancestor, {\n attributes: true,\n attributeFilter: OBSERVED_ANCESTOR_ATTRIBUTES,\n });\n ancestor = ancestor.parentElement;\n }\n }\n\n /** Whether the host has one activation model that this controller can own. */\n get #isSupportedHost(): boolean {\n if (this.element instanceof HTMLButtonElement) return this.element.type === \"button\";\n return !isInteractiveHost(this.element);\n }\n\n /** Whether ARIA or native HTML semantics make the supported switch inoperable. */\n get #isActivationDisabled(): boolean {\n let current: HTMLElement | null = this.element;\n while (current) {\n if (current.getAttribute(\"aria-disabled\") === \"true\") return true;\n current = current.parentElement;\n }\n if (!(this.element instanceof HTMLButtonElement)) return false;\n return this.element.disabled || inheritsFieldsetDisabled(this.element);\n }\n\n /** Whether the switch is currently on. */\n get #checked(): boolean {\n return this.element.getAttribute(\"aria-checked\") === \"true\";\n }\n\n /** Reflects the new state on `aria-checked` and notifies listeners. */\n set #checked(value: boolean) {\n const reflected = value ? \"true\" : \"false\";\n this.#internalAttributeValues.set(\"aria-checked\", reflected);\n this.element.setAttribute(\"aria-checked\", reflected);\n this.dispatch(\"changed\", { detail: { checked: value } });\n }\n}\n"]}
|
|
@@ -18,7 +18,7 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
18
18
|
* Re-measure triggers: `connect()`; `input` and `change` on the element
|
|
19
19
|
* (`change` covers programmatic writers that announce the write with a bubbling
|
|
20
20
|
* event, the convention Stimeo's own field-writing controllers follow); element
|
|
21
|
-
* size reports via the shared
|
|
21
|
+
* size reports via the shared `LayoutObserver` — width changes
|
|
22
22
|
* (re-wrapping) and the applied inline height being stripped (a Turbo morph
|
|
23
23
|
* syncing attributes); font loading settling on `document.fonts`; runtime
|
|
24
24
|
* `minRows` / `maxRows` changes; and the explicit `resize` action. A silent
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/layout_observer.ts","../../src/controllers/textarea_autosize_controller.ts"],"names":[],"mappings":"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","file":"textarea_autosize_controller.js","sourcesContent":["/**\n * Unified element-size and viewport observation for Stimeo controllers.\n *\n * Widgets whose output is measured — an overflow boundary, a masonry column count,\n * an autosized textarea — need to react both to their *own* box changing — via {@link ResizeObserver} —\n * and to the *viewport* changing — via the `window` `resize` event. Wiring those\n * two sources by hand in every controller risks leaked listeners on\n * `disconnect()`. {@link LayoutObserver} owns both behind one callback and one\n * {@link LayoutObserver.disconnect | disconnect()} that releases everything.\n *\n * Behavior only: the helper reports *that* layout changed; it never reads or\n * writes styles. Consumers decide what to recompute.\n */\n\n/** Invoked whenever an observed element or the viewport changes size. */\nexport type LayoutCallback = () => void;\n\n/** Constructs a {@link ResizeObserver}; injectable so tests stay deterministic. */\nexport type ResizeObserverFactory = (callback: ResizeObserverCallback) => ResizeObserver;\n\n/** Options for {@link LayoutObserver}. */\nexport interface LayoutObserverOptions {\n /**\n * Factory for the {@link ResizeObserver} used by {@link LayoutObserver.observe}.\n * Defaults to the global constructor; override it in tests, or to no-op in\n * environments where `ResizeObserver` is unavailable.\n */\n resizeObserverFactory?: ResizeObserverFactory;\n}\n\n/**\n * Observes element resizes and/or viewport resizes through a single callback,\n * with guaranteed teardown.\n *\n * @example\n * ```ts\n * #layout = new LayoutObserver(() => this.#reposition());\n *\n * connect() {\n * this.#layout.observe(this.panelTarget);\n * this.#layout.observeViewport();\n * }\n *\n * disconnect() {\n * this.#layout.disconnect();\n * }\n * ```\n */\nexport class LayoutObserver {\n readonly #callback: LayoutCallback;\n readonly #resizeObserverFactory: ResizeObserverFactory | null;\n #resizeObserver: ResizeObserver | null = null;\n #observingViewport = false;\n\n /** Stable bound handler so add/removeEventListener target the same reference. */\n readonly #handleViewportResize = (): void => {\n this.#callback();\n };\n\n constructor(callback: LayoutCallback, options: LayoutObserverOptions = {}) {\n this.#callback = callback;\n this.#resizeObserverFactory =\n options.resizeObserverFactory ??\n (typeof ResizeObserver === \"undefined\" ? null : (cb) => new ResizeObserver(cb));\n }\n\n /**\n * Starts observing an element's size. Repeated calls observe additional\n * elements through the same shared observer. No-ops when no\n * `ResizeObserver` implementation is available.\n */\n observe(element: Element): void {\n if (!this.#resizeObserverFactory) return;\n if (!this.#resizeObserver) {\n this.#resizeObserver = this.#resizeObserverFactory(() => {\n this.#callback();\n });\n }\n this.#resizeObserver.observe(element);\n }\n\n /** Stops observing a single element while leaving any others in place. */\n unobserve(element: Element): void {\n this.#resizeObserver?.unobserve(element);\n }\n\n /** Starts observing viewport resizes. Idempotent: the listener is added once. */\n observeViewport(): void {\n if (this.#observingViewport) return;\n this.#observingViewport = true;\n window.addEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /** Stops observing viewport resizes without affecting element observation. */\n unobserveViewport(): void {\n if (!this.#observingViewport) return;\n this.#observingViewport = false;\n window.removeEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /**\n * Releases every observation: disconnects the {@link ResizeObserver} and\n * removes the viewport listener. Safe to call multiple times. Call this from a\n * controller's `disconnect()`.\n */\n disconnect(): void {\n this.#resizeObserver?.disconnect();\n this.#resizeObserver = null;\n this.unobserveViewport();\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { LayoutObserver } from \"../utils/layout_observer\";\n\n/** Parses a CSS pixel length, defaulting to 0 for `auto` / empty / `normal`. */\nfunction px(value: string): number {\n const n = Number.parseFloat(value);\n return Number.isNaN(n) ? 0 : n;\n}\n\n/** The event surface used by `document.fonts` without requiring it in older engines. */\ninterface FontEventSource {\n addEventListener(type: string, listener: EventListener): void;\n removeEventListener(type: string, listener: EventListener): void;\n}\n\n/**\n * Headless autosize behavior for a `<textarea>`: grows the element to fit its\n * content (and clamps to `minRows` / `maxRows`, scrolling past the max). No APG\n * pattern; supports WCAG 1.4.4 by following the text as it grows.\n *\n * Markup contract (identifier: `stimeo--textarea-autosize`):\n * <textarea\n * data-controller=\"stimeo--textarea-autosize\"\n * data-stimeo--textarea-autosize-max-rows-value=\"10\"></textarea>\n *\n * Each re-measure collapses the element (`height:auto`), reads `scrollHeight`,\n * and sets an explicit pixel height clamped between `minRows` and `maxRows` (in\n * line-height units), toggling internal scrolling and the `data-at-max-rows`\n * hook at the cap. Height-only changes preserve focus and caret.\n *\n * Re-measure triggers: `connect()`; `input` and `change` on the element\n * (`change` covers programmatic writers that announce the write with a bubbling\n * event, the convention Stimeo's own field-writing controllers follow); element\n * size reports via the shared {@link LayoutObserver} — width changes\n * (re-wrapping) and the applied inline height being stripped (a Turbo morph\n * syncing attributes); font loading settling on `document.fonts`; runtime\n * `minRows` / `maxRows` changes; and the explicit `resize` action. A silent\n * `.value` assignment produces no DOM signal, so callers follow it with\n * `resize` or dispatch `input` / `change`.\n *\n * `resize` dispatches `{ height, rows }` only when the applied height differs\n * from the height this instance last applied. `connect()` adopts a height left\n * on the element by a previous connection (Turbo restore / morph), so restoring\n * an unchanged element stays silent.\n *\n * @remarks\n * Behavior only — the height is written to the element's own inline style, never\n * a CSS class. State lives on the element (no module-scope state), so `connect()`\n * re-measures after a Turbo navigation / morph. Every subscription (`input` /\n * `change` listeners, the layout observer, the font listeners) is released on\n * `disconnect()` (Turbo navigation included). Measurement is synchronous, so\n * there is no pending animation frame to tear down. The layout callback\n * re-measures only when the element's width changed or the applied inline\n * height vanished, so the controller's own height writes do not re-enter it.\n */\nexport class TextareaAutosizeController extends Controller<HTMLTextAreaElement> {\n static override values = {\n minRows: { type: Number, default: 1 },\n maxRows: { type: Number, default: 0 },\n };\n static actions = [\"resize\"] as const;\n static events = [\"resize\"] as const;\n\n declare minRowsValue: number;\n declare maxRowsValue: number;\n\n #lastHeight = -1;\n #lastWidth = -1;\n #started = false;\n #pending = false;\n #fonts: FontEventSource | null = null;\n\n /**\n * Content triggers arriving while the box is collapsed (`display: none`\n * ancestors) are deferred: `scrollHeight` is 0 there, so measuring would\n * clamp the kept height to the `minRows` floor and dispatch a bogus\n * `resize`. The next report from a rendered box flushes the deferral.\n */\n readonly #remeasure = (): void => {\n if (this.element.clientWidth === 0) {\n this.#pending = true;\n return;\n }\n this.resize();\n };\n\n /**\n * Re-measures when the content width changed (re-wrapping, including a\n * horizontal padding change under `border-box`), the applied inline height\n * was stripped (a Turbo morph syncing attributes from server HTML that has no\n * `style`), or a content trigger was deferred while the box was collapsed.\n * This instance's own height writes match none of these signals, so they\n * cannot re-enter; an authored inline height (e.g. a user dragging the native\n * resize handle) is left alone until the next content trigger. Reports from a\n * still-collapsed box are skipped (nothing is measurable there).\n */\n readonly #layout = new LayoutObserver(() => {\n if (this.element.clientWidth === 0) return;\n const width = this.#contentWidth();\n const stripped = this.element.style.height === \"\";\n if (width === this.#lastWidth && !stripped && !this.#pending) return;\n this.#lastWidth = width;\n this.resize();\n });\n\n override connect(): void {\n // Adopt a height applied by a previous connection (kept by Turbo restore /\n // morph) so re-measuring an unchanged element does not re-dispatch `resize`.\n this.#lastHeight = Math.round(px(this.element.style.height));\n this.#lastWidth = this.#contentWidth();\n this.element.addEventListener(\"input\", this.#remeasure);\n this.element.addEventListener(\"change\", this.#remeasure);\n this.#layout.observe(this.element);\n this.#bindFonts();\n this.#started = true;\n this.resize();\n }\n\n override disconnect(): void {\n this.#started = false;\n this.element.removeEventListener(\"input\", this.#remeasure);\n this.element.removeEventListener(\"change\", this.#remeasure);\n this.#layout.disconnect();\n this.#unbindFonts();\n }\n\n /** Re-clamps when application code or a Turbo morph changes `minRows`. */\n minRowsValueChanged(): void {\n if (this.#started) this.#remeasure();\n }\n\n /** Re-clamps when application code or a Turbo morph changes `maxRows`. */\n maxRowsValueChanged(): void {\n if (this.#started) this.#remeasure();\n }\n\n /** Re-measures the content and applies the clamped height. */\n resize(): void {\n this.#pending = false;\n const el = this.element;\n const style = window.getComputedStyle(el);\n const lineHeight = this.#lineHeight(style);\n const paddingV = px(style.paddingTop) + px(style.paddingBottom);\n const borderV = px(style.borderTopWidth) + px(style.borderBottomWidth);\n const borderBox = style.boxSizing === \"border-box\";\n\n // Collapse first so scrollHeight reflects the content, not the prior height.\n el.style.height = \"auto\";\n const contentHeight = Math.max(0, el.scrollHeight - paddingV);\n const rows = Math.max(1, Math.round(contentHeight / lineHeight));\n\n let targetContent = Math.max(contentHeight, this.minRowsValue * lineHeight);\n let atMax = false;\n if (this.maxRowsValue > 0) {\n const maxContent = this.maxRowsValue * lineHeight;\n if (targetContent > maxContent) {\n targetContent = maxContent;\n atMax = true;\n }\n }\n\n const boxExtra = borderBox ? paddingV + borderV : 0;\n const height = Math.round(targetContent + boxExtra);\n el.style.height = `${height}px`;\n el.style.overflowY = atMax ? \"auto\" : \"hidden\";\n\n if (atMax) {\n el.setAttribute(\"data-at-max-rows\", \"true\");\n } else {\n el.removeAttribute(\"data-at-max-rows\");\n }\n el.style.setProperty(\"--stimeo--textarea-rows\", String(rows));\n\n if (height !== this.#lastHeight) {\n this.#lastHeight = height;\n this.dispatch(\"resize\", { detail: { height, rows } });\n }\n }\n\n /** Content-box width — the wrapping input, unaffected by this instance's height writes. */\n #contentWidth(): number {\n const style = window.getComputedStyle(this.element);\n return Math.max(0, this.element.clientWidth - px(style.paddingLeft) - px(style.paddingRight));\n }\n\n /** Resolved line height, falling back to ~1.2× font-size when `normal`. */\n #lineHeight(style: CSSStyleDeclaration): number {\n const lh = px(style.lineHeight);\n if (lh > 0) return lh;\n const fontSize = px(style.fontSize);\n return fontSize > 0 ? fontSize * 1.2 : 16;\n }\n\n /** Subscribes to font completion when the current engine exposes `document.fonts`. */\n #bindFonts(): void {\n const fonts = (this.element.ownerDocument as Document & { fonts?: FontEventSource }).fonts;\n if (!fonts) return;\n this.#fonts = fonts;\n fonts.addEventListener(\"loadingdone\", this.#remeasure);\n fonts.addEventListener(\"loadingerror\", this.#remeasure);\n }\n\n /** Removes the font completion subscriptions. */\n #unbindFonts(): void {\n this.#fonts?.removeEventListener(\"loadingdone\", this.#remeasure);\n this.#fonts?.removeEventListener(\"loadingerror\", this.#remeasure);\n this.#fonts = null;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/layout_observer.ts","../../src/controllers/textarea_autosize_controller.ts"],"names":[],"mappings":"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","file":"textarea_autosize_controller.js","sourcesContent":["/**\n * Unified element-size and viewport observation for Stimeo controllers.\n *\n * Widgets whose output is measured — an overflow boundary, a masonry column count,\n * an autosized textarea — need to react both to their *own* box changing — via {@link ResizeObserver} —\n * and to the *viewport* changing — via the `window` `resize` event. Wiring those\n * two sources by hand in every controller risks leaked listeners on\n * `disconnect()`. {@link LayoutObserver} owns both behind one callback and one\n * {@link LayoutObserver.disconnect | disconnect()} that releases everything.\n *\n * Behavior only: the helper reports *that* layout changed; it never reads or\n * writes styles. Consumers decide what to recompute.\n */\n\n/** Invoked whenever an observed element or the viewport changes size. */\nexport type LayoutCallback = () => void;\n\n/** Constructs a {@link ResizeObserver}; injectable so tests stay deterministic. */\nexport type ResizeObserverFactory = (callback: ResizeObserverCallback) => ResizeObserver;\n\n/** Options for {@link LayoutObserver}. */\nexport interface LayoutObserverOptions {\n /**\n * Factory for the {@link ResizeObserver} used by {@link LayoutObserver.observe}.\n * Defaults to the global constructor; override it in tests, or to no-op in\n * environments where `ResizeObserver` is unavailable.\n */\n resizeObserverFactory?: ResizeObserverFactory;\n}\n\n/**\n * Observes element resizes and/or viewport resizes through a single callback,\n * with guaranteed teardown.\n *\n * @example\n * ```ts\n * #layout = new LayoutObserver(() => this.#reposition());\n *\n * connect() {\n * this.#layout.observe(this.panelTarget);\n * this.#layout.observeViewport();\n * }\n *\n * disconnect() {\n * this.#layout.disconnect();\n * }\n * ```\n */\nexport class LayoutObserver {\n readonly #callback: LayoutCallback;\n readonly #resizeObserverFactory: ResizeObserverFactory | null;\n #resizeObserver: ResizeObserver | null = null;\n #observingViewport = false;\n\n /** Stable bound handler so add/removeEventListener target the same reference. */\n readonly #handleViewportResize = (): void => {\n this.#callback();\n };\n\n constructor(callback: LayoutCallback, options: LayoutObserverOptions = {}) {\n this.#callback = callback;\n this.#resizeObserverFactory =\n options.resizeObserverFactory ??\n (typeof ResizeObserver === \"undefined\" ? null : (cb) => new ResizeObserver(cb));\n }\n\n /**\n * Starts observing an element's size. Repeated calls observe additional\n * elements through the same shared observer. No-ops when no\n * `ResizeObserver` implementation is available.\n */\n observe(element: Element): void {\n if (!this.#resizeObserverFactory) return;\n if (!this.#resizeObserver) {\n this.#resizeObserver = this.#resizeObserverFactory(() => {\n this.#callback();\n });\n }\n this.#resizeObserver.observe(element);\n }\n\n /** Stops observing a single element while leaving any others in place. */\n unobserve(element: Element): void {\n this.#resizeObserver?.unobserve(element);\n }\n\n /** Starts observing viewport resizes. Idempotent: the listener is added once. */\n observeViewport(): void {\n if (this.#observingViewport) return;\n this.#observingViewport = true;\n window.addEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /** Stops observing viewport resizes without affecting element observation. */\n unobserveViewport(): void {\n if (!this.#observingViewport) return;\n this.#observingViewport = false;\n window.removeEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /**\n * Releases every observation: disconnects the {@link ResizeObserver} and\n * removes the viewport listener. Safe to call multiple times. Call this from a\n * controller's `disconnect()`.\n */\n disconnect(): void {\n this.#resizeObserver?.disconnect();\n this.#resizeObserver = null;\n this.unobserveViewport();\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { LayoutObserver } from \"../utils/layout_observer\";\n\n/** Parses a CSS pixel length, defaulting to 0 for `auto` / empty / `normal`. */\nfunction px(value: string): number {\n const n = Number.parseFloat(value);\n return Number.isNaN(n) ? 0 : n;\n}\n\n/** The event surface used by `document.fonts` without requiring it in older engines. */\ninterface FontEventSource {\n addEventListener(type: string, listener: EventListener): void;\n removeEventListener(type: string, listener: EventListener): void;\n}\n\n/**\n * Headless autosize behavior for a `<textarea>`: grows the element to fit its\n * content (and clamps to `minRows` / `maxRows`, scrolling past the max). No APG\n * pattern; supports WCAG 1.4.4 by following the text as it grows.\n *\n * Markup contract (identifier: `stimeo--textarea-autosize`):\n * <textarea\n * data-controller=\"stimeo--textarea-autosize\"\n * data-stimeo--textarea-autosize-max-rows-value=\"10\"></textarea>\n *\n * Each re-measure collapses the element (`height:auto`), reads `scrollHeight`,\n * and sets an explicit pixel height clamped between `minRows` and `maxRows` (in\n * line-height units), toggling internal scrolling and the `data-at-max-rows`\n * hook at the cap. Height-only changes preserve focus and caret.\n *\n * Re-measure triggers: `connect()`; `input` and `change` on the element\n * (`change` covers programmatic writers that announce the write with a bubbling\n * event, the convention Stimeo's own field-writing controllers follow); element\n * size reports via the shared `LayoutObserver` — width changes\n * (re-wrapping) and the applied inline height being stripped (a Turbo morph\n * syncing attributes); font loading settling on `document.fonts`; runtime\n * `minRows` / `maxRows` changes; and the explicit `resize` action. A silent\n * `.value` assignment produces no DOM signal, so callers follow it with\n * `resize` or dispatch `input` / `change`.\n *\n * `resize` dispatches `{ height, rows }` only when the applied height differs\n * from the height this instance last applied. `connect()` adopts a height left\n * on the element by a previous connection (Turbo restore / morph), so restoring\n * an unchanged element stays silent.\n *\n * @remarks\n * Behavior only — the height is written to the element's own inline style, never\n * a CSS class. State lives on the element (no module-scope state), so `connect()`\n * re-measures after a Turbo navigation / morph. Every subscription (`input` /\n * `change` listeners, the layout observer, the font listeners) is released on\n * `disconnect()` (Turbo navigation included). Measurement is synchronous, so\n * there is no pending animation frame to tear down. The layout callback\n * re-measures only when the element's width changed or the applied inline\n * height vanished, so the controller's own height writes do not re-enter it.\n */\nexport class TextareaAutosizeController extends Controller<HTMLTextAreaElement> {\n static override values = {\n minRows: { type: Number, default: 1 },\n maxRows: { type: Number, default: 0 },\n };\n static actions = [\"resize\"] as const;\n static events = [\"resize\"] as const;\n\n declare minRowsValue: number;\n declare maxRowsValue: number;\n\n #lastHeight = -1;\n #lastWidth = -1;\n #started = false;\n #pending = false;\n #fonts: FontEventSource | null = null;\n\n /**\n * Content triggers arriving while the box is collapsed (`display: none`\n * ancestors) are deferred: `scrollHeight` is 0 there, so measuring would\n * clamp the kept height to the `minRows` floor and dispatch a bogus\n * `resize`. The next report from a rendered box flushes the deferral.\n */\n readonly #remeasure = (): void => {\n if (this.element.clientWidth === 0) {\n this.#pending = true;\n return;\n }\n this.resize();\n };\n\n /**\n * Re-measures when the content width changed (re-wrapping, including a\n * horizontal padding change under `border-box`), the applied inline height\n * was stripped (a Turbo morph syncing attributes from server HTML that has no\n * `style`), or a content trigger was deferred while the box was collapsed.\n * This instance's own height writes match none of these signals, so they\n * cannot re-enter; an authored inline height (e.g. a user dragging the native\n * resize handle) is left alone until the next content trigger. Reports from a\n * still-collapsed box are skipped (nothing is measurable there).\n */\n readonly #layout = new LayoutObserver(() => {\n if (this.element.clientWidth === 0) return;\n const width = this.#contentWidth();\n const stripped = this.element.style.height === \"\";\n if (width === this.#lastWidth && !stripped && !this.#pending) return;\n this.#lastWidth = width;\n this.resize();\n });\n\n override connect(): void {\n // Adopt a height applied by a previous connection (kept by Turbo restore /\n // morph) so re-measuring an unchanged element does not re-dispatch `resize`.\n this.#lastHeight = Math.round(px(this.element.style.height));\n this.#lastWidth = this.#contentWidth();\n this.element.addEventListener(\"input\", this.#remeasure);\n this.element.addEventListener(\"change\", this.#remeasure);\n this.#layout.observe(this.element);\n this.#bindFonts();\n this.#started = true;\n this.resize();\n }\n\n override disconnect(): void {\n this.#started = false;\n this.element.removeEventListener(\"input\", this.#remeasure);\n this.element.removeEventListener(\"change\", this.#remeasure);\n this.#layout.disconnect();\n this.#unbindFonts();\n }\n\n /** Re-clamps when application code or a Turbo morph changes `minRows`. */\n minRowsValueChanged(): void {\n if (this.#started) this.#remeasure();\n }\n\n /** Re-clamps when application code or a Turbo morph changes `maxRows`. */\n maxRowsValueChanged(): void {\n if (this.#started) this.#remeasure();\n }\n\n /** Re-measures the content and applies the clamped height. */\n resize(): void {\n this.#pending = false;\n const el = this.element;\n const style = window.getComputedStyle(el);\n const lineHeight = this.#lineHeight(style);\n const paddingV = px(style.paddingTop) + px(style.paddingBottom);\n const borderV = px(style.borderTopWidth) + px(style.borderBottomWidth);\n const borderBox = style.boxSizing === \"border-box\";\n\n // Collapse first so scrollHeight reflects the content, not the prior height.\n el.style.height = \"auto\";\n const contentHeight = Math.max(0, el.scrollHeight - paddingV);\n const rows = Math.max(1, Math.round(contentHeight / lineHeight));\n\n let targetContent = Math.max(contentHeight, this.minRowsValue * lineHeight);\n let atMax = false;\n if (this.maxRowsValue > 0) {\n const maxContent = this.maxRowsValue * lineHeight;\n if (targetContent > maxContent) {\n targetContent = maxContent;\n atMax = true;\n }\n }\n\n const boxExtra = borderBox ? paddingV + borderV : 0;\n const height = Math.round(targetContent + boxExtra);\n el.style.height = `${height}px`;\n el.style.overflowY = atMax ? \"auto\" : \"hidden\";\n\n if (atMax) {\n el.setAttribute(\"data-at-max-rows\", \"true\");\n } else {\n el.removeAttribute(\"data-at-max-rows\");\n }\n el.style.setProperty(\"--stimeo--textarea-rows\", String(rows));\n\n if (height !== this.#lastHeight) {\n this.#lastHeight = height;\n this.dispatch(\"resize\", { detail: { height, rows } });\n }\n }\n\n /** Content-box width — the wrapping input, unaffected by this instance's height writes. */\n #contentWidth(): number {\n const style = window.getComputedStyle(this.element);\n return Math.max(0, this.element.clientWidth - px(style.paddingLeft) - px(style.paddingRight));\n }\n\n /** Resolved line height, falling back to ~1.2× font-size when `normal`. */\n #lineHeight(style: CSSStyleDeclaration): number {\n const lh = px(style.lineHeight);\n if (lh > 0) return lh;\n const fontSize = px(style.fontSize);\n return fontSize > 0 ? fontSize * 1.2 : 16;\n }\n\n /** Subscribes to font completion when the current engine exposes `document.fonts`. */\n #bindFonts(): void {\n const fonts = (this.element.ownerDocument as Document & { fonts?: FontEventSource }).fonts;\n if (!fonts) return;\n this.#fonts = fonts;\n fonts.addEventListener(\"loadingdone\", this.#remeasure);\n fonts.addEventListener(\"loadingerror\", this.#remeasure);\n }\n\n /** Removes the font completion subscriptions. */\n #unbindFonts(): void {\n this.#fonts?.removeEventListener(\"loadingdone\", this.#remeasure);\n this.#fonts?.removeEventListener(\"loadingerror\", this.#remeasure);\n this.#fonts = null;\n }\n}\n"]}
|
|
@@ -35,7 +35,8 @@ type ThemeMode = "light" | "dark" | "system";
|
|
|
35
35
|
* on the root. It applies `data-theme` (the *resolved* light/dark) and a matching
|
|
36
36
|
* `color-scheme` to the `target` element (`<html>` by default), keeps the radiogroup
|
|
37
37
|
* `aria-checked` + roving tabindex (APG radio) or the single button's `aria-pressed`
|
|
38
|
-
* in sync
|
|
38
|
+
* in sync. Arrow/Home/End move focus across the radiogroup as the APG radio pattern
|
|
39
|
+
* requires; applying a theme never moves focus. The `prefers-color-scheme` listener is attached on
|
|
39
40
|
* `connect()` and removed on `disconnect()` (Turbo included). FOUC avoidance for the
|
|
40
41
|
* very first paint is an inline `<head>` snippet, not this controller.
|
|
41
42
|
*
|
|
@@ -47,7 +48,7 @@ type ThemeMode = "light" | "dark" | "system";
|
|
|
47
48
|
* and none at all when the resolved mode matches no option, where the first one
|
|
48
49
|
* keeps the Tab stop.
|
|
49
50
|
*
|
|
50
|
-
* The radiogroup stays one Tab stop through
|
|
51
|
+
* The radiogroup stays one Tab stop through `RovingTabindex`, re-derived
|
|
51
52
|
* whenever an option enters or leaves, so a set rendered after connect or swapped
|
|
52
53
|
* by a Turbo morph carries the same single stop as any other.
|
|
53
54
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/logical_scroll.ts","../../src/utils/arrow_step.ts","../../src/utils/roving_tabindex.ts","../../src/utils/safe_storage.ts","../../src/controllers/theme_controller.ts"],"names":[],"mappings":"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","file":"theme_controller.js","sourcesContent":["/** Normalized scroll position and maximum distance on one logical axis. */\nexport interface LogicalScrollMetrics {\n position: number;\n max: number;\n}\n\n/**\n * Whether horizontal scrolling on `element` follows right-to-left inline flow.\n *\n * Resolved from the **computed** `direction`, so the authoring contract is the\n * usual `dir=\"rtl\"` (or a stylesheet) on the element or any ancestor.\n *\n * Scope: horizontal writing modes. A vertical writing mode (`writing-mode:\n * vertical-rl`) also inverts the horizontal axis, which this check does not\n * model — vertical writing modes are out of scope for the scroll utilities\n * (their consumers describe axes as horizontal/vertical, not inline/block).\n */\nexport function isRtl(element: Element): boolean {\n return window.getComputedStyle(element).direction === \"rtl\";\n}\n\n/**\n * Returns scroll distance from the logical start edge.\n *\n * CSSOM View exposes standards-mode RTL horizontal offsets as `0` at the inline\n * start (right) and increasingly negative values toward the inline end (left).\n * The normalized position is always clamped to `[0, max]`, which also absorbs\n * Safari's elastic overscroll values.\n */\nexport function logicalScrollMetrics(\n element: HTMLElement,\n horizontal: boolean,\n): LogicalScrollMetrics {\n const max = Math.max(\n 0,\n horizontal\n ? element.scrollWidth - element.clientWidth\n : element.scrollHeight - element.clientHeight,\n );\n const raw = horizontal ? element.scrollLeft : element.scrollTop;\n const position = horizontal && isRtl(element) ? -raw : raw;\n return { position: Math.min(max, Math.max(0, position)), max };\n}\n\n/**\n * Converts a logical start/end delta to the physical value accepted by\n * `Element.scrollBy`.\n */\nexport function physicalScrollDelta(\n element: HTMLElement,\n horizontal: boolean,\n logicalDelta: number,\n): number {\n return horizontal && isRtl(element) ? -logicalDelta : logicalDelta;\n}\n","import { isRtl } from \"./logical_scroll\";\n\n/**\n * Turns an arrow key into a **logical** step: `+1` for \"next\", `-1` for\n * \"previous\", `0` when the key names neither.\n *\n * APG defines the horizontal pair as *next / previous* and says a vertical\n * arrangement swaps in Down/Up for the same meaning — so the pair is one axis's\n * spelling of an order, and the order reverses with the writing direction. Only\n * the horizontal pair reverses. Down/Up name an axis the writing direction does\n * not mirror, and returning them unchanged is the point: many controllers fold\n * both pairs into one branch, where swapping the branches under RTL would flip\n * the vertical axis too — a bug that reads as \"the arrows work\" until someone\n * presses Down.\n *\n * **Direction is read from the element the caller passes, which should be the\n * container that lays the items out** — not the focused child. A child may carry\n * its own `dir` (an LTR input inside an RTL form is ordinary authoring), and\n * probing per handler makes two handlers disagree at the boundary between them.\n *\n * This decides direction only. Whether the axis is even active (an\n * `orientation=\"horizontal\"` widget ignoring Down/Up), how far the step lands,\n * and what wrapping does all stay with the caller.\n *\n * **It encodes the list-order convention: `ArrowDown` is *next*.** Widgets that\n * pair the arrows by *value* instead — `ArrowUp` meaning \"more\", as a rating or a\n * slider does — must not use this, or their vertical axis inverts. Reverse the\n * horizontal pair on its own there.\n *\n * @example\n * ```ts\n * const step = logicalArrowStep(event.key, this.element);\n * if (step === 0) return;\n * this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n * ```\n */\nexport function logicalArrowStep(key: string, element: Element): 1 | -1 | 0 {\n if (key === \"ArrowDown\") return 1;\n if (key === \"ArrowUp\") return -1;\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = isRtl(element) ? \"ArrowLeft\" : \"ArrowRight\";\n return key === forward ? 1 : -1;\n}\n\n/**\n * Rewrites `key` so an existing LTR-shaped branch keeps working under RTL:\n * `ArrowRight` and `ArrowLeft` trade places, everything else passes through.\n *\n * The alternative — negating a delta — silently breaks handlers whose two\n * horizontal branches are **not mirror images**. A grid that clamps one edge but\n * not the other, or a segmented field guarding `index > 0` on one side and\n * `index < length - 1` on the other, ends up applying the wrong guard to the\n * wrong direction. Swapping the key leaves each branch, guards and all, exactly\n * where its author put it.\n *\n * Same rule as {@link logicalArrowStep} about which element to read: pass the\n * container that lays the items out, not the focused child.\n *\n * @example\n * ```ts\n * switch (logicalArrowKey(event.key, this.element)) {\n * case \"ArrowLeft\": // \"previous\" — whatever direction that is on screen\n * ```\n */\nexport function logicalArrowKey(key: string, element: Element): string {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return key;\n if (!isRtl(element)) return key;\n return key === \"ArrowRight\" ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\n/** Modifiers a widget may claim on an arrow key, named for the `allow` list. */\nexport type ArrowModifier = \"alt\" | \"ctrl\" | \"meta\" | \"shift\";\n\n/**\n * True when an arrow key arrived carrying a modifier the widget must leave to\n * the browser: return without calling `preventDefault()` and without moving any\n * state.\n *\n * A bare arrow belongs to the widget; a chorded one usually does not.\n * `Alt`/`Meta` plus a horizontal arrow is history back/forward on every desktop\n * browser, and a widget that swallows it makes the shortcut work or not\n * depending on where focus happens to sit — a coin-flip the user cannot see.\n *\n * `allow` is for the combinations APG assigns to a pattern **and the widget\n * actually implements** — today only Combobox's optional `Alt+Down`/`Alt+Up`.\n * Listing one the widget does not implement defeats the point: the chord then\n * runs the plain-arrow branch, which is exactly what this guard exists to stop.\n * Non-arrow keys return `false`, so chorded letters and\n * `Control+Home`/`Control+End` are untouched.\n *\n * @example\n * ```ts\n * if (isReservedArrowChord(event)) return;\n * ```\n */\nexport function isReservedArrowChord(\n event: KeyboardEvent,\n allow: readonly ArrowModifier[] = [],\n): boolean {\n if (!event.key.startsWith(\"Arrow\")) return false;\n return (\n (event.altKey && !allow.includes(\"alt\")) ||\n (event.ctrlKey && !allow.includes(\"ctrl\")) ||\n (event.metaKey && !allow.includes(\"meta\")) ||\n (event.shiftKey && !allow.includes(\"shift\"))\n );\n}\n\n/**\n * True when a press arrived carrying any modifier, for the keys the browser and\n * the OS own outright: return without calling `preventDefault()` and without\n * moving any state.\n *\n * `Control+Home` and `Control+End` jump the document to its ends, and a widget\n * that swallows them makes the shortcut work or not depending on where focus\n * happens to sit. {@link isReservedArrowChord} answers the same question for the\n * arrows, but returns `false` for every other key so that this one can decide.\n *\n * There is no `allow` list here on purpose. APG assigns no modifier chord to\n * `Home`/`End`, so a widget that wanted one would be claiming a combination the\n * pattern never gave it.\n *\n * @example\n * ```ts\n * case \"Home\":\n * case \"End\":\n * if (hasModifierChord(event)) return;\n * ```\n */\nexport function hasModifierChord(event: KeyboardEvent): boolean {\n return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;\n}\n","/**\n * Low-level roving-tabindex primitive shared by composite-widget controllers.\n *\n * The APG roving-tabindex pattern keeps a composite widget a single Tab stop:\n * exactly one item is in the Tab sequence (`tabindex=\"0\"`) while the rest are\n * removed from it (`tabindex=\"-1\"`), and the arrow keys move both DOM focus and\n * that single tabbable position together. {@link RovingTabindex} owns *only* that\n * mechanical bookkeeping — \"which one item is tabbable, and move focus there\".\n *\n * It is intentionally **policy-free**. Orientation, wrapping vs. clamping,\n * selection-follows-focus, typeahead, and `Home`/`End` semantics differ per APG\n * pattern (Radio Group, Toolbar, Rating, …); folding them into one helper would\n * flatten those widgets to a lowest common denominator and lose each pattern's\n * correctness. Those decisions therefore stay in each controller, which calls\n * {@link RovingTabindex.setActive} with an index it computed itself (optionally\n * via the pure {@link rovingMove} helper).\n *\n * @remarks\n * Items are read lazily through a getter so a controller can add or remove\n * targets (Stimulus re-scans the DOM) without re-wiring this helper. A caller\n * that already captured one event-scoped snapshot may pass it to\n * {@link RovingTabindex.setActive}, avoiding a second live-target scan while\n * preserving one coherent collection for the whole operation.\n */\nexport class RovingTabindex {\n /** Returns the current ordered item elements; called on every operation. */\n readonly #getItems: () => HTMLElement[];\n\n /**\n * @param getItems - Returns the current ordered item elements. Called on every\n * operation so the live target list is always used.\n */\n constructor(getItems: () => HTMLElement[]) {\n this.#getItems = getItems;\n }\n\n /** Index of the currently tabbable item (`tabindex=\"0\"`), or `-1` if none. */\n get activeIndex(): number {\n return this.#getItems().findIndex((item) => item.tabIndex === 0);\n }\n\n /**\n * Makes exactly the item at `index` tabbable (`tabindex=\"0\"`) and removes every\n * other item from the Tab sequence (`tabindex=\"-1\"`). An out-of-range `index`\n * (e.g. `-1`) leaves all items at `-1`, which a controller can use to express\n * \"nothing is currently tabbable\".\n *\n * @param index - Position of the item to make tabbable.\n * @param options - Pass `{ focus: true }` to also move DOM focus to that item,\n * and `items` to reuse an event-scoped collection snapshot.\n */\n setActive(\n index: number,\n options: { focus?: boolean; items?: readonly HTMLElement[] } = {},\n ): void {\n const { focus = false } = options;\n const items = options.items ?? this.#getItems();\n items.forEach((item, i) => {\n item.tabIndex = i === index ? 0 : -1;\n });\n if (focus) items[index]?.focus();\n }\n}\n\n/** Edge behavior for {@link rovingMove}: cycle past the ends, or stop at them. */\nexport type RovingWrap = \"wrap\" | \"clamp\";\n\n/**\n * Pure helper that resolves the target index for a one-step directional move.\n *\n * Keyboard/orientation mapping stays in the caller: it decides that a key means\n * `delta` `+1` (next) or `-1` (previous) and whether the widget should `\"wrap\"`\n * (Radio Group, Toolbar) or `\"clamp\"` at the ends.\n *\n * @param current - The index focus is moving from.\n * @param length - Number of items in the set.\n * @param delta - `+1` to move to the next item, `-1` for the previous.\n * @param wrap - `\"wrap\"` cycles around the ends; `\"clamp\"` stops at them.\n * @returns The resolved index, or `-1` when there are no items.\n */\nexport function rovingMove(\n current: number,\n length: number,\n delta: number,\n wrap: RovingWrap,\n): number {\n if (length === 0) return -1;\n const next = current + delta;\n if (wrap === \"wrap\") return (next + length) % length;\n return Math.min(length - 1, Math.max(0, next));\n}\n","/** A successful guarded storage operation. */\nexport interface StorageSuccess<T> {\n readonly ok: true;\n readonly value: T;\n}\n\n/** A guarded storage operation rejected by the browser. */\nexport interface StorageFailure {\n readonly ok: false;\n readonly error: unknown;\n}\n\n/** Result of accessing browser storage without allowing an exception to escape. */\nexport type StorageResult<T> = StorageSuccess<T> | StorageFailure;\n\n/**\n * Guarded `localStorage` access shared by controllers that persist client state.\n *\n * `window.localStorage` can throw while resolving the storage object or while\n * reading, writing, or removing a key. The result keeps an unavailable store\n * distinct from an available store whose key is unset, so callers can expose an\n * accurate success or failure state without duplicating exception guards.\n */\n\n/**\n * Reads `key` from `localStorage`.\n *\n * @param key - The storage key to read.\n * @returns A successful result containing the stored string or `null`, or a\n * failure result when storage is unavailable.\n */\nexport function readLocalStorage(key: string): StorageResult<string | null> {\n try {\n return { ok: true, value: window.localStorage.getItem(key) };\n } catch (error: unknown) {\n return { ok: false, error };\n }\n}\n\n/**\n * Writes `value` under `key` in `localStorage`.\n *\n * @param key - The storage key to write.\n * @param value - The string to store.\n * @returns Success only after `setItem` completes; otherwise the browser error.\n */\nexport function writeLocalStorage(key: string, value: string): StorageResult<void> {\n try {\n window.localStorage.setItem(key, value);\n return { ok: true, value: undefined };\n } catch (error: unknown) {\n return { ok: false, error };\n }\n}\n\n/**\n * Removes `key` from `localStorage`.\n *\n * @param key - The storage key to remove.\n * @returns Success only after `removeItem` completes; otherwise the browser error.\n */\nexport function removeLocalStorage(key: string): StorageResult<void> {\n try {\n window.localStorage.removeItem(key);\n return { ok: true, value: undefined };\n } catch (error: unknown) {\n return { ok: false, error };\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { hasModifierChord, isReservedArrowChord, logicalArrowStep } from \"../utils/arrow_step\";\nimport { RovingTabindex, rovingMove } from \"../utils/roving_tabindex\";\nimport { readLocalStorage, writeLocalStorage } from \"../utils/safe_storage\";\n\n/** The three selectable modes; `system` follows the OS `prefers-color-scheme`. */\ntype ThemeMode = \"light\" | \"dark\" | \"system\";\n/** The two effective (resolved) themes applied to the root. */\ntype ResolvedTheme = \"light\" | \"dark\";\n\nconst MODES: readonly ThemeMode[] = [\"light\", \"dark\", \"system\"];\nconst isMode = (value: unknown): value is ThemeMode =>\n typeof value === \"string\" && (MODES as readonly string[]).includes(value);\n\n/** The mode a declaration falls back to when it cannot be read as one. */\nconst DEFAULT_MODE: ThemeMode = \"system\";\n/** The state-hook target a declaration falls back to when it cannot be parsed. */\nconst DEFAULT_TARGET = \"html\";\n\n/**\n * Headless **theme / color-scheme toggle** — persists a light/dark/system choice to\n * `localStorage`, follows the OS setting while in `system`, and reflects the\n * effective theme onto the root for the consumer's CSS. Ships no colors; only state\n * hooks.\n *\n * Two markup contracts (identifier: `stimeo--theme`):\n *\n * Canonical 3-value radiogroup (use this when `system` is offered):\n * <div data-controller=\"stimeo--theme\" data-stimeo--theme-mode-value=\"system\"\n * role=\"radiogroup\" aria-label=\"Theme\">\n * <button data-stimeo--theme-target=\"option\" role=\"radio\"\n * data-action=\"click->stimeo--theme#set\"\n * data-value=\"light\">Light</button>\n * <button data-stimeo--theme-target=\"option\" role=\"radio\"\n * data-action=\"click->stimeo--theme#set\"\n * data-value=\"dark\">Dark</button>\n * <button data-stimeo--theme-target=\"option\" role=\"radio\"\n * data-action=\"click->stimeo--theme#set\"\n * data-value=\"system\">System</button>\n * </div>\n *\n * Auxiliary 2-value toggle (light↔dark only — `system` is not representable):\n * <button data-controller=\"stimeo--theme\" data-action=\"click->stimeo--theme#toggle\"\n * aria-pressed=\"false\">Dark mode</button>\n *\n * `change` dispatches `{ mode, resolved }`, and only when one of the two moved.\n *\n * @remarks\n * Behavior only — the actual palette is the consumer's CSS keyed off `data-theme`\n * on the root. It applies `data-theme` (the *resolved* light/dark) and a matching\n * `color-scheme` to the `target` element (`<html>` by default), keeps the radiogroup\n * `aria-checked` + roving tabindex (APG radio) or the single button's `aria-pressed`\n * in sync, and never moves focus. The `prefers-color-scheme` listener is attached on\n * `connect()` and removed on `disconnect()` (Turbo included). FOUC avoidance for the\n * very first paint is an inline `<head>` snippet, not this controller.\n *\n * Every declaration is validated where it enters, and an unreadable one falls back\n * to that Value's default rather than taking the widget with it: a `mode` outside\n * the three modes reads as `system`, and a `target` that is not a parsable selector\n * reads as `html`. An option whose `data-value` is outside the three can be focused\n * but never becomes the selection, so no more than one option is ever checked —\n * and none at all when the resolved mode matches no option, where the first one\n * keeps the Tab stop.\n *\n * The radiogroup stays one Tab stop through {@link RovingTabindex}, re-derived\n * whenever an option enters or leaves, so a set rendered after connect or swapped\n * by a Turbo morph carries the same single stop as any other.\n */\nexport class ThemeController extends Controller<HTMLElement> {\n static override targets = [\"option\"];\n static override values = {\n mode: { type: String, default: DEFAULT_MODE },\n storageKey: { type: String, default: \"stimeo-theme\" },\n target: { type: String, default: DEFAULT_TARGET },\n };\n static actions = [\"set\", \"toggle\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly optionTargets: HTMLElement[];\n declare readonly hasOptionTarget: boolean;\n\n declare modeValue: string;\n declare storageKeyValue: string;\n declare targetValue: string;\n\n /** The OS dark-mode query, watched so `system` tracks live changes. */\n #media: MediaQueryList | null = null;\n\n /** Gate for the target callbacks, which Stimulus runs before `connect()`. */\n #connected = false;\n\n /** The `target` declaration after validation; the default when unparsable. */\n #targetSelector = DEFAULT_TARGET;\n\n /** Owns the single Tab stop across the option set (APG radiogroup). */\n readonly #roving = new RovingTabindex(() => this.optionTargets);\n\n /**\n * The pair last reported, so a move can be told from a repeat. Neither side is\n * readable after the fact — assigning the Value updates the mode before any\n * comparison, and the OS query has already flipped by the time it notifies —\n * so what was reported has to be kept rather than recomputed.\n */\n #published: { mode: ThemeMode; resolved: ResolvedTheme } = {\n mode: DEFAULT_MODE,\n resolved: \"light\",\n };\n\n /** Re-resolves while in `system` mode when the OS preference flips. */\n readonly #onMediaChange = (): void => {\n // Only `system` follows the OS; an explicit mode already decided the answer.\n if (this.#mode !== \"system\") return;\n this.#commit();\n };\n\n /** Arrow/Home/End navigation for the radiogroup (APG radio pattern). */\n readonly #onKeydown = (event: KeyboardEvent): void => {\n // A descendant widget that already claimed the key (a grabbed drag handle, a\n // nested menu) must not ALSO act on it — composition depends on this yield.\n if (event.defaultPrevented) return;\n if (isReservedArrowChord(event)) return;\n // Resolved once per keydown: every `optionTargets` access re-queries the scope.\n const options = this.optionTargets;\n const target = event.target as HTMLElement | null;\n const current = options.indexOf(target as HTMLElement);\n if (current === -1) return;\n\n let next = current;\n // Logical, not physical. The helper reverses only the horizontal pair, so\n // folding Down/Up into the same branch stays correct.\n const step = logicalArrowStep(event.key, this.element);\n switch (event.key) {\n case \"ArrowDown\":\n case \"ArrowRight\":\n case \"ArrowUp\":\n case \"ArrowLeft\":\n next = rovingMove(current, options.length, step, \"wrap\");\n break;\n case \"Home\":\n case \"End\":\n // Control+Home jumps the document, not the widget.\n if (hasModifierChord(event)) return;\n next = event.key === \"Home\" ? 0 : options.length - 1;\n break;\n default:\n return;\n }\n event.preventDefault();\n const option = options[next];\n if (!option) return;\n option.focus();\n const mode = this.#optionMode(option);\n // An option outside the three modes can hold focus, but the selection does\n // not follow it there.\n if (mode) this.#setMode(mode);\n };\n\n override connect(): void {\n const stored = this.#readStored();\n if (stored) this.modeValue = stored;\n\n this.#media = window.matchMedia?.(\"(prefers-color-scheme: dark)\") ?? null;\n this.#media?.addEventListener(\"change\", this.#onMediaChange);\n this.element.addEventListener(\"keydown\", this.#onKeydown);\n\n this.#connected = true;\n this.#applyTheme();\n this.#syncControls();\n // Connecting is not a change: seed the baseline instead of announcing one.\n this.#published = this.#current;\n }\n\n override disconnect(): void {\n this.#connected = false;\n this.#media?.removeEventListener(\"change\", this.#onMediaChange);\n this.element.removeEventListener(\"keydown\", this.#onKeydown);\n }\n\n /** Validates the `target` declaration once, so the render path never parses. */\n targetValueChanged(): void {\n const selector = this.targetValue;\n if (selector.length > 0) {\n try {\n this.element.matches(selector);\n this.#targetSelector = selector;\n return;\n } catch {\n // Unparsable selector: fall through to the default below.\n }\n }\n this.#targetSelector = DEFAULT_TARGET;\n }\n\n /** Re-derives the single Tab stop and ARIA for an option set that changed. */\n optionTargetConnected(): void {\n if (this.#connected) this.#syncControls();\n }\n\n /** Re-derives them again when an option leaves, so a Tab stop always remains. */\n optionTargetDisconnected(): void {\n if (this.#connected) this.#syncControls();\n }\n\n /**\n * Selects the mode the activated option declares.\n *\n * Read through {@link ThemeController.#optionMode}, the same lane that decides\n * which option is checked, so the two can never disagree about what an option\n * declares.\n */\n set(event: Event): void {\n const option = event.currentTarget;\n if (!(option instanceof HTMLElement)) return;\n const mode = this.#optionMode(option);\n if (mode) this.#setMode(mode);\n }\n\n /** Toggles light↔dark for the 2-value single-button contract. */\n toggle(): void {\n this.#setMode(this.#resolved() === \"dark\" ? \"light\" : \"dark\");\n }\n\n /** Central mode change: persist, apply to the root, resync controls, announce. */\n #setMode(mode: ThemeMode): void {\n this.modeValue = mode;\n this.#writeStored(mode);\n this.#commit();\n }\n\n /**\n * Applies the current mode and reports it, but reports only a real move: the\n * event means \"the selection or the effective theme moved\", so re-choosing the\n * option already chosen is not one.\n */\n #commit(): void {\n this.#applyTheme();\n this.#syncControls();\n const next = this.#current;\n const last = this.#published;\n this.#published = next;\n if (last.mode !== next.mode || last.resolved !== next.resolved) {\n // A copy: the baseline has to survive a listener that writes to what it\n // was handed, or the next unchanged operation reads as a change.\n this.dispatch(\"change\", { detail: { ...next } });\n }\n }\n\n /** The pair the `change` detail carries, read from current state. */\n get #current(): { mode: ThemeMode; resolved: ResolvedTheme } {\n return { mode: this.#mode, resolved: this.#resolved() };\n }\n\n /** Writes `data-theme` + `color-scheme` (the resolved theme) onto the target. */\n #applyTheme(): void {\n const root = this.#targetElement();\n if (!root) return;\n const resolved = this.#resolved();\n root.setAttribute(\"data-theme\", resolved);\n root.style.setProperty(\"color-scheme\", resolved);\n }\n\n /** Keeps the radiogroup (aria-checked + roving tabindex) or toggle (aria-pressed) in sync. */\n #syncControls(): void {\n const options = this.optionTargets;\n if (options.length > 0) {\n const mode = this.#mode;\n let selected = -1;\n options.forEach((option, index) => {\n const isSelected = this.#optionMode(option) === mode;\n option.setAttribute(\"aria-checked\", String(isSelected));\n if (isSelected && selected === -1) selected = index;\n });\n // APG: a radiogroup with no selection keeps its first radio tabbable.\n this.#roving.setActive(selected === -1 ? 0 : selected, { items: options });\n return;\n }\n // `aria-pressed` belongs to the 2-value contract, where the controller sits on\n // the button itself. A radiogroup whose options have not rendered yet is not\n // that, and must not be told it is a toggle.\n if (this.#isToggleButton) {\n this.element.setAttribute(\"aria-pressed\", String(this.#resolved() === \"dark\"));\n }\n }\n\n /** Whether the controller element is the button of the 2-value contract. */\n get #isToggleButton(): boolean {\n return this.element.tagName === \"BUTTON\" || this.element.getAttribute(\"role\") === \"button\";\n }\n\n /** The selected mode after validation; an unreadable declaration is the default. */\n get #mode(): ThemeMode {\n return isMode(this.modeValue) ? this.modeValue : DEFAULT_MODE;\n }\n\n /** The effective theme: the OS preference when `system`, else the mode itself. */\n #resolved(): ResolvedTheme {\n const mode = this.#mode;\n if (mode === \"dark\") return \"dark\";\n if (mode === \"light\") return \"light\";\n return this.#media?.matches ? \"dark\" : \"light\";\n }\n\n /** An option's mode from its `data-value`, or `null` when that is not one of the three. */\n #optionMode(option: HTMLElement): ThemeMode | null {\n const mode = option.getAttribute(\"data-value\");\n return isMode(mode) ? mode : null;\n }\n\n /** Resolves the state-hook target (`<html>` by default). */\n #targetElement(): HTMLElement | null {\n const selector = this.#targetSelector;\n if (selector === DEFAULT_TARGET || selector === \":root\") return document.documentElement;\n return document.querySelector<HTMLElement>(selector);\n }\n\n /** Reads a persisted, validated mode from `localStorage` (null when absent/blocked). */\n #readStored(): ThemeMode | null {\n const result = readLocalStorage(this.storageKeyValue);\n return result.ok && isMode(result.value) ? result.value : null;\n }\n\n /** Persists the mode, swallowing storage errors (private mode / quota). */\n #writeStored(mode: ThemeMode): void {\n writeLocalStorage(this.storageKeyValue, mode);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/logical_scroll.ts","../../src/utils/arrow_step.ts","../../src/utils/roving_tabindex.ts","../../src/utils/safe_storage.ts","../../src/controllers/theme_controller.ts"],"names":[],"mappings":"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","file":"theme_controller.js","sourcesContent":["/** Normalized scroll position and maximum distance on one logical axis. */\nexport interface LogicalScrollMetrics {\n position: number;\n max: number;\n}\n\n/**\n * Whether horizontal scrolling on `element` follows right-to-left inline flow.\n *\n * Resolved from the **computed** `direction`, so the authoring contract is the\n * usual `dir=\"rtl\"` (or a stylesheet) on the element or any ancestor.\n *\n * Scope: horizontal writing modes. A vertical writing mode (`writing-mode:\n * vertical-rl`) also inverts the horizontal axis, which this check does not\n * model — vertical writing modes are out of scope for the scroll utilities\n * (their consumers describe axes as horizontal/vertical, not inline/block).\n */\nexport function isRtl(element: Element): boolean {\n return window.getComputedStyle(element).direction === \"rtl\";\n}\n\n/**\n * Returns scroll distance from the logical start edge.\n *\n * CSSOM View exposes standards-mode RTL horizontal offsets as `0` at the inline\n * start (right) and increasingly negative values toward the inline end (left).\n * The normalized position is always clamped to `[0, max]`, which also absorbs\n * Safari's elastic overscroll values.\n */\nexport function logicalScrollMetrics(\n element: HTMLElement,\n horizontal: boolean,\n): LogicalScrollMetrics {\n const max = Math.max(\n 0,\n horizontal\n ? element.scrollWidth - element.clientWidth\n : element.scrollHeight - element.clientHeight,\n );\n const raw = horizontal ? element.scrollLeft : element.scrollTop;\n const position = horizontal && isRtl(element) ? -raw : raw;\n return { position: Math.min(max, Math.max(0, position)), max };\n}\n\n/**\n * Converts a logical start/end delta to the physical value accepted by\n * `Element.scrollBy`.\n */\nexport function physicalScrollDelta(\n element: HTMLElement,\n horizontal: boolean,\n logicalDelta: number,\n): number {\n return horizontal && isRtl(element) ? -logicalDelta : logicalDelta;\n}\n","import { isRtl } from \"./logical_scroll\";\n\n/**\n * Turns an arrow key into a **logical** step: `+1` for \"next\", `-1` for\n * \"previous\", `0` when the key names neither.\n *\n * APG defines the horizontal pair as *next / previous* and says a vertical\n * arrangement swaps in Down/Up for the same meaning — so the pair is one axis's\n * spelling of an order, and the order reverses with the writing direction. Only\n * the horizontal pair reverses. Down/Up name an axis the writing direction does\n * not mirror, and returning them unchanged is the point: many controllers fold\n * both pairs into one branch, where swapping the branches under RTL would flip\n * the vertical axis too — a bug that reads as \"the arrows work\" until someone\n * presses Down.\n *\n * **Direction is read from the element the caller passes, which should be the\n * container that lays the items out** — not the focused child. A child may carry\n * its own `dir` (an LTR input inside an RTL form is ordinary authoring), and\n * probing per handler makes two handlers disagree at the boundary between them.\n *\n * This decides direction only. Whether the axis is even active (an\n * `orientation=\"horizontal\"` widget ignoring Down/Up), how far the step lands,\n * and what wrapping does all stay with the caller.\n *\n * **It encodes the list-order convention: `ArrowDown` is *next*.** Widgets that\n * pair the arrows by *value* instead — `ArrowUp` meaning \"more\", as a rating or a\n * slider does — must not use this, or their vertical axis inverts. Reverse the\n * horizontal pair on its own there.\n *\n * @example\n * ```ts\n * const step = logicalArrowStep(event.key, this.element);\n * if (step === 0) return;\n * this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n * ```\n */\nexport function logicalArrowStep(key: string, element: Element): 1 | -1 | 0 {\n if (key === \"ArrowDown\") return 1;\n if (key === \"ArrowUp\") return -1;\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = isRtl(element) ? \"ArrowLeft\" : \"ArrowRight\";\n return key === forward ? 1 : -1;\n}\n\n/**\n * Rewrites `key` so an existing LTR-shaped branch keeps working under RTL:\n * `ArrowRight` and `ArrowLeft` trade places, everything else passes through.\n *\n * The alternative — negating a delta — silently breaks handlers whose two\n * horizontal branches are **not mirror images**. A grid that clamps one edge but\n * not the other, or a segmented field guarding `index > 0` on one side and\n * `index < length - 1` on the other, ends up applying the wrong guard to the\n * wrong direction. Swapping the key leaves each branch, guards and all, exactly\n * where its author put it.\n *\n * Same rule as {@link logicalArrowStep} about which element to read: pass the\n * container that lays the items out, not the focused child.\n *\n * @example\n * ```ts\n * switch (logicalArrowKey(event.key, this.element)) {\n * case \"ArrowLeft\": // \"previous\" — whatever direction that is on screen\n * ```\n */\nexport function logicalArrowKey(key: string, element: Element): string {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return key;\n if (!isRtl(element)) return key;\n return key === \"ArrowRight\" ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\n/** Modifiers a widget may claim on an arrow key, named for the `allow` list. */\nexport type ArrowModifier = \"alt\" | \"ctrl\" | \"meta\" | \"shift\";\n\n/**\n * True when an arrow key arrived carrying a modifier the widget must leave to\n * the browser: return without calling `preventDefault()` and without moving any\n * state.\n *\n * A bare arrow belongs to the widget; a chorded one usually does not.\n * `Alt`/`Meta` plus a horizontal arrow is history back/forward on every desktop\n * browser, and a widget that swallows it makes the shortcut work or not\n * depending on where focus happens to sit — a coin-flip the user cannot see.\n *\n * `allow` is for the combinations APG assigns to a pattern **and the widget\n * actually implements** — today only Combobox's optional `Alt+Down`/`Alt+Up`.\n * Listing one the widget does not implement defeats the point: the chord then\n * runs the plain-arrow branch, which is exactly what this guard exists to stop.\n * Non-arrow keys return `false`, so chorded letters and\n * `Control+Home`/`Control+End` are untouched.\n *\n * @example\n * ```ts\n * if (isReservedArrowChord(event)) return;\n * ```\n */\nexport function isReservedArrowChord(\n event: KeyboardEvent,\n allow: readonly ArrowModifier[] = [],\n): boolean {\n if (!event.key.startsWith(\"Arrow\")) return false;\n return (\n (event.altKey && !allow.includes(\"alt\")) ||\n (event.ctrlKey && !allow.includes(\"ctrl\")) ||\n (event.metaKey && !allow.includes(\"meta\")) ||\n (event.shiftKey && !allow.includes(\"shift\"))\n );\n}\n\n/**\n * True when a press arrived carrying any modifier, for the keys the browser and\n * the OS own outright: return without calling `preventDefault()` and without\n * moving any state.\n *\n * `Control+Home` and `Control+End` jump the document to its ends, and a widget\n * that swallows them makes the shortcut work or not depending on where focus\n * happens to sit. {@link isReservedArrowChord} answers the same question for the\n * arrows, but returns `false` for every other key so that this one can decide.\n *\n * There is no `allow` list here on purpose. APG assigns no modifier chord to\n * `Home`/`End`, so a widget that wanted one would be claiming a combination the\n * pattern never gave it.\n *\n * @example\n * ```ts\n * case \"Home\":\n * case \"End\":\n * if (hasModifierChord(event)) return;\n * ```\n */\nexport function hasModifierChord(event: KeyboardEvent): boolean {\n return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;\n}\n","/**\n * Low-level roving-tabindex primitive shared by composite-widget controllers.\n *\n * The APG roving-tabindex pattern keeps a composite widget a single Tab stop:\n * exactly one item is in the Tab sequence (`tabindex=\"0\"`) while the rest are\n * removed from it (`tabindex=\"-1\"`), and the arrow keys move both DOM focus and\n * that single tabbable position together. {@link RovingTabindex} owns *only* that\n * mechanical bookkeeping — \"which one item is tabbable, and move focus there\".\n *\n * It is intentionally **policy-free**. Orientation, wrapping vs. clamping,\n * selection-follows-focus, typeahead, and `Home`/`End` semantics differ per APG\n * pattern (Radio Group, Toolbar, Rating, …); folding them into one helper would\n * flatten those widgets to a lowest common denominator and lose each pattern's\n * correctness. Those decisions therefore stay in each controller, which calls\n * {@link RovingTabindex.setActive} with an index it computed itself (optionally\n * via the pure {@link rovingMove} helper).\n *\n * @remarks\n * Items are read lazily through a getter so a controller can add or remove\n * targets (Stimulus re-scans the DOM) without re-wiring this helper. A caller\n * that already captured one event-scoped snapshot may pass it to\n * {@link RovingTabindex.setActive}, avoiding a second live-target scan while\n * preserving one coherent collection for the whole operation.\n */\nexport class RovingTabindex {\n /** Returns the current ordered item elements; called on every operation. */\n readonly #getItems: () => HTMLElement[];\n\n /**\n * @param getItems - Returns the current ordered item elements. Called on every\n * operation so the live target list is always used.\n */\n constructor(getItems: () => HTMLElement[]) {\n this.#getItems = getItems;\n }\n\n /** Index of the currently tabbable item (`tabindex=\"0\"`), or `-1` if none. */\n get activeIndex(): number {\n return this.#getItems().findIndex((item) => item.tabIndex === 0);\n }\n\n /**\n * Makes exactly the item at `index` tabbable (`tabindex=\"0\"`) and removes every\n * other item from the Tab sequence (`tabindex=\"-1\"`). An out-of-range `index`\n * (e.g. `-1`) leaves all items at `-1`, which a controller can use to express\n * \"nothing is currently tabbable\".\n *\n * @param index - Position of the item to make tabbable.\n * @param options - Pass `{ focus: true }` to also move DOM focus to that item,\n * and `items` to reuse an event-scoped collection snapshot.\n */\n setActive(\n index: number,\n options: { focus?: boolean; items?: readonly HTMLElement[] } = {},\n ): void {\n const { focus = false } = options;\n const items = options.items ?? this.#getItems();\n items.forEach((item, i) => {\n item.tabIndex = i === index ? 0 : -1;\n });\n if (focus) items[index]?.focus();\n }\n}\n\n/** Edge behavior for {@link rovingMove}: cycle past the ends, or stop at them. */\nexport type RovingWrap = \"wrap\" | \"clamp\";\n\n/**\n * Pure helper that resolves the target index for a one-step directional move.\n *\n * Keyboard/orientation mapping stays in the caller: it decides that a key means\n * `delta` `+1` (next) or `-1` (previous) and whether the widget should `\"wrap\"`\n * (Radio Group, Toolbar) or `\"clamp\"` at the ends.\n *\n * @param current - The index focus is moving from.\n * @param length - Number of items in the set.\n * @param delta - `+1` to move to the next item, `-1` for the previous.\n * @param wrap - `\"wrap\"` cycles around the ends; `\"clamp\"` stops at them.\n * @returns The resolved index, or `-1` when there are no items.\n */\nexport function rovingMove(\n current: number,\n length: number,\n delta: number,\n wrap: RovingWrap,\n): number {\n if (length === 0) return -1;\n const next = current + delta;\n if (wrap === \"wrap\") return (next + length) % length;\n return Math.min(length - 1, Math.max(0, next));\n}\n","/** A successful guarded storage operation. */\nexport interface StorageSuccess<T> {\n readonly ok: true;\n readonly value: T;\n}\n\n/** A guarded storage operation rejected by the browser. */\nexport interface StorageFailure {\n readonly ok: false;\n readonly error: unknown;\n}\n\n/** Result of accessing browser storage without allowing an exception to escape. */\nexport type StorageResult<T> = StorageSuccess<T> | StorageFailure;\n\n/**\n * Guarded `localStorage` access shared by controllers that persist client state.\n *\n * `window.localStorage` can throw while resolving the storage object or while\n * reading, writing, or removing a key. The result keeps an unavailable store\n * distinct from an available store whose key is unset, so callers can expose an\n * accurate success or failure state without duplicating exception guards.\n */\n\n/**\n * Reads `key` from `localStorage`.\n *\n * @param key - The storage key to read.\n * @returns A successful result containing the stored string or `null`, or a\n * failure result when storage is unavailable.\n */\nexport function readLocalStorage(key: string): StorageResult<string | null> {\n try {\n return { ok: true, value: window.localStorage.getItem(key) };\n } catch (error: unknown) {\n return { ok: false, error };\n }\n}\n\n/**\n * Writes `value` under `key` in `localStorage`.\n *\n * @param key - The storage key to write.\n * @param value - The string to store.\n * @returns Success only after `setItem` completes; otherwise the browser error.\n */\nexport function writeLocalStorage(key: string, value: string): StorageResult<void> {\n try {\n window.localStorage.setItem(key, value);\n return { ok: true, value: undefined };\n } catch (error: unknown) {\n return { ok: false, error };\n }\n}\n\n/**\n * Removes `key` from `localStorage`.\n *\n * @param key - The storage key to remove.\n * @returns Success only after `removeItem` completes; otherwise the browser error.\n */\nexport function removeLocalStorage(key: string): StorageResult<void> {\n try {\n window.localStorage.removeItem(key);\n return { ok: true, value: undefined };\n } catch (error: unknown) {\n return { ok: false, error };\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { hasModifierChord, isReservedArrowChord, logicalArrowStep } from \"../utils/arrow_step\";\nimport { RovingTabindex, rovingMove } from \"../utils/roving_tabindex\";\nimport { readLocalStorage, writeLocalStorage } from \"../utils/safe_storage\";\n\n/** The three selectable modes; `system` follows the OS `prefers-color-scheme`. */\ntype ThemeMode = \"light\" | \"dark\" | \"system\";\n/** The two effective (resolved) themes applied to the root. */\ntype ResolvedTheme = \"light\" | \"dark\";\n\nconst MODES: readonly ThemeMode[] = [\"light\", \"dark\", \"system\"];\nconst isMode = (value: unknown): value is ThemeMode =>\n typeof value === \"string\" && (MODES as readonly string[]).includes(value);\n\n/** The mode a declaration falls back to when it cannot be read as one. */\nconst DEFAULT_MODE: ThemeMode = \"system\";\n/** The state-hook target a declaration falls back to when it cannot be parsed. */\nconst DEFAULT_TARGET = \"html\";\n\n/**\n * Headless **theme / color-scheme toggle** — persists a light/dark/system choice to\n * `localStorage`, follows the OS setting while in `system`, and reflects the\n * effective theme onto the root for the consumer's CSS. Ships no colors; only state\n * hooks.\n *\n * Two markup contracts (identifier: `stimeo--theme`):\n *\n * Canonical 3-value radiogroup (use this when `system` is offered):\n * <div data-controller=\"stimeo--theme\" data-stimeo--theme-mode-value=\"system\"\n * role=\"radiogroup\" aria-label=\"Theme\">\n * <button data-stimeo--theme-target=\"option\" role=\"radio\"\n * data-action=\"click->stimeo--theme#set\"\n * data-value=\"light\">Light</button>\n * <button data-stimeo--theme-target=\"option\" role=\"radio\"\n * data-action=\"click->stimeo--theme#set\"\n * data-value=\"dark\">Dark</button>\n * <button data-stimeo--theme-target=\"option\" role=\"radio\"\n * data-action=\"click->stimeo--theme#set\"\n * data-value=\"system\">System</button>\n * </div>\n *\n * Auxiliary 2-value toggle (light↔dark only — `system` is not representable):\n * <button data-controller=\"stimeo--theme\" data-action=\"click->stimeo--theme#toggle\"\n * aria-pressed=\"false\">Dark mode</button>\n *\n * `change` dispatches `{ mode, resolved }`, and only when one of the two moved.\n *\n * @remarks\n * Behavior only — the actual palette is the consumer's CSS keyed off `data-theme`\n * on the root. It applies `data-theme` (the *resolved* light/dark) and a matching\n * `color-scheme` to the `target` element (`<html>` by default), keeps the radiogroup\n * `aria-checked` + roving tabindex (APG radio) or the single button's `aria-pressed`\n * in sync. Arrow/Home/End move focus across the radiogroup as the APG radio pattern\n * requires; applying a theme never moves focus. The `prefers-color-scheme` listener is attached on\n * `connect()` and removed on `disconnect()` (Turbo included). FOUC avoidance for the\n * very first paint is an inline `<head>` snippet, not this controller.\n *\n * Every declaration is validated where it enters, and an unreadable one falls back\n * to that Value's default rather than taking the widget with it: a `mode` outside\n * the three modes reads as `system`, and a `target` that is not a parsable selector\n * reads as `html`. An option whose `data-value` is outside the three can be focused\n * but never becomes the selection, so no more than one option is ever checked —\n * and none at all when the resolved mode matches no option, where the first one\n * keeps the Tab stop.\n *\n * The radiogroup stays one Tab stop through `RovingTabindex`, re-derived\n * whenever an option enters or leaves, so a set rendered after connect or swapped\n * by a Turbo morph carries the same single stop as any other.\n */\nexport class ThemeController extends Controller<HTMLElement> {\n static override targets = [\"option\"];\n static override values = {\n mode: { type: String, default: DEFAULT_MODE },\n storageKey: { type: String, default: \"stimeo-theme\" },\n target: { type: String, default: DEFAULT_TARGET },\n };\n static actions = [\"set\", \"toggle\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly optionTargets: HTMLElement[];\n declare readonly hasOptionTarget: boolean;\n\n declare modeValue: string;\n declare storageKeyValue: string;\n declare targetValue: string;\n\n /** The OS dark-mode query, watched so `system` tracks live changes. */\n #media: MediaQueryList | null = null;\n\n /** Gate for the target callbacks, which Stimulus runs before `connect()`. */\n #connected = false;\n\n /** The `target` declaration after validation; the default when unparsable. */\n #targetSelector = DEFAULT_TARGET;\n\n /** Owns the single Tab stop across the option set (APG radiogroup). */\n readonly #roving = new RovingTabindex(() => this.optionTargets);\n\n /**\n * The pair last reported, so a move can be told from a repeat. Neither side is\n * readable after the fact — assigning the Value updates the mode before any\n * comparison, and the OS query has already flipped by the time it notifies —\n * so what was reported has to be kept rather than recomputed.\n */\n #published: { mode: ThemeMode; resolved: ResolvedTheme } = {\n mode: DEFAULT_MODE,\n resolved: \"light\",\n };\n\n /** Re-resolves while in `system` mode when the OS preference flips. */\n readonly #onMediaChange = (): void => {\n // Only `system` follows the OS; an explicit mode already decided the answer.\n if (this.#mode !== \"system\") return;\n this.#commit();\n };\n\n /** Arrow/Home/End navigation for the radiogroup (APG radio pattern). */\n readonly #onKeydown = (event: KeyboardEvent): void => {\n // A descendant widget that already claimed the key (a grabbed drag handle, a\n // nested menu) must not ALSO act on it — composition depends on this yield.\n if (event.defaultPrevented) return;\n if (isReservedArrowChord(event)) return;\n // Resolved once per keydown: every `optionTargets` access re-queries the scope.\n const options = this.optionTargets;\n const target = event.target as HTMLElement | null;\n const current = options.indexOf(target as HTMLElement);\n if (current === -1) return;\n\n let next = current;\n // Logical, not physical. The helper reverses only the horizontal pair, so\n // folding Down/Up into the same branch stays correct.\n const step = logicalArrowStep(event.key, this.element);\n switch (event.key) {\n case \"ArrowDown\":\n case \"ArrowRight\":\n case \"ArrowUp\":\n case \"ArrowLeft\":\n next = rovingMove(current, options.length, step, \"wrap\");\n break;\n case \"Home\":\n case \"End\":\n // Control+Home jumps the document, not the widget.\n if (hasModifierChord(event)) return;\n next = event.key === \"Home\" ? 0 : options.length - 1;\n break;\n default:\n return;\n }\n event.preventDefault();\n const option = options[next];\n if (!option) return;\n option.focus();\n const mode = this.#optionMode(option);\n // An option outside the three modes can hold focus, but the selection does\n // not follow it there.\n if (mode) this.#setMode(mode);\n };\n\n override connect(): void {\n const stored = this.#readStored();\n if (stored) this.modeValue = stored;\n\n this.#media = window.matchMedia?.(\"(prefers-color-scheme: dark)\") ?? null;\n this.#media?.addEventListener(\"change\", this.#onMediaChange);\n this.element.addEventListener(\"keydown\", this.#onKeydown);\n\n this.#connected = true;\n this.#applyTheme();\n this.#syncControls();\n // Connecting is not a change: seed the baseline instead of announcing one.\n this.#published = this.#current;\n }\n\n override disconnect(): void {\n this.#connected = false;\n this.#media?.removeEventListener(\"change\", this.#onMediaChange);\n this.element.removeEventListener(\"keydown\", this.#onKeydown);\n }\n\n /** Validates the `target` declaration once, so the render path never parses. */\n targetValueChanged(): void {\n const selector = this.targetValue;\n if (selector.length > 0) {\n try {\n this.element.matches(selector);\n this.#targetSelector = selector;\n return;\n } catch {\n // Unparsable selector: fall through to the default below.\n }\n }\n this.#targetSelector = DEFAULT_TARGET;\n }\n\n /** Re-derives the single Tab stop and ARIA for an option set that changed. */\n optionTargetConnected(): void {\n if (this.#connected) this.#syncControls();\n }\n\n /** Re-derives them again when an option leaves, so a Tab stop always remains. */\n optionTargetDisconnected(): void {\n if (this.#connected) this.#syncControls();\n }\n\n /**\n * Selects the mode the activated option declares.\n *\n * Read through {@link ThemeController.#optionMode}, the same lane that decides\n * which option is checked, so the two can never disagree about what an option\n * declares.\n */\n set(event: Event): void {\n const option = event.currentTarget;\n if (!(option instanceof HTMLElement)) return;\n const mode = this.#optionMode(option);\n if (mode) this.#setMode(mode);\n }\n\n /** Toggles light↔dark for the 2-value single-button contract. */\n toggle(): void {\n this.#setMode(this.#resolved() === \"dark\" ? \"light\" : \"dark\");\n }\n\n /** Central mode change: persist, apply to the root, resync controls, announce. */\n #setMode(mode: ThemeMode): void {\n this.modeValue = mode;\n this.#writeStored(mode);\n this.#commit();\n }\n\n /**\n * Applies the current mode and reports it, but reports only a real move: the\n * event means \"the selection or the effective theme moved\", so re-choosing the\n * option already chosen is not one.\n */\n #commit(): void {\n this.#applyTheme();\n this.#syncControls();\n const next = this.#current;\n const last = this.#published;\n this.#published = next;\n if (last.mode !== next.mode || last.resolved !== next.resolved) {\n // A copy: the baseline has to survive a listener that writes to what it\n // was handed, or the next unchanged operation reads as a change.\n this.dispatch(\"change\", { detail: { ...next } });\n }\n }\n\n /** The pair the `change` detail carries, read from current state. */\n get #current(): { mode: ThemeMode; resolved: ResolvedTheme } {\n return { mode: this.#mode, resolved: this.#resolved() };\n }\n\n /** Writes `data-theme` + `color-scheme` (the resolved theme) onto the target. */\n #applyTheme(): void {\n const root = this.#targetElement();\n if (!root) return;\n const resolved = this.#resolved();\n root.setAttribute(\"data-theme\", resolved);\n root.style.setProperty(\"color-scheme\", resolved);\n }\n\n /** Keeps the radiogroup (aria-checked + roving tabindex) or toggle (aria-pressed) in sync. */\n #syncControls(): void {\n const options = this.optionTargets;\n if (options.length > 0) {\n const mode = this.#mode;\n let selected = -1;\n options.forEach((option, index) => {\n const isSelected = this.#optionMode(option) === mode;\n option.setAttribute(\"aria-checked\", String(isSelected));\n if (isSelected && selected === -1) selected = index;\n });\n // APG: a radiogroup with no selection keeps its first radio tabbable.\n this.#roving.setActive(selected === -1 ? 0 : selected, { items: options });\n return;\n }\n // `aria-pressed` belongs to the 2-value contract, where the controller sits on\n // the button itself. A radiogroup whose options have not rendered yet is not\n // that, and must not be told it is a toggle.\n if (this.#isToggleButton) {\n this.element.setAttribute(\"aria-pressed\", String(this.#resolved() === \"dark\"));\n }\n }\n\n /** Whether the controller element is the button of the 2-value contract. */\n get #isToggleButton(): boolean {\n return this.element.tagName === \"BUTTON\" || this.element.getAttribute(\"role\") === \"button\";\n }\n\n /** The selected mode after validation; an unreadable declaration is the default. */\n get #mode(): ThemeMode {\n return isMode(this.modeValue) ? this.modeValue : DEFAULT_MODE;\n }\n\n /** The effective theme: the OS preference when `system`, else the mode itself. */\n #resolved(): ResolvedTheme {\n const mode = this.#mode;\n if (mode === \"dark\") return \"dark\";\n if (mode === \"light\") return \"light\";\n return this.#media?.matches ? \"dark\" : \"light\";\n }\n\n /** An option's mode from its `data-value`, or `null` when that is not one of the three. */\n #optionMode(option: HTMLElement): ThemeMode | null {\n const mode = option.getAttribute(\"data-value\");\n return isMode(mode) ? mode : null;\n }\n\n /** Resolves the state-hook target (`<html>` by default). */\n #targetElement(): HTMLElement | null {\n const selector = this.#targetSelector;\n if (selector === DEFAULT_TARGET || selector === \":root\") return document.documentElement;\n return document.querySelector<HTMLElement>(selector);\n }\n\n /** Reads a persisted, validated mode from `localStorage` (null when absent/blocked). */\n #readStored(): ThemeMode | null {\n const result = readLocalStorage(this.storageKeyValue);\n return result.ok && isMode(result.value) ? result.value : null;\n }\n\n /** Persists the mode, swallowing storage errors (private mode / quota). */\n #writeStored(mode: ThemeMode): void {\n writeLocalStorage(this.storageKeyValue, mode);\n }\n}\n"]}
|
|
@@ -101,7 +101,7 @@ var ToastController = class extends Controller {
|
|
|
101
101
|
static events = ["dismiss", "show"];
|
|
102
102
|
/**
|
|
103
103
|
* Registry for every auto-dismiss and transition-finalize timer the controller
|
|
104
|
-
* schedules.
|
|
104
|
+
* schedules. `SafeTimeout` owns *registration and teardown only*; the
|
|
105
105
|
* pause/resume remaining-time accounting stays in `#activeTimeouts` so the
|
|
106
106
|
* per-widget WCAG 2.2.1 semantics are not flattened into the helper.
|
|
107
107
|
*/
|