@lo-tek/interfaces 0.0.0 → 0.37.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 +3254 -0
- package/LICENSE +21 -0
- package/README.md +128 -1
- package/agent/astro.md +400 -0
- package/agent/bundles.md +429 -0
- package/agent/datastar.md +409 -0
- package/agent/html.md +388 -0
- package/agent/react.md +440 -0
- package/agent/templates.md +472 -0
- package/core/lotek-core.wasm +0 -0
- package/core/lotek.wasm +0 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +14 -0
- package/dist/cli.js.map +1 -0
- package/dist/components/css/accordion/accordion.d.ts +37 -0
- package/dist/components/css/accordion/accordion.d.ts.map +1 -0
- package/dist/components/css/accordion/accordion.js +31 -0
- package/dist/components/css/accordion/accordion.js.map +1 -0
- package/dist/components/css/alert/alert.d.ts +35 -0
- package/dist/components/css/alert/alert.d.ts.map +1 -0
- package/dist/components/css/alert/alert.js +29 -0
- package/dist/components/css/alert/alert.js.map +1 -0
- package/dist/components/css/avatar/avatar.d.ts +40 -0
- package/dist/components/css/avatar/avatar.d.ts.map +1 -0
- package/dist/components/css/avatar/avatar.js +30 -0
- package/dist/components/css/avatar/avatar.js.map +1 -0
- package/dist/components/css/badge/badge.d.ts +24 -0
- package/dist/components/css/badge/badge.d.ts.map +1 -0
- package/dist/components/css/badge/badge.js +24 -0
- package/dist/components/css/badge/badge.js.map +1 -0
- package/dist/components/css/breadcrumbs/breadcrumbs.d.ts +26 -0
- package/dist/components/css/breadcrumbs/breadcrumbs.d.ts.map +1 -0
- package/dist/components/css/breadcrumbs/breadcrumbs.js +24 -0
- package/dist/components/css/breadcrumbs/breadcrumbs.js.map +1 -0
- package/dist/components/css/button/button.d.ts +26 -0
- package/dist/components/css/button/button.d.ts.map +1 -0
- package/dist/components/css/button/button.js +25 -0
- package/dist/components/css/button/button.js.map +1 -0
- package/dist/components/css/card/card.d.ts +36 -0
- package/dist/components/css/card/card.d.ts.map +1 -0
- package/dist/components/css/card/card.js +28 -0
- package/dist/components/css/card/card.js.map +1 -0
- package/dist/components/css/checkbox/checkbox.d.ts +32 -0
- package/dist/components/css/checkbox/checkbox.d.ts.map +1 -0
- package/dist/components/css/checkbox/checkbox.js +27 -0
- package/dist/components/css/checkbox/checkbox.js.map +1 -0
- package/dist/components/css/divider/divider.d.ts +29 -0
- package/dist/components/css/divider/divider.d.ts.map +1 -0
- package/dist/components/css/divider/divider.js +23 -0
- package/dist/components/css/divider/divider.js.map +1 -0
- package/dist/components/css/error-text/error-text.d.ts +24 -0
- package/dist/components/css/error-text/error-text.d.ts.map +1 -0
- package/dist/components/css/error-text/error-text.js +18 -0
- package/dist/components/css/error-text/error-text.js.map +1 -0
- package/dist/components/css/fieldset/fieldset.d.ts +55 -0
- package/dist/components/css/fieldset/fieldset.d.ts.map +1 -0
- package/dist/components/css/fieldset/fieldset.js +39 -0
- package/dist/components/css/fieldset/fieldset.js.map +1 -0
- package/dist/components/css/file-upload/file-upload.d.ts +41 -0
- package/dist/components/css/file-upload/file-upload.d.ts.map +1 -0
- package/dist/components/css/file-upload/file-upload.js +32 -0
- package/dist/components/css/file-upload/file-upload.js.map +1 -0
- package/dist/components/css/heading/heading.d.ts +29 -0
- package/dist/components/css/heading/heading.d.ts.map +1 -0
- package/dist/components/css/heading/heading.js +23 -0
- package/dist/components/css/heading/heading.js.map +1 -0
- package/dist/components/css/help-text/help-text.d.ts +24 -0
- package/dist/components/css/help-text/help-text.d.ts.map +1 -0
- package/dist/components/css/help-text/help-text.js +23 -0
- package/dist/components/css/help-text/help-text.js.map +1 -0
- package/dist/components/css/icon/icon.d.ts +34 -0
- package/dist/components/css/icon/icon.d.ts.map +1 -0
- package/dist/components/css/icon/icon.js +25 -0
- package/dist/components/css/icon/icon.js.map +1 -0
- package/dist/components/css/input/input.d.ts +40 -0
- package/dist/components/css/input/input.d.ts.map +1 -0
- package/dist/components/css/input/input.js +45 -0
- package/dist/components/css/input/input.js.map +1 -0
- package/dist/components/css/label/label.d.ts +33 -0
- package/dist/components/css/label/label.d.ts.map +1 -0
- package/dist/components/css/label/label.js +23 -0
- package/dist/components/css/label/label.js.map +1 -0
- package/dist/components/css/link/link.d.ts +28 -0
- package/dist/components/css/link/link.d.ts.map +1 -0
- package/dist/components/css/link/link.js +20 -0
- package/dist/components/css/link/link.js.map +1 -0
- package/dist/components/css/list/list.d.ts +31 -0
- package/dist/components/css/list/list.d.ts.map +1 -0
- package/dist/components/css/list/list.js +25 -0
- package/dist/components/css/list/list.js.map +1 -0
- package/dist/components/css/pagination/pagination.d.ts +27 -0
- package/dist/components/css/pagination/pagination.d.ts.map +1 -0
- package/dist/components/css/pagination/pagination.js +25 -0
- package/dist/components/css/pagination/pagination.js.map +1 -0
- package/dist/components/css/paragraph/paragraph.d.ts +36 -0
- package/dist/components/css/paragraph/paragraph.d.ts.map +1 -0
- package/dist/components/css/paragraph/paragraph.js +26 -0
- package/dist/components/css/paragraph/paragraph.js.map +1 -0
- package/dist/components/css/progress/progress.d.ts +53 -0
- package/dist/components/css/progress/progress.d.ts.map +1 -0
- package/dist/components/css/progress/progress.js +38 -0
- package/dist/components/css/progress/progress.js.map +1 -0
- package/dist/components/css/radio/radio.d.ts +32 -0
- package/dist/components/css/radio/radio.d.ts.map +1 -0
- package/dist/components/css/radio/radio.js +27 -0
- package/dist/components/css/radio/radio.js.map +1 -0
- package/dist/components/css/search/search.d.ts +34 -0
- package/dist/components/css/search/search.d.ts.map +1 -0
- package/dist/components/css/search/search.js +32 -0
- package/dist/components/css/search/search.js.map +1 -0
- package/dist/components/css/select/select.d.ts +40 -0
- package/dist/components/css/select/select.d.ts.map +1 -0
- package/dist/components/css/select/select.js +31 -0
- package/dist/components/css/select/select.js.map +1 -0
- package/dist/components/css/shared.d.ts +90 -0
- package/dist/components/css/shared.d.ts.map +1 -0
- package/dist/components/css/shared.js +167 -0
- package/dist/components/css/shared.js.map +1 -0
- package/dist/components/css/skeleton/skeleton.d.ts +29 -0
- package/dist/components/css/skeleton/skeleton.d.ts.map +1 -0
- package/dist/components/css/skeleton/skeleton.js +23 -0
- package/dist/components/css/skeleton/skeleton.js.map +1 -0
- package/dist/components/css/skip-link/skip-link.d.ts +18 -0
- package/dist/components/css/skip-link/skip-link.d.ts.map +1 -0
- package/dist/components/css/skip-link/skip-link.js +16 -0
- package/dist/components/css/skip-link/skip-link.js.map +1 -0
- package/dist/components/css/spinner/spinner.d.ts +33 -0
- package/dist/components/css/spinner/spinner.d.ts.map +1 -0
- package/dist/components/css/spinner/spinner.js +25 -0
- package/dist/components/css/spinner/spinner.js.map +1 -0
- package/dist/components/css/sr-only/sr-only.d.ts +17 -0
- package/dist/components/css/sr-only/sr-only.d.ts.map +1 -0
- package/dist/components/css/sr-only/sr-only.js +15 -0
- package/dist/components/css/sr-only/sr-only.js.map +1 -0
- package/dist/components/css/switch/switch.d.ts +37 -0
- package/dist/components/css/switch/switch.d.ts.map +1 -0
- package/dist/components/css/switch/switch.js +29 -0
- package/dist/components/css/switch/switch.js.map +1 -0
- package/dist/components/css/table/table.d.ts +76 -0
- package/dist/components/css/table/table.d.ts.map +1 -0
- package/dist/components/css/table/table.js +59 -0
- package/dist/components/css/table/table.js.map +1 -0
- package/dist/components/css/tag/tag.d.ts +35 -0
- package/dist/components/css/tag/tag.d.ts.map +1 -0
- package/dist/components/css/tag/tag.js +27 -0
- package/dist/components/css/tag/tag.js.map +1 -0
- package/dist/components/css/textarea/textarea.d.ts +23 -0
- package/dist/components/css/textarea/textarea.d.ts.map +1 -0
- package/dist/components/css/textarea/textarea.js +18 -0
- package/dist/components/css/textarea/textarea.js.map +1 -0
- package/dist/components/css/toggle-group/toggle-group.d.ts +31 -0
- package/dist/components/css/toggle-group/toggle-group.d.ts.map +1 -0
- package/dist/components/css/toggle-group/toggle-group.js +29 -0
- package/dist/components/css/toggle-group/toggle-group.js.map +1 -0
- package/dist/components/css/tooltip/tooltip.d.ts +31 -0
- package/dist/components/css/tooltip/tooltip.d.ts.map +1 -0
- package/dist/components/css/tooltip/tooltip.js +29 -0
- package/dist/components/css/tooltip/tooltip.js.map +1 -0
- package/dist/components/default-texts.d.ts +35 -0
- package/dist/components/default-texts.d.ts.map +1 -0
- package/dist/components/default-texts.js +35 -0
- package/dist/components/default-texts.js.map +1 -0
- package/dist/components/events.d.ts +77 -0
- package/dist/components/events.d.ts.map +1 -0
- package/dist/components/events.js +3 -0
- package/dist/components/events.js.map +1 -0
- package/dist/components/frame/dialog/dialog.d.ts +122 -0
- package/dist/components/frame/dialog/dialog.d.ts.map +1 -0
- package/dist/components/frame/dialog/dialog.js +112 -0
- package/dist/components/frame/dialog/dialog.js.map +1 -0
- package/dist/components/frame/dialog/lt-dialog.d.ts +104 -0
- package/dist/components/frame/dialog/lt-dialog.d.ts.map +1 -0
- package/dist/components/frame/dialog/lt-dialog.js +431 -0
- package/dist/components/frame/dialog/lt-dialog.js.map +1 -0
- package/dist/components/frame/error-summary/error-summary.d.ts +40 -0
- package/dist/components/frame/error-summary/error-summary.d.ts.map +1 -0
- package/dist/components/frame/error-summary/error-summary.js +32 -0
- package/dist/components/frame/error-summary/error-summary.js.map +1 -0
- package/dist/components/frame/error-summary/lt-error-summary.d.ts +82 -0
- package/dist/components/frame/error-summary/lt-error-summary.d.ts.map +1 -0
- package/dist/components/frame/error-summary/lt-error-summary.js +302 -0
- package/dist/components/frame/error-summary/lt-error-summary.js.map +1 -0
- package/dist/components/frame/field/field-core.d.ts +92 -0
- package/dist/components/frame/field/field-core.d.ts.map +1 -0
- package/dist/components/frame/field/field-core.js +63 -0
- package/dist/components/frame/field/field-core.js.map +1 -0
- package/dist/components/frame/field/lt-field.d.ts +173 -0
- package/dist/components/frame/field/lt-field.d.ts.map +1 -0
- package/dist/components/frame/field/lt-field.js +580 -0
- package/dist/components/frame/field/lt-field.js.map +1 -0
- package/dist/components/frame/popover/lt-popover.d.ts +132 -0
- package/dist/components/frame/popover/lt-popover.d.ts.map +1 -0
- package/dist/components/frame/popover/lt-popover.js +490 -0
- package/dist/components/frame/popover/lt-popover.js.map +1 -0
- package/dist/components/frame/popover/popover.d.ts +49 -0
- package/dist/components/frame/popover/popover.d.ts.map +1 -0
- package/dist/components/frame/popover/popover.js +64 -0
- package/dist/components/frame/popover/popover.js.map +1 -0
- package/dist/components/frame/session-timeout/lt-session-timeout.d.ts +198 -0
- package/dist/components/frame/session-timeout/lt-session-timeout.d.ts.map +1 -0
- package/dist/components/frame/session-timeout/lt-session-timeout.js +573 -0
- package/dist/components/frame/session-timeout/lt-session-timeout.js.map +1 -0
- package/dist/components/frame/session-timeout/session-timeout.d.ts +113 -0
- package/dist/components/frame/session-timeout/session-timeout.d.ts.map +1 -0
- package/dist/components/frame/session-timeout/session-timeout.js +101 -0
- package/dist/components/frame/session-timeout/session-timeout.js.map +1 -0
- package/dist/components/frame/suggestion/lt-suggestion.d.ts +361 -0
- package/dist/components/frame/suggestion/lt-suggestion.d.ts.map +1 -0
- package/dist/components/frame/suggestion/lt-suggestion.js +1152 -0
- package/dist/components/frame/suggestion/lt-suggestion.js.map +1 -0
- package/dist/components/frame/suggestion/suggestion.d.ts +143 -0
- package/dist/components/frame/suggestion/suggestion.d.ts.map +1 -0
- package/dist/components/frame/suggestion/suggestion.js +127 -0
- package/dist/components/frame/suggestion/suggestion.js.map +1 -0
- package/dist/components/frame/tabs/lt-tabs.d.ts +404 -0
- package/dist/components/frame/tabs/lt-tabs.d.ts.map +1 -0
- package/dist/components/frame/tabs/lt-tabs.js +1201 -0
- package/dist/components/frame/tabs/lt-tabs.js.map +1 -0
- package/dist/components/frame/tabs/tabs.d.ts +112 -0
- package/dist/components/frame/tabs/tabs.d.ts.map +1 -0
- package/dist/components/frame/tabs/tabs.js +107 -0
- package/dist/components/frame/tabs/tabs.js.map +1 -0
- package/dist/components/host-element.d.ts +135 -0
- package/dist/components/host-element.d.ts.map +1 -0
- package/dist/components/host-element.js +366 -0
- package/dist/components/host-element.js.map +1 -0
- package/dist/components/standalone/connection-status/connection-status.d.ts +28 -0
- package/dist/components/standalone/connection-status/connection-status.d.ts.map +1 -0
- package/dist/components/standalone/connection-status/connection-status.js +22 -0
- package/dist/components/standalone/connection-status/connection-status.js.map +1 -0
- package/dist/components/standalone/connection-status/lt-connection-status.d.ts +81 -0
- package/dist/components/standalone/connection-status/lt-connection-status.d.ts.map +1 -0
- package/dist/components/standalone/connection-status/lt-connection-status.js +216 -0
- package/dist/components/standalone/connection-status/lt-connection-status.js.map +1 -0
- package/dist/components/standalone/toast/lt-toast.d.ts +109 -0
- package/dist/components/standalone/toast/lt-toast.d.ts.map +1 -0
- package/dist/components/standalone/toast/lt-toast.js +256 -0
- package/dist/components/standalone/toast/lt-toast.js.map +1 -0
- package/dist/components/standalone/toast/toast.d.ts +52 -0
- package/dist/components/standalone/toast/toast.d.ts.map +1 -0
- package/dist/components/standalone/toast/toast.js +42 -0
- package/dist/components/standalone/toast/toast.js.map +1 -0
- package/dist/diagnostics/core.d.ts +137 -0
- package/dist/diagnostics/core.d.ts.map +1 -0
- package/dist/diagnostics/core.js +72 -0
- package/dist/diagnostics/core.js.map +1 -0
- package/dist/diagnostics/default-core.d.ts +10 -0
- package/dist/diagnostics/default-core.d.ts.map +1 -0
- package/dist/diagnostics/default-core.js +72 -0
- package/dist/diagnostics/default-core.js.map +1 -0
- package/dist/diagnostics/index.d.ts +84 -0
- package/dist/diagnostics/index.d.ts.map +1 -0
- package/dist/diagnostics/index.js +106 -0
- package/dist/diagnostics/index.js.map +1 -0
- package/dist/dom.d.ts +55 -0
- package/dist/dom.d.ts.map +1 -0
- package/dist/dom.js +162 -0
- package/dist/dom.js.map +1 -0
- package/dist/index.d.ts +97 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +58 -0
- package/dist/index.js.map +1 -0
- package/dist/jsx/react.d.ts +180 -0
- package/dist/jsx/react.d.ts.map +1 -0
- package/dist/jsx/react.js +2 -0
- package/dist/jsx/react.js.map +1 -0
- package/dist/lotek.css +3617 -0
- package/dist/lt.d.ts +325 -0
- package/dist/lt.d.ts.map +1 -0
- package/dist/lt.js +183 -0
- package/dist/lt.js.map +1 -0
- package/dist/react.d.ts +492 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +296 -0
- package/dist/react.js.map +1 -0
- package/dist/register.d.ts +17 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/register.js +35 -0
- package/dist/register.js.map +1 -0
- package/dist/tokens/color.d.ts +43 -0
- package/dist/tokens/color.d.ts.map +1 -0
- package/dist/tokens/color.js +123 -0
- package/dist/tokens/color.js.map +1 -0
- package/dist/tokens/contract.d.ts +18 -0
- package/dist/tokens/contract.d.ts.map +1 -0
- package/dist/tokens/contract.js +18 -0
- package/dist/tokens/contract.js.map +1 -0
- package/dist/tokens/matrix-cells.d.ts +30 -0
- package/dist/tokens/matrix-cells.d.ts.map +1 -0
- package/dist/tokens/matrix-cells.js +273 -0
- package/dist/tokens/matrix-cells.js.map +1 -0
- package/dist/tokens/matrix.d.ts +18 -0
- package/dist/tokens/matrix.d.ts.map +1 -0
- package/dist/tokens/matrix.js +21 -0
- package/dist/tokens/matrix.js.map +1 -0
- package/dist/tokens/theme-check.d.ts +42 -0
- package/dist/tokens/theme-check.d.ts.map +1 -0
- package/dist/tokens/theme-check.js +10 -0
- package/dist/tokens/theme-check.js.map +1 -0
- package/dist/tokens/theme.d.ts +94 -0
- package/dist/tokens/theme.d.ts.map +1 -0
- package/dist/tokens/theme.js +14 -0
- package/dist/tokens/theme.js.map +1 -0
- package/dist/tokens/tokens.d.ts +122 -0
- package/dist/tokens/tokens.d.ts.map +1 -0
- package/dist/tokens/tokens.js +158 -0
- package/dist/tokens/tokens.js.map +1 -0
- package/dist/vocabulary/classes.d.ts +3 -0
- package/dist/vocabulary/classes.d.ts.map +1 -0
- package/dist/vocabulary/classes.js +1320 -0
- package/dist/vocabulary/classes.js.map +1 -0
- package/dist/vocabulary/elements.d.ts +3 -0
- package/dist/vocabulary/elements.d.ts.map +1 -0
- package/dist/vocabulary/elements.js +410 -0
- package/dist/vocabulary/elements.js.map +1 -0
- package/dist/vocabulary/types.d.ts +175 -0
- package/dist/vocabulary/types.d.ts.map +1 -0
- package/dist/vocabulary/types.js +11 -0
- package/dist/vocabulary/types.js.map +1 -0
- package/dist/wasi-host.d.ts +24 -0
- package/dist/wasi-host.d.ts.map +1 -0
- package/dist/wasi-host.js +140 -0
- package/dist/wasi-host.js.map +1 -0
- package/live-templates.xml +62 -0
- package/manifest/files.json +373 -0
- package/manifest/manifest.json +4965 -0
- package/manifest/manifest.schema.json +513 -0
- package/package.json +398 -5
- package/src/cli.ts +21 -0
- package/src/components/css/accordion/accordion.css +102 -0
- package/src/components/css/accordion/accordion.ts +50 -0
- package/src/components/css/alert/alert.css +53 -0
- package/src/components/css/alert/alert.ts +48 -0
- package/src/components/css/avatar/avatar.css +96 -0
- package/src/components/css/avatar/avatar.ts +57 -0
- package/src/components/css/badge/badge.css +42 -0
- package/src/components/css/badge/badge.ts +43 -0
- package/src/components/css/breadcrumbs/breadcrumbs.css +62 -0
- package/src/components/css/breadcrumbs/breadcrumbs.ts +30 -0
- package/src/components/css/button/button.css +128 -0
- package/src/components/css/button/button.ts +44 -0
- package/src/components/css/card/card.css +65 -0
- package/src/components/css/card/card.ts +50 -0
- package/src/components/css/checkbox/checkbox.css +114 -0
- package/src/components/css/checkbox/checkbox.ts +50 -0
- package/src/components/css/divider/divider.css +38 -0
- package/src/components/css/divider/divider.ts +42 -0
- package/src/components/css/error-text/error-text.css +21 -0
- package/src/components/css/error-text/error-text.ts +37 -0
- package/src/components/css/fieldset/fieldset.css +72 -0
- package/src/components/css/fieldset/fieldset.ts +87 -0
- package/src/components/css/file-upload/file-upload.css +102 -0
- package/src/components/css/file-upload/file-upload.ts +65 -0
- package/src/components/css/heading/heading.css +33 -0
- package/src/components/css/heading/heading.ts +42 -0
- package/src/components/css/help-text/help-text.css +24 -0
- package/src/components/css/help-text/help-text.ts +42 -0
- package/src/components/css/icon/icon.css +20 -0
- package/src/components/css/icon/icon.ts +44 -0
- package/src/components/css/input/input.css +145 -0
- package/src/components/css/input/input.ts +77 -0
- package/src/components/css/label/label.css +42 -0
- package/src/components/css/label/label.ts +51 -0
- package/src/components/css/link/link.css +38 -0
- package/src/components/css/link/link.ts +38 -0
- package/src/components/css/list/list.css +53 -0
- package/src/components/css/list/list.ts +44 -0
- package/src/components/css/pagination/pagination.css +95 -0
- package/src/components/css/pagination/pagination.ts +33 -0
- package/src/components/css/paragraph/paragraph.css +31 -0
- package/src/components/css/paragraph/paragraph.ts +53 -0
- package/src/components/css/progress/progress.css +161 -0
- package/src/components/css/progress/progress.ts +78 -0
- package/src/components/css/radio/radio.css +94 -0
- package/src/components/css/radio/radio.ts +50 -0
- package/src/components/css/search/search.css +50 -0
- package/src/components/css/search/search.ts +56 -0
- package/src/components/css/select/select.css +321 -0
- package/src/components/css/select/select.ts +63 -0
- package/src/components/css/shared.ts +199 -0
- package/src/components/css/skeleton/skeleton.css +62 -0
- package/src/components/css/skeleton/skeleton.ts +43 -0
- package/src/components/css/skip-link/skip-link.css +41 -0
- package/src/components/css/skip-link/skip-link.ts +22 -0
- package/src/components/css/spinner/spinner.css +48 -0
- package/src/components/css/spinner/spinner.ts +48 -0
- package/src/components/css/sr-only/sr-only.css +16 -0
- package/src/components/css/sr-only/sr-only.ts +21 -0
- package/src/components/css/switch/switch.css +105 -0
- package/src/components/css/switch/switch.ts +50 -0
- package/src/components/css/table/table.css +159 -0
- package/src/components/css/table/table.ts +107 -0
- package/src/components/css/tag/tag.css +58 -0
- package/src/components/css/tag/tag.ts +49 -0
- package/src/components/css/textarea/textarea.css +83 -0
- package/src/components/css/textarea/textarea.ts +40 -0
- package/src/components/css/toggle-group/toggle-group.css +92 -0
- package/src/components/css/toggle-group/toggle-group.ts +37 -0
- package/src/components/css/tooltip/tooltip.css +89 -0
- package/src/components/css/tooltip/tooltip.ts +37 -0
- package/src/components/default-texts.ts +34 -0
- package/src/components/events.ts +68 -0
- package/src/components/frame/dialog/dialog.css +306 -0
- package/src/components/frame/dialog/dialog.ts +166 -0
- package/src/components/frame/dialog/lt-dialog.ts +510 -0
- package/src/components/frame/error-summary/error-summary.css +47 -0
- package/src/components/frame/error-summary/error-summary.ts +56 -0
- package/src/components/frame/error-summary/lt-error-summary.ts +344 -0
- package/src/components/frame/field/field-core.ts +152 -0
- package/src/components/frame/field/field.css +17 -0
- package/src/components/frame/field/lt-field.ts +753 -0
- package/src/components/frame/popover/lt-popover.ts +555 -0
- package/src/components/frame/popover/popover.css +50 -0
- package/src/components/frame/popover/popover.ts +102 -0
- package/src/components/frame/session-timeout/lt-session-timeout.ts +681 -0
- package/src/components/frame/session-timeout/session-timeout.css +50 -0
- package/src/components/frame/session-timeout/session-timeout.ts +156 -0
- package/src/components/frame/suggestion/lt-suggestion.ts +1358 -0
- package/src/components/frame/suggestion/suggestion.css +132 -0
- package/src/components/frame/suggestion/suggestion.ts +172 -0
- package/src/components/frame/tabs/lt-tabs.ts +1319 -0
- package/src/components/frame/tabs/tabs.css +128 -0
- package/src/components/frame/tabs/tabs.ts +188 -0
- package/src/components/host-element.ts +400 -0
- package/src/components/standalone/connection-status/connection-status.css +41 -0
- package/src/components/standalone/connection-status/connection-status.ts +41 -0
- package/src/components/standalone/connection-status/lt-connection-status.ts +242 -0
- package/src/components/standalone/toast/lt-toast.ts +300 -0
- package/src/components/standalone/toast/toast.css +78 -0
- package/src/components/standalone/toast/toast.ts +70 -0
- package/src/diagnostics/core.ts +210 -0
- package/src/diagnostics/default-core.ts +81 -0
- package/src/diagnostics/index.ts +150 -0
- package/src/dom.ts +185 -0
- package/src/index.ts +218 -0
- package/src/jsx/react.ts +212 -0
- package/src/lt.ts +192 -0
- package/src/react.ts +411 -0
- package/src/register.ts +43 -0
- package/src/tailwind/tailwind.css +169 -0
- package/src/tokens/boundary.css +104 -0
- package/src/tokens/color-contract.json +231 -0
- package/src/tokens/color.ts +150 -0
- package/src/tokens/contract.ts +24 -0
- package/src/tokens/matrix-cells.ts +375 -0
- package/src/tokens/matrix.ts +32 -0
- package/src/tokens/theme-check.ts +51 -0
- package/src/tokens/theme.ts +104 -0
- package/src/tokens/tokens.css +569 -0
- package/src/tokens/tokens.ts +223 -0
- package/src/vocabulary/classes.ts +1322 -0
- package/src/vocabulary/elements.ts +412 -0
- package/src/vocabulary/types.ts +178 -0
- package/src/wasi-host.ts +155 -0
- package/web-types.json +800 -0
|
@@ -0,0 +1,1201 @@
|
|
|
1
|
+
import { defineElement, derivedParts, HostElement, isServerControlled, SERVER_CONTROLLED, setAttr, setFlag, uniqueId, warnAboutMarkup, } from "../../host-element.js";
|
|
2
|
+
export const LT_TABS_TAG = "lt-tabs";
|
|
3
|
+
/**
|
|
4
|
+
* The parts the tabs look for, as selectors relative to the element. The
|
|
5
|
+
* web component finds its parts with these, and the manifest describes them
|
|
6
|
+
* for anyone writing the markup by hand, so the two cannot disagree.
|
|
7
|
+
*/
|
|
8
|
+
export const LT_TABS_PARTS = {
|
|
9
|
+
/** The tab list: the one the server gave the role, or the class from `tabs.css`. */
|
|
10
|
+
list: "[role='tablist'], .lt-tabs__list",
|
|
11
|
+
/**
|
|
12
|
+
* A tab is a link to a panel in the tab list. A link is the only tab
|
|
13
|
+
* pattern that works without JavaScript: without the script the tab list is
|
|
14
|
+
* a table of contents, and the links lead to the panels. If the server has
|
|
15
|
+
* written the roles, only the links with `role="tab"` are tabs.
|
|
16
|
+
*/
|
|
17
|
+
tab: "a[role='tab'][href^='#'], .lt-tabs__list a[href^='#']:not([role])",
|
|
18
|
+
/** A panel: the role when it is there, otherwise the class without a role. */
|
|
19
|
+
panel: "[role='tabpanel'], .lt-tabs__panel:not([role])",
|
|
20
|
+
};
|
|
21
|
+
const LIST_SELECTOR = LT_TABS_PARTS.list;
|
|
22
|
+
const TAB_SELECTOR = LT_TABS_PARTS.tab;
|
|
23
|
+
/** What the server has called a tab, including what is not a link. */
|
|
24
|
+
const ANY_TAB_SELECTOR = "[role='tab']";
|
|
25
|
+
/** The heading in a panel, which gives the panel its name. */
|
|
26
|
+
const HEADING_SELECTOR = ":scope > :is(h1, h2, h3, h4, h5, h6)";
|
|
27
|
+
/**
|
|
28
|
+
* The id a tab points to, or `null` when it is not a link to a fragment.
|
|
29
|
+
* `href="#"` on its own points to nothing.
|
|
30
|
+
*
|
|
31
|
+
* `href` is the truth, and `aria-controls` is written from it. The fragment
|
|
32
|
+
* is looked up the way the browser does: as written, and otherwise decoded,
|
|
33
|
+
* so both `#attachments` and `#s%C3%B8knaden` find their panel.
|
|
34
|
+
*/
|
|
35
|
+
function linkTarget(tab) {
|
|
36
|
+
if (tab.localName !== "a")
|
|
37
|
+
return null;
|
|
38
|
+
const href = tab.getAttribute("href") ?? "";
|
|
39
|
+
if (!href.startsWith("#") || href.length < 2)
|
|
40
|
+
return null;
|
|
41
|
+
return href.slice(1);
|
|
42
|
+
}
|
|
43
|
+
function decoded(fragment) {
|
|
44
|
+
try {
|
|
45
|
+
const plain = decodeURIComponent(fragment);
|
|
46
|
+
return plain === fragment ? null : plain;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Text as a name is computed: whitespace collapsed, and trimmed. */
|
|
53
|
+
function flat(text) {
|
|
54
|
+
return text.replace(/\s+/g, " ").trim();
|
|
55
|
+
}
|
|
56
|
+
/** Is the element hidden from assistive technology: `hidden`, `aria-hidden` or CSS? */
|
|
57
|
+
function hiddenFromAT(element) {
|
|
58
|
+
if (element.getAttribute("aria-hidden") === "true")
|
|
59
|
+
return true;
|
|
60
|
+
if (element.hasAttribute("hidden"))
|
|
61
|
+
return true;
|
|
62
|
+
if (!(element instanceof HTMLElement) || !element.isConnected)
|
|
63
|
+
return false;
|
|
64
|
+
const style = getComputedStyle(element);
|
|
65
|
+
return style.display === "none" || style.visibility === "hidden";
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The text an element contributes to a name, as AccName 1.2 computes it from
|
|
69
|
+
* step 2.
|
|
70
|
+
*
|
|
71
|
+
* `aria-labelledby` (2B), `aria-label` on something inside, `alt` on an
|
|
72
|
+
* image, and the text, but not what is hidden from assistive technology.
|
|
73
|
+
* `textContent` included hidden text, such as "Messages (3)", and gave an
|
|
74
|
+
* empty name to a tab with only an icon and `aria-label`.
|
|
75
|
+
*
|
|
76
|
+
* - `includeHidden`: when a hidden element is pointed to directly with
|
|
77
|
+
* `aria-labelledby`, it is included anyway, with everything inside it (2A).
|
|
78
|
+
* - `inReference`: the node is inside an `aria-labelledby` that is already
|
|
79
|
+
* being followed, and then another one is not followed (2B).
|
|
80
|
+
*
|
|
81
|
+
* Text from CSS, `::before` and `::after` (2F.ii), is not included. LO-TEK's
|
|
82
|
+
* tabs do not use it, and reading `content` correctly is a lot of code for
|
|
83
|
+
* something nobody needs (decided by the maintainer on 23 October).
|
|
84
|
+
*/
|
|
85
|
+
function textOf(node, includeHidden = false, inReference = false) {
|
|
86
|
+
if (node.nodeType === Node.TEXT_NODE)
|
|
87
|
+
return node.textContent ?? "";
|
|
88
|
+
if (!(node instanceof Element))
|
|
89
|
+
return "";
|
|
90
|
+
if (!includeHidden && hiddenFromAT(node))
|
|
91
|
+
return "";
|
|
92
|
+
if (!inReference) {
|
|
93
|
+
const referenced = referencedText(node);
|
|
94
|
+
if (referenced !== "")
|
|
95
|
+
return ` ${referenced} `;
|
|
96
|
+
}
|
|
97
|
+
const label = node.getAttribute("aria-label");
|
|
98
|
+
if (label && flat(label) !== "")
|
|
99
|
+
return ` ${label} `;
|
|
100
|
+
if (node.localName === "img")
|
|
101
|
+
return ` ${node.getAttribute("alt") ?? ""} `;
|
|
102
|
+
let text = "";
|
|
103
|
+
for (const child of node.childNodes)
|
|
104
|
+
text += textOf(child, includeHidden, inReference);
|
|
105
|
+
return text;
|
|
106
|
+
}
|
|
107
|
+
/** The text from the elements `aria-labelledby` points to, or empty (AccName 2B). */
|
|
108
|
+
function referencedText(element) {
|
|
109
|
+
const ids = element.getAttribute("aria-labelledby");
|
|
110
|
+
if (!ids)
|
|
111
|
+
return "";
|
|
112
|
+
const root = element.getRootNode();
|
|
113
|
+
return flat(ids
|
|
114
|
+
.split(/\s+/)
|
|
115
|
+
.map((id) => (id === "" ? null : root.getElementById?.(id)))
|
|
116
|
+
.filter((e) => Boolean(e))
|
|
117
|
+
.map((e) => textOf(e, hiddenFromAT(e), true))
|
|
118
|
+
.join(" "));
|
|
119
|
+
}
|
|
120
|
+
/** The name of an element, as a screen reader computes it. */
|
|
121
|
+
function accessibleName(element) {
|
|
122
|
+
const referenced = referencedText(element);
|
|
123
|
+
if (referenced !== "")
|
|
124
|
+
return referenced;
|
|
125
|
+
const label = element.getAttribute("aria-label");
|
|
126
|
+
if (label && flat(label) !== "")
|
|
127
|
+
return flat(label);
|
|
128
|
+
let text = "";
|
|
129
|
+
for (const child of element.childNodes)
|
|
130
|
+
text += textOf(child);
|
|
131
|
+
if (flat(text) !== "")
|
|
132
|
+
return flat(text);
|
|
133
|
+
return flat(element.getAttribute("title") ?? "");
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The name a panel gets when it points to the element with `aria-labelledby`.
|
|
137
|
+
*
|
|
138
|
+
* Not the same as the element's name: inside an `aria-labelledby`, the
|
|
139
|
+
* element's `aria-labelledby` is not followed (AccName 2B), and a hidden
|
|
140
|
+
* element is included (2A). A heading with `aria-labelledby` pointing to
|
|
141
|
+
* "Attachments" and the text "Enclosures" gave the panel the name
|
|
142
|
+
* "Enclosures", while the web component thought it was "Attachments".
|
|
143
|
+
*/
|
|
144
|
+
function nameAsReferenced(element) {
|
|
145
|
+
return flat(textOf(element, hiddenFromAT(element), true));
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Keyboard behaviour in a tab list, and the wiring when the markup comes
|
|
149
|
+
* without it.
|
|
150
|
+
*
|
|
151
|
+
* The tabs are links to their panels, the way GOV.UK does it:
|
|
152
|
+
*
|
|
153
|
+
* ```html
|
|
154
|
+
* <lt-tabs>
|
|
155
|
+
* <ul class="lt-tabs__list" aria-label="Parts of the case">
|
|
156
|
+
* <li><a href="#application">Application</a></li>
|
|
157
|
+
* <li><a href="#attachments">Attachments</a></li>
|
|
158
|
+
* </ul>
|
|
159
|
+
* <section class="lt-tabs__panel" id="application"><h2>Application</h2>…</section>
|
|
160
|
+
* <section class="lt-tabs__panel" id="attachments"><h2>Attachments</h2>…</section>
|
|
161
|
+
* </lt-tabs>
|
|
162
|
+
* ```
|
|
163
|
+
*
|
|
164
|
+
* Without JavaScript this is a table of contents with all the panels one
|
|
165
|
+
* after another, and the links work. What the template writes is an id on
|
|
166
|
+
* the panels, and it needs the links anyway.
|
|
167
|
+
*
|
|
168
|
+
* The web component never writes an id on a tab or a panel. Datastar and
|
|
169
|
+
* idiomorph recognise an element by its id, and an element with an id the
|
|
170
|
+
* server did not send is replaced at the next update, along with the tab
|
|
171
|
+
* selection and focus. The panels' ids come from the server, so they are
|
|
172
|
+
* kept. What the web component fills in, it computes from the markup: the
|
|
173
|
+
* roles, `aria-controls` from `href`, `role="presentation"` on the list
|
|
174
|
+
* items, and the panel's name, which is the tab's. The heading in the panel
|
|
175
|
+
* is the name when it says the same as the tab. Only the selected panel is a
|
|
176
|
+
* tab stop.
|
|
177
|
+
*
|
|
178
|
+
* The panels that are not selected are hidden with `hidden="until-found"`, so
|
|
179
|
+
* find in page and a link to something in a panel reach them, and the
|
|
180
|
+
* browser fires `beforematch`, which the web component selects the tab from.
|
|
181
|
+
* If the server wrote `hidden` itself, it is written back in the server's
|
|
182
|
+
* form, across the whole tab list: `lt.tabs()` from
|
|
183
|
+
* `@lo-tek/interfaces/react` gives `hidden=""`, because React cannot write
|
|
184
|
+
* `until-found`. The address selects the tab when the page loads and when it
|
|
185
|
+
* changes, and each selection is written to the address, unless it already
|
|
186
|
+
* points to the selected panel or something in it. On load the server's
|
|
187
|
+
* `aria-selected` comes before the address, and under `server-controlled`
|
|
188
|
+
* the address never selects.
|
|
189
|
+
*
|
|
190
|
+
* What the server has written stays. Which tab is selected is read from
|
|
191
|
+
* `aria-selected`, and when that is missing, from which panel is not hidden;
|
|
192
|
+
* if nothing is said, it is the first. Before the web component is
|
|
193
|
+
* registered, `tabs.css` shows only the panel the web component is going to
|
|
194
|
+
* select: the first visible one, or the one the address points to, so the
|
|
195
|
+
* content does not jump when the rest are hidden.
|
|
196
|
+
*
|
|
197
|
+
* What the web component then does is what the browser does not do by
|
|
198
|
+
* itself: Tab skips past the whole tab list and into the panel, while the
|
|
199
|
+
* arrow keys move between the tabs. With `activation="manual"` the arrow keys
|
|
200
|
+
* only move focus, and Enter or Space selects.
|
|
201
|
+
*
|
|
202
|
+
* The tab selection belongs to the user, and the web component restores it
|
|
203
|
+
* when a patch strips it. If the server needs to move the tab, as in
|
|
204
|
+
* "continue to step 2", set `server-controlled` on the host, and then every
|
|
205
|
+
* patch decides.
|
|
206
|
+
*/
|
|
207
|
+
export class LtTabs extends HostElement {
|
|
208
|
+
constructor() {
|
|
209
|
+
super(...arguments);
|
|
210
|
+
/**
|
|
211
|
+
* Roles the web component set itself. Tells a bare tab list, where every
|
|
212
|
+
* link is a tab, from one where the server wrote the roles, where a link
|
|
213
|
+
* without a role is something else, such as a help link.
|
|
214
|
+
*/
|
|
215
|
+
this.roledByMe = new WeakSet();
|
|
216
|
+
this.selectedMemory = new WeakMap();
|
|
217
|
+
this.hiddenMemory = new WeakMap();
|
|
218
|
+
/**
|
|
219
|
+
* The form the server hides panels in, the last time it hid something:
|
|
220
|
+
* `""` from `lt.tabs()`, `"until-found"` or something else. The web
|
|
221
|
+
* component writes `hidden` in that form on every panel in the tab list,
|
|
222
|
+
* including a panel the server never hid. Otherwise one panel got
|
|
223
|
+
* `until-found` and the others `""` in the same tab list. If the server has
|
|
224
|
+
* never hidden anything, it is `until-found`, so find in page reaches the
|
|
225
|
+
* panel.
|
|
226
|
+
*/
|
|
227
|
+
this.hiddenForm = "until-found";
|
|
228
|
+
/**
|
|
229
|
+
* The id the web component gave the heading in a panel, per panel. A morph
|
|
230
|
+
* strips the id from the heading and `aria-labelledby` from the panel,
|
|
231
|
+
* since neither was in the server's HTML, and the panel must get the same
|
|
232
|
+
* one back, so a screen reader does not follow a reference that changes
|
|
233
|
+
* under it.
|
|
234
|
+
*/
|
|
235
|
+
this.headingIds = new WeakMap();
|
|
236
|
+
/** The name the web component wrote in `aria-label` on a panel without a heading. */
|
|
237
|
+
this.labelByMe = new WeakMap();
|
|
238
|
+
/**
|
|
239
|
+
* Whether the tab list has loaded: whether the address or the server's
|
|
240
|
+
* selection has been read. That happens the first time the tab list has
|
|
241
|
+
* tabs and the address can be resolved, see `sync()`, not when the element
|
|
242
|
+
* is connected. An `<lt-tabs>` the server fills in later otherwise always
|
|
243
|
+
* got the first tab.
|
|
244
|
+
*/
|
|
245
|
+
this.started = false;
|
|
246
|
+
this.handleClick = (event) => {
|
|
247
|
+
const index = this.tabIndexOf(event);
|
|
248
|
+
if (index < 0)
|
|
249
|
+
return;
|
|
250
|
+
const tab = this.tabs[index];
|
|
251
|
+
// With a modifier the browser opens the link somewhere else, and that is
|
|
252
|
+
// the user's choice, not a tab selection.
|
|
253
|
+
if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
// Without this the page jumped to the panel.
|
|
257
|
+
event.preventDefault();
|
|
258
|
+
if (this.isDisabled(tab))
|
|
259
|
+
return;
|
|
260
|
+
this.select(index);
|
|
261
|
+
};
|
|
262
|
+
/** Find in page or a link revealed a hidden panel. */
|
|
263
|
+
this.handleBeforematch = (event) => {
|
|
264
|
+
// Also under `server-controlled`, like a click: the selection stands until
|
|
265
|
+
// the server answers, and `tab-select` tells the app. Otherwise the web
|
|
266
|
+
// component hid the match again in the next round.
|
|
267
|
+
const panel = event.target;
|
|
268
|
+
const index = this.tabs.findIndex((tab) => this.panelFor(tab) === panel);
|
|
269
|
+
if (index < 0 || !(panel instanceof Element))
|
|
270
|
+
return;
|
|
271
|
+
// The browser removes `hidden` on the panel itself, see `revealing`.
|
|
272
|
+
this.revealing = panel;
|
|
273
|
+
try {
|
|
274
|
+
this.select(index);
|
|
275
|
+
}
|
|
276
|
+
finally {
|
|
277
|
+
// After an event from a script, the browser reveals nothing.
|
|
278
|
+
if (event.isTrusted)
|
|
279
|
+
setTimeout(() => this.revealed(panel), 0);
|
|
280
|
+
else
|
|
281
|
+
this.revealing = undefined;
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
this.handleHashchange = () => {
|
|
285
|
+
this.selectFromAddress();
|
|
286
|
+
};
|
|
287
|
+
this.handleKeydown = (event) => {
|
|
288
|
+
// Every keystroke in a field in a panel comes here. The cheap checks first.
|
|
289
|
+
if (!(event.target instanceof Element))
|
|
290
|
+
return;
|
|
291
|
+
if (!event.target.closest("[role='tab']"))
|
|
292
|
+
return;
|
|
293
|
+
const tabs = this.tabs;
|
|
294
|
+
const current = this.tabIndexOf(event);
|
|
295
|
+
if (current < 0)
|
|
296
|
+
return;
|
|
297
|
+
// Alt+Left is "back" in the browser, and Cmd+arrow moves through the
|
|
298
|
+
// history on macOS. With a modifier the key is not for the tabs.
|
|
299
|
+
if (event.altKey || event.ctrlKey || event.metaKey)
|
|
300
|
+
return;
|
|
301
|
+
// Space selects with manual activation. A link does nothing on Space by
|
|
302
|
+
// itself, and Enter sends a click, which `handleClick` handles.
|
|
303
|
+
if (this.manual && event.key === " ") {
|
|
304
|
+
event.preventDefault();
|
|
305
|
+
this.select(current);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
// On a page read from right to left the next tab is to the left. Without
|
|
309
|
+
// this the right arrow moved focus visually backwards.
|
|
310
|
+
const rtl = getComputedStyle(this).direction === "rtl";
|
|
311
|
+
// Up and down arrows stole page scrolling in a horizontal tab list and
|
|
312
|
+
// departed from WAI-ARIA. They apply only when the tab list says it is
|
|
313
|
+
// vertical.
|
|
314
|
+
const vertical = tabs[current]
|
|
315
|
+
.closest("[role='tablist']")
|
|
316
|
+
?.getAttribute("aria-orientation") === "vertical";
|
|
317
|
+
const steps = vertical
|
|
318
|
+
? { ArrowDown: 1, ArrowUp: -1 }
|
|
319
|
+
: { ArrowRight: rtl ? -1 : 1, ArrowLeft: rtl ? 1 : -1 };
|
|
320
|
+
let next;
|
|
321
|
+
if (event.key in steps) {
|
|
322
|
+
next = this.nextEnabled(current, steps[event.key]);
|
|
323
|
+
}
|
|
324
|
+
else if (event.key === "Home") {
|
|
325
|
+
next = this.nextEnabled(-1, 1);
|
|
326
|
+
}
|
|
327
|
+
else if (event.key === "End") {
|
|
328
|
+
next = this.nextEnabled(tabs.length, -1);
|
|
329
|
+
}
|
|
330
|
+
if (next === undefined)
|
|
331
|
+
return;
|
|
332
|
+
event.preventDefault();
|
|
333
|
+
if (!this.manual)
|
|
334
|
+
this.select(next);
|
|
335
|
+
tabs[next].focus();
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
connectedCallback() {
|
|
339
|
+
/*
|
|
340
|
+
* The attributes are included, not just the children. A morph strips what
|
|
341
|
+
* is not in the server's HTML, and the tab selection is exactly that:
|
|
342
|
+
* something the user did after the page arrived. Every write compares
|
|
343
|
+
* first, or the observer would trigger itself.
|
|
344
|
+
*/
|
|
345
|
+
// One listener on the host, not one per tab. Capture phase, so a listener
|
|
346
|
+
// on the tab list that stops the event does not take clicks and arrow
|
|
347
|
+
// keys from the tabs.
|
|
348
|
+
this.addEventListener("click", this.handleClick, true);
|
|
349
|
+
this.addEventListener("keydown", this.handleKeydown, true);
|
|
350
|
+
// Find in page, or a link to something in a panel hidden with
|
|
351
|
+
// `until-found`. On the document, not on the host: a panel can be outside
|
|
352
|
+
// `<lt-tabs>`, and then the event never reaches the host.
|
|
353
|
+
document.addEventListener("beforematch", this.handleBeforematch, true);
|
|
354
|
+
const root = this.getRootNode();
|
|
355
|
+
if (root !== document) {
|
|
356
|
+
this.root = root;
|
|
357
|
+
root.addEventListener("beforematch", this.handleBeforematch, true);
|
|
358
|
+
}
|
|
359
|
+
window.addEventListener("hashchange", this.handleHashchange);
|
|
360
|
+
this.observer = new MutationObserver((records) => {
|
|
361
|
+
if (this.concerns(records))
|
|
362
|
+
this.sync();
|
|
363
|
+
});
|
|
364
|
+
this.observer.observe(this, {
|
|
365
|
+
childList: true,
|
|
366
|
+
subtree: true,
|
|
367
|
+
attributes: true,
|
|
368
|
+
// The panel's name is the tab's. idiomorph changes the text in a text
|
|
369
|
+
// node, with nothing else the observer sees.
|
|
370
|
+
characterData: true,
|
|
371
|
+
// `disabled` is included because the web component reads it: if a patch
|
|
372
|
+
// disables the selected tab, the tab stop moves to one that can take
|
|
373
|
+
// focus. `alt`, `aria-hidden` and `title` change the name of a tab.
|
|
374
|
+
attributeFilter: [
|
|
375
|
+
"aria-selected",
|
|
376
|
+
"tabindex",
|
|
377
|
+
"hidden",
|
|
378
|
+
"disabled",
|
|
379
|
+
"aria-disabled",
|
|
380
|
+
// The wiring the web component sets when the markup came without it,
|
|
381
|
+
// and restores when a patch removes it.
|
|
382
|
+
"role",
|
|
383
|
+
"id",
|
|
384
|
+
"href",
|
|
385
|
+
"aria-controls",
|
|
386
|
+
"aria-labelledby",
|
|
387
|
+
"aria-label",
|
|
388
|
+
"alt",
|
|
389
|
+
"aria-hidden",
|
|
390
|
+
"title",
|
|
391
|
+
],
|
|
392
|
+
});
|
|
393
|
+
this.sync();
|
|
394
|
+
}
|
|
395
|
+
attributeChangedCallback(name) {
|
|
396
|
+
// `server-controlled` switched on midway: the web component lets go, and
|
|
397
|
+
// the next patch decides.
|
|
398
|
+
if (name === SERVER_CONTROLLED && isServerControlled(this)) {
|
|
399
|
+
this.chosenTab = undefined;
|
|
400
|
+
// What is there now is the starting point. Without this the tab list
|
|
401
|
+
// jumped back to the tab the server last named, before the user chose
|
|
402
|
+
// another. But as when the server answers a click: `aria-selected` the
|
|
403
|
+
// web component wrote is not the server's. When everything counted as
|
|
404
|
+
// the server's, it took precedence over `hidden`, and a server that
|
|
405
|
+
// moves the tab with `hidden` alone was not heard.
|
|
406
|
+
this.releasePending();
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
disconnectedCallback() {
|
|
410
|
+
this.observer?.disconnect();
|
|
411
|
+
this.observer = undefined;
|
|
412
|
+
this.removeEventListener("click", this.handleClick, true);
|
|
413
|
+
this.removeEventListener("keydown", this.handleKeydown, true);
|
|
414
|
+
document.removeEventListener("beforematch", this.handleBeforematch, true);
|
|
415
|
+
this.root?.removeEventListener("beforematch", this.handleBeforematch, true);
|
|
416
|
+
this.root = undefined;
|
|
417
|
+
window.removeEventListener("hashchange", this.handleHashchange);
|
|
418
|
+
// If the element is connected again, the address is read again.
|
|
419
|
+
this.started = false;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* The tabs that belong to this tab list, and only those.
|
|
423
|
+
*
|
|
424
|
+
* Tabs inside tabs are ordinary markup. Without the boundary an inner tab
|
|
425
|
+
* list became part of the outer one: a click on an inner tab hid the outer
|
|
426
|
+
* panel it was in, and both web components attached listeners to the same
|
|
427
|
+
* tabs.
|
|
428
|
+
*/
|
|
429
|
+
get tabs() {
|
|
430
|
+
return derivedParts(this, TAB_SELECTOR, "tab", this.roledByMe).filter((tab) => linkTarget(tab) !== null);
|
|
431
|
+
}
|
|
432
|
+
/** The server's word about an attribute, against what the web component last wrote. */
|
|
433
|
+
serverWord(memory, element, now) {
|
|
434
|
+
const known = memory.get(element);
|
|
435
|
+
if (!known)
|
|
436
|
+
return now;
|
|
437
|
+
return now !== known.written ? now : known.server;
|
|
438
|
+
}
|
|
439
|
+
/** Is there something on a tab or a panel other than what the web component wrote? */
|
|
440
|
+
touchedByOthers() {
|
|
441
|
+
return this.tabs.some((tab) => {
|
|
442
|
+
const selected = this.selectedMemory.get(tab);
|
|
443
|
+
if (!selected || tab.getAttribute("aria-selected") !== selected.written) {
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
446
|
+
const panel = this.panelFor(tab);
|
|
447
|
+
if (!panel)
|
|
448
|
+
return false;
|
|
449
|
+
const hidden = this.hiddenMemory.get(panel);
|
|
450
|
+
return !hidden || panel.getAttribute("hidden") !== hidden.written;
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Releases the click, and makes what is in the markup now the server's
|
|
455
|
+
* word.
|
|
456
|
+
*
|
|
457
|
+
* The memory holds the server's word from before the click, and it is
|
|
458
|
+
* stale: the server has answered. An attribute the server wrote the same as
|
|
459
|
+
* the web component cannot be told from one it did not touch, so the old
|
|
460
|
+
* words won, and the tab list jumped back to the tab from before the click
|
|
461
|
+
* just as the server confirmed the new one.
|
|
462
|
+
*
|
|
463
|
+
* `hidden` on the panels is therefore read as it is. `aria-selected` is read
|
|
464
|
+
* as it is when the server has touched at least one tab, that is, when the
|
|
465
|
+
* server speaks through `aria-selected`. If it has not, the values are the
|
|
466
|
+
* ones the web component wrote for the click, and then the panels say which
|
|
467
|
+
* tab applies.
|
|
468
|
+
*/
|
|
469
|
+
releasePending() {
|
|
470
|
+
this.pendingTab = undefined;
|
|
471
|
+
const tabs = this.tabs;
|
|
472
|
+
const serverSpoke = tabs.some((tab) => {
|
|
473
|
+
const now = tab.getAttribute("aria-selected");
|
|
474
|
+
const known = this.selectedMemory.get(tab);
|
|
475
|
+
// A new, bare tab has said nothing. Had it counted, the click's
|
|
476
|
+
// `aria-selected` was read as the server's, and a later move with
|
|
477
|
+
// `hidden` alone was undone.
|
|
478
|
+
return known ? now !== known.written : now !== null;
|
|
479
|
+
});
|
|
480
|
+
this.hiddenMemory = new WeakMap();
|
|
481
|
+
if (serverSpoke) {
|
|
482
|
+
this.selectedMemory = new WeakMap();
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
for (const tab of tabs) {
|
|
486
|
+
this.selectedMemory.set(tab, {
|
|
487
|
+
written: tab.getAttribute("aria-selected"),
|
|
488
|
+
server: null,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
serverSelected(tab) {
|
|
493
|
+
return this.serverWord(this.selectedMemory, tab, tab.getAttribute("aria-selected"));
|
|
494
|
+
}
|
|
495
|
+
/** The server's word about `hidden` on the panel: the form, or `null` for visible. */
|
|
496
|
+
serverHidden(panel) {
|
|
497
|
+
return this.serverWord(this.hiddenMemory, panel, panel.getAttribute("hidden"));
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Can any of the changes concern the tab list?
|
|
501
|
+
*
|
|
502
|
+
* The observer sees the whole tree, and a suggestion field, a counter or an
|
|
503
|
+
* inner tab list in a panel changes `aria-selected`, `hidden` and text on
|
|
504
|
+
* every keystroke. Each of them gave a full round, with styles read for the
|
|
505
|
+
* names. Only what is deep inside one of the tab list's panels is skipped,
|
|
506
|
+
* and only when it is neither a heading in the panel, something
|
|
507
|
+
* `aria-labelledby` points to, nor an id, which can make an element
|
|
508
|
+
* something a tab points to. Everything else gives a round, as before.
|
|
509
|
+
*/
|
|
510
|
+
concerns(records) {
|
|
511
|
+
let panels;
|
|
512
|
+
let names;
|
|
513
|
+
for (const record of records) {
|
|
514
|
+
if (record.type === "attributes" && record.attributeName === "id")
|
|
515
|
+
return true;
|
|
516
|
+
const target = record.target instanceof Element
|
|
517
|
+
? record.target
|
|
518
|
+
: record.target.parentElement;
|
|
519
|
+
if (!target)
|
|
520
|
+
return true;
|
|
521
|
+
panels ??= this.tabs.flatMap((tab) => this.panelFor(tab) ?? []);
|
|
522
|
+
const panel = panels.find((p) => p !== target && p.contains(target));
|
|
523
|
+
if (!panel)
|
|
524
|
+
return true;
|
|
525
|
+
const heading = target.closest("h1, h2, h3, h4, h5, h6");
|
|
526
|
+
if (heading && heading.parentElement === panel)
|
|
527
|
+
return true;
|
|
528
|
+
names ??= this.watchedIds(panels);
|
|
529
|
+
if (this.within(target, names))
|
|
530
|
+
return true;
|
|
531
|
+
if (record.type === "childList") {
|
|
532
|
+
for (const node of [...record.addedNodes, ...record.removedNodes])
|
|
533
|
+
if (this.carries(node, names))
|
|
534
|
+
return true;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* The ids `aria-labelledby` points to from the tabs, what is inside them,
|
|
541
|
+
* and the panels. Before the tab list has loaded, also the id the address
|
|
542
|
+
* points to: the tab list waits for that element, see `sync()`, and it can
|
|
543
|
+
* arrive deep inside a panel.
|
|
544
|
+
*/
|
|
545
|
+
watchedIds(panels) {
|
|
546
|
+
const ids = new Set();
|
|
547
|
+
if (!this.started) {
|
|
548
|
+
const hash = location.hash.slice(1);
|
|
549
|
+
if (hash !== "")
|
|
550
|
+
ids.add(hash);
|
|
551
|
+
const plain = decoded(hash);
|
|
552
|
+
if (plain !== null)
|
|
553
|
+
ids.add(plain);
|
|
554
|
+
}
|
|
555
|
+
const from = (element) => {
|
|
556
|
+
for (const id of (element.getAttribute("aria-labelledby") ?? "").split(/\s+/))
|
|
557
|
+
if (id !== "")
|
|
558
|
+
ids.add(id);
|
|
559
|
+
};
|
|
560
|
+
for (const tab of this.tabs) {
|
|
561
|
+
from(tab);
|
|
562
|
+
for (const inner of tab.querySelectorAll("[aria-labelledby]"))
|
|
563
|
+
from(inner);
|
|
564
|
+
}
|
|
565
|
+
for (const panel of panels)
|
|
566
|
+
from(panel);
|
|
567
|
+
return ids;
|
|
568
|
+
}
|
|
569
|
+
/** Is the element inside an element with one of the ids, or is it one itself? */
|
|
570
|
+
within(element, ids) {
|
|
571
|
+
if (ids.size === 0)
|
|
572
|
+
return false;
|
|
573
|
+
for (let e = element; e; e = e.parentElement)
|
|
574
|
+
if (e.id && ids.has(e.id))
|
|
575
|
+
return true;
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
/** Does an added or removed node carry one of the ids, itself or inside it? */
|
|
579
|
+
carries(node, ids) {
|
|
580
|
+
if (!(node instanceof Element) || ids.size === 0)
|
|
581
|
+
return false;
|
|
582
|
+
if (node.id && ids.has(node.id))
|
|
583
|
+
return true;
|
|
584
|
+
for (const inner of node.querySelectorAll("[id]"))
|
|
585
|
+
if (ids.has(inner.id))
|
|
586
|
+
return true;
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
/** The tab list the tabs are in, when it belongs to this web component. */
|
|
590
|
+
get list() {
|
|
591
|
+
// The first tab list that belongs to this web component, not the first in
|
|
592
|
+
// the tree: when the panels with an inner tab list come before the outer
|
|
593
|
+
// tab list, the inner one was first.
|
|
594
|
+
for (const list of this.querySelectorAll(LIST_SELECTOR))
|
|
595
|
+
if (list.closest(this.localName) === this)
|
|
596
|
+
return list;
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* The panel a tab controls, through the id it points to.
|
|
601
|
+
*
|
|
602
|
+
* The panel can be outside the host. It is then not in what the web
|
|
603
|
+
* component observes, so if a patch strips `hidden` from it, it does not
|
|
604
|
+
* come back by itself. If the tab points to nothing, the answer is nothing,
|
|
605
|
+
* and the web component warns. The order is not a fallback: two tabs could
|
|
606
|
+
* then have got the same panel, and the two wrote opposite `hidden` on it in
|
|
607
|
+
* every single round.
|
|
608
|
+
*/
|
|
609
|
+
panelFor(tab) {
|
|
610
|
+
const target = linkTarget(tab);
|
|
611
|
+
return target === null ? null : this.byFragment(target);
|
|
612
|
+
}
|
|
613
|
+
/** The element a fragment points to, as written or decoded. */
|
|
614
|
+
byFragment(fragment) {
|
|
615
|
+
const root = this.getRootNode();
|
|
616
|
+
const found = root.getElementById?.(fragment);
|
|
617
|
+
if (found)
|
|
618
|
+
return found;
|
|
619
|
+
// A fragment can be encoded, such as `#s%C3%B8knaden`.
|
|
620
|
+
const plain = decoded(fragment);
|
|
621
|
+
return plain === null ? null : (root.getElementById?.(plain) ?? null);
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* A tab that cannot be selected, with either mouse or keyboard: disabled,
|
|
625
|
+
* or hidden with `hidden`. A hidden tab cannot take focus, so when an arrow
|
|
626
|
+
* key selected it, the tab stop was left on something Tab never reaches.
|
|
627
|
+
*/
|
|
628
|
+
isDisabled(tab) {
|
|
629
|
+
return (tab.hasAttribute("disabled") ||
|
|
630
|
+
tab.getAttribute("aria-disabled") === "true" ||
|
|
631
|
+
tab.hasAttribute("hidden"));
|
|
632
|
+
}
|
|
633
|
+
/** The arrow keys only move focus, and Enter or Space selects. */
|
|
634
|
+
get manual() {
|
|
635
|
+
return this.getAttribute("activation") === "manual";
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* The index of the marked tab, or `-1` when none is.
|
|
639
|
+
*
|
|
640
|
+
* `selected` answers 0 in that case, because it is public API and a tab
|
|
641
|
+
* list in practice always has a selected tab. But that lie cannot be used
|
|
642
|
+
* internally: when it said 0 with nothing marked, `select(0)` returned at
|
|
643
|
+
* once, and the tab list did not respond to a click at all.
|
|
644
|
+
*/
|
|
645
|
+
get markedIndex() {
|
|
646
|
+
return this.tabs.findIndex((tab) => tab.getAttribute("aria-selected") === "true");
|
|
647
|
+
}
|
|
648
|
+
/** The index of the selected tab. */
|
|
649
|
+
get selected() {
|
|
650
|
+
const index = this.markedIndex;
|
|
651
|
+
return index < 0 ? 0 : index;
|
|
652
|
+
}
|
|
653
|
+
set selected(index) {
|
|
654
|
+
// React 19 writes the property when it exists, and the documentation
|
|
655
|
+
// shows `tabs.selected` as API. A getter alone threw on assignment.
|
|
656
|
+
this.select(index);
|
|
657
|
+
}
|
|
658
|
+
sync() {
|
|
659
|
+
/*
|
|
660
|
+
* The first time the tab list has tabs, it has loaded. If the server has
|
|
661
|
+
* said which tab is selected, that applies: the server cannot see the
|
|
662
|
+
* address, and the React entry's `lt.tabs()` writes `aria-selected`.
|
|
663
|
+
* When the web component selected from the address before React
|
|
664
|
+
* hydrated, React reported a mismatch for every attribute it changed.
|
|
665
|
+
* Otherwise the address selects. The server's word is read before the
|
|
666
|
+
* web component writes anything.
|
|
667
|
+
*/
|
|
668
|
+
// Loaded when the tab list has tabs and the address can be resolved: it
|
|
669
|
+
// is empty, or the element it points to exists. When the tab list came
|
|
670
|
+
// before the panels, or the content after the panel, the address
|
|
671
|
+
// otherwise found nothing, and the first tab was selected over the one
|
|
672
|
+
// the user asked for. Waiting for every tab to have a panel was not
|
|
673
|
+
// enough: with one link pointing to nothing, the tab list never loaded.
|
|
674
|
+
// If the address points to something that never comes, such as
|
|
675
|
+
// `#/case/1`, there is nothing to select either. The server's word is read
|
|
676
|
+
// through the memory, so a new connection does not take the web
|
|
677
|
+
// component's writes for the server's.
|
|
678
|
+
const tabs = this.tabs;
|
|
679
|
+
const hash = location.hash.slice(1);
|
|
680
|
+
const loading = !this.started &&
|
|
681
|
+
tabs.length > 0 &&
|
|
682
|
+
(hash === "" || this.byFragment(hash) !== null);
|
|
683
|
+
const serverChose = loading && tabs.some((tab) => this.serverSelected(tab) === "true");
|
|
684
|
+
this.wire();
|
|
685
|
+
this.bind();
|
|
686
|
+
this.repair();
|
|
687
|
+
if (!loading)
|
|
688
|
+
return;
|
|
689
|
+
this.started = true;
|
|
690
|
+
if (serverChose)
|
|
691
|
+
this.alignAddress(this.markedIndex);
|
|
692
|
+
else
|
|
693
|
+
this.selectFromAddress();
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Fills in the wiring where the markup came without it.
|
|
697
|
+
*
|
|
698
|
+
* Only what is missing, with one exception: `aria-controls` is written from
|
|
699
|
+
* `href`, since the two are the same information and `href` is the one that
|
|
700
|
+
* works without JavaScript. Any other attribute the server has written is
|
|
701
|
+
* the server's word, even when it says something other than the web
|
|
702
|
+
* component would have said.
|
|
703
|
+
*/
|
|
704
|
+
wire() {
|
|
705
|
+
const list = this.list;
|
|
706
|
+
if (list && !list.hasAttribute("role"))
|
|
707
|
+
setAttr(list, "role", "tablist");
|
|
708
|
+
// In a list each list item is only a wrapper around the tab. Without the
|
|
709
|
+
// role a screen reader says "list, 2 items" before each tab.
|
|
710
|
+
if (list && (list.localName === "ul" || list.localName === "ol")) {
|
|
711
|
+
for (const item of list.children) {
|
|
712
|
+
if (item.localName === "li" && !item.hasAttribute("role")) {
|
|
713
|
+
setAttr(item, "role", "presentation");
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
/*
|
|
718
|
+
* Each panel gets its name once, from the first tab that points to it.
|
|
719
|
+
* When two tabs pointed to the same one, through a mistake in the markup,
|
|
720
|
+
* each of them gave the panel its name in every round, and the observer
|
|
721
|
+
* called itself forever.
|
|
722
|
+
*/
|
|
723
|
+
const named = new Set();
|
|
724
|
+
for (const tab of this.tabs) {
|
|
725
|
+
if (!tab.hasAttribute("role")) {
|
|
726
|
+
setAttr(tab, "role", "tab");
|
|
727
|
+
this.roledByMe.add(tab);
|
|
728
|
+
}
|
|
729
|
+
const panel = this.panelFor(tab);
|
|
730
|
+
setAttr(tab, "aria-controls", panel?.id ?? linkTarget(tab));
|
|
731
|
+
if (!panel)
|
|
732
|
+
continue;
|
|
733
|
+
if (!panel.hasAttribute("role")) {
|
|
734
|
+
setAttr(panel, "role", "tabpanel");
|
|
735
|
+
this.roledByMe.add(panel);
|
|
736
|
+
}
|
|
737
|
+
if (named.has(panel))
|
|
738
|
+
continue;
|
|
739
|
+
named.add(panel);
|
|
740
|
+
this.name(panel, tab);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Gives the panel a name, when the server has not given it one.
|
|
745
|
+
*
|
|
746
|
+
* The panel is named what the tab is named, as the APG does it. The APG
|
|
747
|
+
* lets the panel point to the tab, but the tab has no id here, and the web
|
|
748
|
+
* component writes none. If the heading in the panel says the same as the
|
|
749
|
+
* tab, the heading is the name, and it can get an id from the web
|
|
750
|
+
* component: it has no state the user can lose. Otherwise the name is the
|
|
751
|
+
* tab's, as `aria-label`.
|
|
752
|
+
*
|
|
753
|
+
* An `aria-label` or `aria-labelledby` the server wrote is never changed,
|
|
754
|
+
* even when the reference points to something that does not exist yet.
|
|
755
|
+
* Until it arrives, the tab's name stands as `aria-label`: a reference
|
|
756
|
+
* without a match is ignored when the name is computed, and then
|
|
757
|
+
* `aria-label` applies.
|
|
758
|
+
*/
|
|
759
|
+
name(panel, tab) {
|
|
760
|
+
const ownLabel = this.labelByMe.get(panel);
|
|
761
|
+
const label = panel.getAttribute("aria-label");
|
|
762
|
+
if (label !== null && label !== ownLabel)
|
|
763
|
+
return;
|
|
764
|
+
const tabName = accessibleName(tab);
|
|
765
|
+
const named = panel.getAttribute("aria-labelledby");
|
|
766
|
+
const remembered = this.headingIds.get(panel);
|
|
767
|
+
if (named !== null && named !== remembered) {
|
|
768
|
+
if (this.resolves(named))
|
|
769
|
+
this.dropOwnLabel(panel);
|
|
770
|
+
else
|
|
771
|
+
this.setOwnLabel(panel, tabName);
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
// Without a name on the tab there is nothing to give the panel. An empty
|
|
775
|
+
// `aria-label` would also have taken the name from the heading.
|
|
776
|
+
if (tabName === "") {
|
|
777
|
+
this.dropOwnLabel(panel);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
const heading = panel.querySelector(HEADING_SELECTOR);
|
|
781
|
+
if (heading && nameAsReferenced(heading) === tabName) {
|
|
782
|
+
this.dropOwnLabel(panel);
|
|
783
|
+
// If the reference is the web component's, and the heading lost its id
|
|
784
|
+
// in a morph, it gets the same one back.
|
|
785
|
+
let id = heading.id;
|
|
786
|
+
if (!id) {
|
|
787
|
+
id = remembered ?? uniqueId("lt-tabs-heading");
|
|
788
|
+
setAttr(heading, "id", id);
|
|
789
|
+
}
|
|
790
|
+
this.headingIds.set(panel, id);
|
|
791
|
+
setAttr(panel, "aria-labelledby", id);
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
if (named !== null) {
|
|
795
|
+
setAttr(panel, "aria-labelledby", null);
|
|
796
|
+
this.headingIds.delete(panel);
|
|
797
|
+
}
|
|
798
|
+
this.setOwnLabel(panel, tabName);
|
|
799
|
+
}
|
|
800
|
+
/** Writes the tab's name as `aria-label`, and remembers that the web component wrote it. */
|
|
801
|
+
setOwnLabel(panel, text) {
|
|
802
|
+
setAttr(panel, "aria-label", text);
|
|
803
|
+
this.labelByMe.set(panel, text);
|
|
804
|
+
}
|
|
805
|
+
/** Removes the fallback, when a name that exists has taken over. */
|
|
806
|
+
dropOwnLabel(panel) {
|
|
807
|
+
if (!this.labelByMe.has(panel))
|
|
808
|
+
return;
|
|
809
|
+
setAttr(panel, "aria-label", null);
|
|
810
|
+
this.labelByMe.delete(panel);
|
|
811
|
+
}
|
|
812
|
+
/** Does at least one of the ids point to an element that exists? */
|
|
813
|
+
resolves(ids) {
|
|
814
|
+
const root = this.getRootNode();
|
|
815
|
+
return ids
|
|
816
|
+
.split(/\s+/)
|
|
817
|
+
.some((id) => id !== "" && (root.getElementById?.(id) ?? null) !== null);
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* The tab the server says is selected, when the user has not selected
|
|
821
|
+
* anything.
|
|
822
|
+
*
|
|
823
|
+
* The server's word, not what is there: what the web component wrote does
|
|
824
|
+
* not count, see `Word`. `aria-selected` first. When that is missing, as in
|
|
825
|
+
* markup written without JavaScript, `hidden` on the panels says the same:
|
|
826
|
+
* the first panel the server shows belongs to the selected tab. If the
|
|
827
|
+
* markup says nothing, or hides every panel, it is the first, which is what
|
|
828
|
+
* a tab list starts on anyway.
|
|
829
|
+
*/
|
|
830
|
+
get derivedIndex() {
|
|
831
|
+
const tabs = this.tabs;
|
|
832
|
+
const marked = tabs.findIndex((tab) => this.serverSelected(tab) === "true");
|
|
833
|
+
if (marked >= 0)
|
|
834
|
+
return marked;
|
|
835
|
+
const shown = tabs.findIndex((tab) => {
|
|
836
|
+
const panel = this.panelFor(tab);
|
|
837
|
+
return panel !== null && this.serverHidden(panel) === null;
|
|
838
|
+
});
|
|
839
|
+
return shown < 0 ? 0 : shown;
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Restores the user's selection after a patch.
|
|
843
|
+
*
|
|
844
|
+
* Without this the template had to write
|
|
845
|
+
* `data-preserve-attr="aria-selected tabindex"` on every tab and `hidden` on
|
|
846
|
+
* every panel, that is, list the attributes the web component was going to
|
|
847
|
+
* touch. No compiler looked at that list.
|
|
848
|
+
*
|
|
849
|
+
* `apply()` is called whether or not the tabs look right, because a patch
|
|
850
|
+
* may have touched only the panels. That is the narrow patch the
|
|
851
|
+
* documentation itself recommends, and a check on `aria-selected` alone did
|
|
852
|
+
* not see it: the tab said one thing and the screen another.
|
|
853
|
+
*/
|
|
854
|
+
repair() {
|
|
855
|
+
if (this.pendingTab) {
|
|
856
|
+
const index = this.tabs.indexOf(this.pendingTab);
|
|
857
|
+
if (isServerControlled(this) && index >= 0 && !this.touchedByOthers()) {
|
|
858
|
+
this.apply(index);
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
this.releasePending();
|
|
862
|
+
}
|
|
863
|
+
if (!this.chosenTab || isServerControlled(this)) {
|
|
864
|
+
/*
|
|
865
|
+
* No user selection to restore, so the server's word decides, and it
|
|
866
|
+
* is written out in full: `aria-selected`, the tab stop and `hidden` in
|
|
867
|
+
* step. Bare structure without `aria-selected` thus gets the first tab
|
|
868
|
+
* selected and the other panels hidden, and a patch that only touched
|
|
869
|
+
* the panels also moves the selection. That applies under
|
|
870
|
+
* `server-controlled` too: the web component does not restore the
|
|
871
|
+
* user's selection there, but still writes out what the server's
|
|
872
|
+
* markup says. If everything already matches, `apply()` writes nothing.
|
|
873
|
+
*/
|
|
874
|
+
if (this.tabs.length > 0) {
|
|
875
|
+
const index = this.derivedIndex;
|
|
876
|
+
this.apply(index);
|
|
877
|
+
if (this.started)
|
|
878
|
+
this.alignAddress(index);
|
|
879
|
+
}
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
const index = this.tabs.indexOf(this.chosenTab);
|
|
883
|
+
if (index < 0) {
|
|
884
|
+
/*
|
|
885
|
+
* The tab no longer exists. The server has sent something else, and
|
|
886
|
+
* then the server's markup applies.
|
|
887
|
+
*
|
|
888
|
+
* With one exception: the markup must hang together afterwards. When
|
|
889
|
+
* the patch removed the marked tab, the tab list is left without a
|
|
890
|
+
* single `aria-selected="true"`, every panel is hidden, and a click
|
|
891
|
+
* does nothing. The web component then selects what the server's
|
|
892
|
+
* markup says, the same way as when the user had not selected
|
|
893
|
+
* anything: `aria-selected`, then the first panel the server shows,
|
|
894
|
+
* then the first tab.
|
|
895
|
+
*
|
|
896
|
+
* The selection is announced with an event, because it was made by the
|
|
897
|
+
* web component and not by the user. Without the event an app that
|
|
898
|
+
* loads the panel's content or writes the selection to the address
|
|
899
|
+
* would never learn that it moved.
|
|
900
|
+
*/
|
|
901
|
+
const lostPanel = this.panelFor(this.chosenTab);
|
|
902
|
+
this.chosenTab = undefined;
|
|
903
|
+
// The server marked a tab itself. Then that applies, and the panels and
|
|
904
|
+
// the tab stop are written to match it, even when nothing else gave the
|
|
905
|
+
// observer another round.
|
|
906
|
+
if (this.markedIndex >= 0) {
|
|
907
|
+
const marked = this.derivedIndex;
|
|
908
|
+
this.apply(marked);
|
|
909
|
+
this.alignAddress(marked);
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
if (this.tabs.length > 0) {
|
|
913
|
+
// The address says which tab the user was on, and is the only thing
|
|
914
|
+
// that survives the links being replaced. If it is the same panel as
|
|
915
|
+
// before, the selection has not changed, only the links, and then
|
|
916
|
+
// nothing is announced.
|
|
917
|
+
const fromAddress = this.addressIndex;
|
|
918
|
+
const index = fromAddress >= 0 ? fromAddress : this.derivedIndex;
|
|
919
|
+
const tab = this.tabs[index];
|
|
920
|
+
this.apply(index);
|
|
921
|
+
if (this.panelFor(tab) === lostPanel) {
|
|
922
|
+
this.chosenTab = tab;
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
if (fromAddress >= 0)
|
|
926
|
+
this.chosenTab = tab;
|
|
927
|
+
this.showInAddress(index);
|
|
928
|
+
this.notify(index);
|
|
929
|
+
}
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
this.apply(index);
|
|
933
|
+
}
|
|
934
|
+
bind() {
|
|
935
|
+
const tabs = this.tabs;
|
|
936
|
+
if (tabs.length === 0) {
|
|
937
|
+
warnAboutMarkup(this, "found no tabs. The tab list needs a .lt-tabs__list with links " +
|
|
938
|
+
'to the panels, such as <a href="#attachments">; without them a ' +
|
|
939
|
+
'screen reader says "link" where it should say "tab, 2 of 3, ' +
|
|
940
|
+
'selected", and the arrow keys do nothing.',
|
|
941
|
+
// An empty `<lt-tabs>` is a region the server has not filled yet,
|
|
942
|
+
// and that is not a mistake in the markup.
|
|
943
|
+
() => this.childElementCount > 0 && this.tabs.length === 0);
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
// Without the numbers in the message. If it interpolates a number, every
|
|
947
|
+
// round is a new message, and the warning comes again for every panel
|
|
948
|
+
// that appears instead of once.
|
|
949
|
+
warnAboutMarkup(this, "has tabs without a panel: a link points to an id that does not " +
|
|
950
|
+
"exist. Write an id on each .lt-tabs__panel, the same one the " +
|
|
951
|
+
"link points to, or the tabs without a panel can be selected " +
|
|
952
|
+
"without anything being shown.", () => this.tabs.some((tab) => this.panelFor(tab) === null));
|
|
953
|
+
warnAboutMarkup(this, 'has a tab without href: role="tab" is on something that is not ' +
|
|
954
|
+
"a link to a panel. Tabs are links, such as <a " +
|
|
955
|
+
'href="#attachments">, so they work without JavaScript, and the ' +
|
|
956
|
+
"component leaves anything that is not a link alone.", () => this.nonLinkTabs().length > 0);
|
|
957
|
+
warnAboutMarkup(this, "has a tab list without a name. Write aria-label on " +
|
|
958
|
+
'.lt-tabs__list, such as aria-label="Parts of the case", or ' +
|
|
959
|
+
"aria-labelledby pointing to a heading; without a name a screen " +
|
|
960
|
+
'reader only says "tab list".', () => {
|
|
961
|
+
const list = this.list;
|
|
962
|
+
if (!list || this.tabs.length === 0)
|
|
963
|
+
return false;
|
|
964
|
+
const label = list.getAttribute("aria-label");
|
|
965
|
+
const labelledby = list.getAttribute("aria-labelledby");
|
|
966
|
+
return ((label === null || flat(label) === "") &&
|
|
967
|
+
(labelledby === null || !this.resolves(labelledby)));
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
/** What the server has called a tab in the tab list, but is not a link to a panel. */
|
|
971
|
+
nonLinkTabs() {
|
|
972
|
+
const list = this.list;
|
|
973
|
+
if (!list)
|
|
974
|
+
return [];
|
|
975
|
+
const tabs = new Set(this.tabs);
|
|
976
|
+
return [...list.querySelectorAll(ANY_TAB_SELECTOR)].filter((e) => e.closest(this.localName) === this && !tabs.has(e));
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* The tab in this tab list the event came from, or -1.
|
|
980
|
+
*
|
|
981
|
+
* `indexOf` and not just `closest`: a tab list inside a panel bubbles its
|
|
982
|
+
* clicks up here too, and they do not belong to this tab list.
|
|
983
|
+
*/
|
|
984
|
+
tabIndexOf(event) {
|
|
985
|
+
const target = event.target;
|
|
986
|
+
if (!(target instanceof Element))
|
|
987
|
+
return -1;
|
|
988
|
+
const tab = target.closest("[role='tab']");
|
|
989
|
+
return tab ? this.tabs.indexOf(tab) : -1;
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Releases the panel after `beforematch`, in the task after the browser has
|
|
993
|
+
* finished revealing, and writes the tab list.
|
|
994
|
+
*
|
|
995
|
+
* Usually the browser has removed `hidden` by then. It has not when the
|
|
996
|
+
* panel was taken out of the document during the event, and without this
|
|
997
|
+
* round the panel stayed hidden even though the tab was selected.
|
|
998
|
+
*/
|
|
999
|
+
revealed(panel) {
|
|
1000
|
+
if (this.revealing !== panel)
|
|
1001
|
+
return;
|
|
1002
|
+
this.revealing = undefined;
|
|
1003
|
+
if (this.isConnected)
|
|
1004
|
+
this.sync();
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* The index of the tab the address points to, or -1: the panel itself, such
|
|
1008
|
+
* as `#attachments`, or something inside it, such as a heading in the panel.
|
|
1009
|
+
*/
|
|
1010
|
+
get addressIndex() {
|
|
1011
|
+
const hash = location.hash.slice(1);
|
|
1012
|
+
if (!hash)
|
|
1013
|
+
return -1;
|
|
1014
|
+
const target = this.byFragment(hash);
|
|
1015
|
+
if (!target)
|
|
1016
|
+
return -1;
|
|
1017
|
+
return this.tabs.findIndex((tab) => {
|
|
1018
|
+
const panel = this.panelFor(tab);
|
|
1019
|
+
return panel !== null && (panel === target || panel.contains(target));
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Corrects the address when it points to another tab in the tab list than
|
|
1024
|
+
* the selected one, as when the server has moved the selection. An address
|
|
1025
|
+
* to something outside the tab list stays.
|
|
1026
|
+
*/
|
|
1027
|
+
alignAddress(index) {
|
|
1028
|
+
const pointsAt = this.addressIndex;
|
|
1029
|
+
if (pointsAt >= 0 && pointsAt !== index)
|
|
1030
|
+
this.showInAddress(index);
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* Writes the tab to the address, so it can be shared, and so the selection
|
|
1034
|
+
* survives the server replacing the links.
|
|
1035
|
+
*
|
|
1036
|
+
* If the address already points to the selected panel or something in it,
|
|
1037
|
+
* it stays: a link into the content must not be lost. `replaceState`, so
|
|
1038
|
+
* the selection does not become an entry in the history.
|
|
1039
|
+
*/
|
|
1040
|
+
showInAddress(index) {
|
|
1041
|
+
const tab = this.tabs[index];
|
|
1042
|
+
const panel = tab ? this.panelFor(tab) : null;
|
|
1043
|
+
if (!tab || !panel)
|
|
1044
|
+
return;
|
|
1045
|
+
if (this.addressIndex === index)
|
|
1046
|
+
return;
|
|
1047
|
+
// Resolved against the page's address, not against `<base>`: with
|
|
1048
|
+
// `<base href="/">`, which Angular has, the path was replaced silently,
|
|
1049
|
+
// and with a `<base>` to another origin `replaceState` threw.
|
|
1050
|
+
const url = new URL(tab.getAttribute("href") ?? "", location.href).href;
|
|
1051
|
+
try {
|
|
1052
|
+
history.replaceState(history.state, "", url);
|
|
1053
|
+
}
|
|
1054
|
+
catch {
|
|
1055
|
+
// Safari throws after a hundred calls in ten seconds. The address is an
|
|
1056
|
+
// extra, so the selection applies anyway.
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Selects the tab the address points to.
|
|
1061
|
+
*
|
|
1062
|
+
* On load, on `hashchange`, and when the server replaces the links without
|
|
1063
|
+
* saying which one is selected: the selection is then still in the
|
|
1064
|
+
* address. Not under `server-controlled`, where the server decides.
|
|
1065
|
+
*/
|
|
1066
|
+
selectFromAddress() {
|
|
1067
|
+
if (isServerControlled(this))
|
|
1068
|
+
return;
|
|
1069
|
+
const index = this.addressIndex;
|
|
1070
|
+
if (index >= 0)
|
|
1071
|
+
this.select(index);
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* The next tab that can be selected, in one direction, wrapping round if
|
|
1075
|
+
* needed.
|
|
1076
|
+
*
|
|
1077
|
+
* A tab that cannot be selected is skipped. When a hidden tab was selected,
|
|
1078
|
+
* it got the tab stop, and Tab never reached the tab list. The keyboard
|
|
1079
|
+
* user was locked out.
|
|
1080
|
+
*/
|
|
1081
|
+
nextEnabled(from, step) {
|
|
1082
|
+
const tabs = this.tabs;
|
|
1083
|
+
for (let i = 1; i <= tabs.length; i++) {
|
|
1084
|
+
const index = (from + step * i + tabs.length * i) % tabs.length;
|
|
1085
|
+
if (!this.isDisabled(tabs[index]))
|
|
1086
|
+
return index;
|
|
1087
|
+
}
|
|
1088
|
+
return undefined;
|
|
1089
|
+
}
|
|
1090
|
+
/** Selects a tab and fires the event. */
|
|
1091
|
+
select(index) {
|
|
1092
|
+
const tabs = this.tabs;
|
|
1093
|
+
if (index < 0 || index >= tabs.length)
|
|
1094
|
+
return;
|
|
1095
|
+
if (this.isDisabled(tabs[index]))
|
|
1096
|
+
return;
|
|
1097
|
+
// `markedIndex` and not `selected`: the latter answers 0 also when nothing
|
|
1098
|
+
// is marked, and then the first tab could never be selected.
|
|
1099
|
+
if (index === this.markedIndex)
|
|
1100
|
+
return;
|
|
1101
|
+
// No memory when the server decides the selection. Without this a
|
|
1102
|
+
// selection made while the attribute was there would be restored the
|
|
1103
|
+
// moment someone removed it again.
|
|
1104
|
+
if (!isServerControlled(this))
|
|
1105
|
+
this.chosenTab = tabs[index];
|
|
1106
|
+
this.apply(index);
|
|
1107
|
+
if (isServerControlled(this))
|
|
1108
|
+
this.pendingTab = tabs[index];
|
|
1109
|
+
this.showInAddress(index);
|
|
1110
|
+
this.notify(index);
|
|
1111
|
+
}
|
|
1112
|
+
/** Announces which tab is selected now. */
|
|
1113
|
+
notify(index) {
|
|
1114
|
+
this.dispatchEvent(new CustomEvent("tab-select", {
|
|
1115
|
+
detail: { index },
|
|
1116
|
+
bubbles: true,
|
|
1117
|
+
composed: true,
|
|
1118
|
+
}));
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* Writes the selection out into the markup.
|
|
1122
|
+
*
|
|
1123
|
+
* Every write compares first. The web component observes the same
|
|
1124
|
+
* attributes it sets, so a write without a comparison would trigger the
|
|
1125
|
+
* observer, which would write again, forever.
|
|
1126
|
+
*/
|
|
1127
|
+
apply(index) {
|
|
1128
|
+
const tabs = this.tabs;
|
|
1129
|
+
/*
|
|
1130
|
+
* The tab stop is the selected tab, unless a patch has disabled or hidden
|
|
1131
|
+
* it. Then the next tab that can be selected gets it, or Tab skips past
|
|
1132
|
+
* the whole tab list.
|
|
1133
|
+
*/
|
|
1134
|
+
const stop = this.isDisabled(tabs[index])
|
|
1135
|
+
? (this.nextEnabled(index, 1) ?? index)
|
|
1136
|
+
: index;
|
|
1137
|
+
/*
|
|
1138
|
+
* A panel is visible when any tab pointing to it is selected, and is
|
|
1139
|
+
* written once. When two tabs point to the same one, through a mistake in
|
|
1140
|
+
* the markup, they would otherwise write opposite `hidden` on it in every
|
|
1141
|
+
* round, and the page would freeze.
|
|
1142
|
+
*/
|
|
1143
|
+
const panels = new Map();
|
|
1144
|
+
tabs.forEach((tab, i) => {
|
|
1145
|
+
const chosen = i === index;
|
|
1146
|
+
// The server's word is noted before the write, or the write was read as
|
|
1147
|
+
// the server's in the next round.
|
|
1148
|
+
const server = this.serverSelected(tab);
|
|
1149
|
+
setAttr(tab, "aria-selected", String(chosen));
|
|
1150
|
+
this.selectedMemory.set(tab, { written: String(chosen), server });
|
|
1151
|
+
setAttr(tab, "tabindex", i === stop ? "0" : "-1");
|
|
1152
|
+
const panel = this.panelFor(tab);
|
|
1153
|
+
if (panel)
|
|
1154
|
+
panels.set(panel, (panels.get(panel) ?? false) || chosen);
|
|
1155
|
+
});
|
|
1156
|
+
// The form is read from a panel the server hides before anything is
|
|
1157
|
+
// written, or a panel to be hidden first in the tab list was written in a
|
|
1158
|
+
// different form from the ones the server hides.
|
|
1159
|
+
for (const panel of panels.keys()) {
|
|
1160
|
+
const form = this.serverHidden(panel);
|
|
1161
|
+
if (form !== null) {
|
|
1162
|
+
this.hiddenForm = form;
|
|
1163
|
+
break;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
for (const [panel, shown] of panels) {
|
|
1167
|
+
const server = this.serverHidden(panel);
|
|
1168
|
+
const written = shown ? null : this.hiddenForm;
|
|
1169
|
+
if (panel !== this.revealing) {
|
|
1170
|
+
if (written === null)
|
|
1171
|
+
setFlag(panel, "hidden", false);
|
|
1172
|
+
else
|
|
1173
|
+
setAttr(panel, "hidden", written);
|
|
1174
|
+
this.hiddenMemory.set(panel, { written, server });
|
|
1175
|
+
}
|
|
1176
|
+
/*
|
|
1177
|
+
* Only the selected panel is a tab stop, so Tab from the tab list goes
|
|
1178
|
+
* straight into the content that was just shown. A panel with
|
|
1179
|
+
* `hidden="until-found"` keeps its box, so with `tabindex="0"` it was
|
|
1180
|
+
* an invisible stop for the keyboard. Another value the server wrote,
|
|
1181
|
+
* such as `-1`, stays.
|
|
1182
|
+
*/
|
|
1183
|
+
const tabindex = panel.getAttribute("tabindex");
|
|
1184
|
+
if (shown && tabindex === null)
|
|
1185
|
+
setAttr(panel, "tabindex", "0");
|
|
1186
|
+
else if (!shown && tabindex === "0")
|
|
1187
|
+
setAttr(panel, "tabindex", null);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
/**
|
|
1192
|
+
* `server-controlled` and `activation`. Which tab is selected is in the
|
|
1193
|
+
* markup the server sent, as `aria-selected` on the tab and `hidden` on the
|
|
1194
|
+
* panels, and is read from there. A separate `selected` would have been a
|
|
1195
|
+
* parallel copy of the same thing.
|
|
1196
|
+
*/
|
|
1197
|
+
LtTabs.observedAttributes = [SERVER_CONTROLLED, "activation"];
|
|
1198
|
+
export function defineLtTabs(tagName = LT_TABS_TAG) {
|
|
1199
|
+
defineElement(tagName, LtTabs);
|
|
1200
|
+
}
|
|
1201
|
+
//# sourceMappingURL=lt-tabs.js.map
|