@zeix/le-truc 0.15.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/.ai-context.md +234 -0
- package/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc +111 -0
- package/.editorconfig +12 -0
- package/.github/copilot-instructions.md +62 -0
- package/.github/workflows/codeql.yml +108 -0
- package/.github/workflows/static.yml +43 -0
- package/.prettierrc +17 -0
- package/CLAUDE.md +215 -0
- package/CODE_OF_CONDUCT.md +128 -0
- package/CONTRIBUTING.md +160 -0
- package/LICENSE +21 -0
- package/README.md +474 -0
- package/biome.json +295 -0
- package/bun.lock +239 -0
- package/docs/about.html +105 -0
- package/docs/assets/main.css +1 -0
- package/docs/assets/main.js +10 -0
- package/docs/assets/main.js.map +66 -0
- package/docs/components.html +293 -0
- package/docs/data-flow.html +308 -0
- package/docs/examples/basic-button.html +367 -0
- package/docs/examples/basic-counter.html +188 -0
- package/docs/examples/basic-hello.html +138 -0
- package/docs/examples/basic-number.html +271 -0
- package/docs/examples/basic-pluralize.html +214 -0
- package/docs/examples/card-callout.html +152 -0
- package/docs/examples/card-mediaqueries.html +138 -0
- package/docs/examples/context-media.html +198 -0
- package/docs/examples/empty.html +37 -0
- package/docs/examples/form-checkbox.html +233 -0
- package/docs/examples/form-combobox.html +420 -0
- package/docs/examples/form-listbox.html +434 -0
- package/docs/examples/form-radiogroup.html +296 -0
- package/docs/examples/form-spinbutton.html +402 -0
- package/docs/examples/form-textbox.html +361 -0
- package/docs/examples/layout.html +67 -0
- package/docs/examples/module-carousel.html +552 -0
- package/docs/examples/module-catalog.html +241 -0
- package/docs/examples/module-codeblock.html +270 -0
- package/docs/examples/module-dialog.html +343 -0
- package/docs/examples/module-lazyload.html +289 -0
- package/docs/examples/module-list.html +197 -0
- package/docs/examples/module-pagination.html +283 -0
- package/docs/examples/module-scrollarea.html +447 -0
- package/docs/examples/module-tabgroup.html +526 -0
- package/docs/examples/module-todo.html +367 -0
- package/docs/examples/module-with-type.html +63 -0
- package/docs/examples/nested-components.html +88 -0
- package/docs/examples/recursive.html +56 -0
- package/docs/examples/simple-text.html +39 -0
- package/docs/examples/snippet.html +93 -0
- package/docs/examples/with-styles.html +75 -0
- package/docs/getting-started.html +143 -0
- package/docs/index.html +112 -0
- package/docs/sitemap.xml +28 -0
- package/docs/styling.html +160 -0
- package/docs/sw.js +112 -0
- package/docs-src/api/README.md +478 -0
- package/docs-src/api/_media/LICENSE +21 -0
- package/docs-src/api/classes/CircularDependencyError.md +299 -0
- package/docs-src/api/classes/CircularMutationError.md +301 -0
- package/docs-src/api/classes/ContextRequestEvent.md +590 -0
- package/docs-src/api/classes/DependencyTimeoutError.md +301 -0
- package/docs-src/api/classes/InvalidCallbackError.md +303 -0
- package/docs-src/api/classes/InvalidComponentNameError.md +295 -0
- package/docs-src/api/classes/InvalidCustomElementError.md +301 -0
- package/docs-src/api/classes/InvalidEffectsError.md +301 -0
- package/docs-src/api/classes/InvalidPropertyNameError.md +307 -0
- package/docs-src/api/classes/InvalidReactivesError.md +307 -0
- package/docs-src/api/classes/InvalidSignalValueError.md +303 -0
- package/docs-src/api/classes/MissingElementError.md +307 -0
- package/docs-src/api/classes/NullishSignalValueError.md +299 -0
- package/docs-src/api/classes/StoreKeyExistsError.md +303 -0
- package/docs-src/api/classes/StoreKeyRangeError.md +299 -0
- package/docs-src/api/classes/StoreKeyReadonlyError.md +303 -0
- package/docs-src/api/functions/asBoolean.md +21 -0
- package/docs-src/api/functions/asEnum.md +31 -0
- package/docs-src/api/functions/asInteger.md +39 -0
- package/docs-src/api/functions/asJSON.md +49 -0
- package/docs-src/api/functions/asNumber.md +37 -0
- package/docs-src/api/functions/asString.md +37 -0
- package/docs-src/api/functions/createCollection.md +83 -0
- package/docs-src/api/functions/createSensor.md +71 -0
- package/docs-src/api/functions/dangerouslySetInnerHTML.md +48 -0
- package/docs-src/api/functions/defineComponent.md +65 -0
- package/docs-src/api/functions/isCollection.md +37 -0
- package/docs-src/api/functions/isParser.md +41 -0
- package/docs-src/api/functions/match.md +47 -0
- package/docs-src/api/functions/on.md +58 -0
- package/docs-src/api/functions/pass.md +53 -0
- package/docs-src/api/functions/provideContexts.md +47 -0
- package/docs-src/api/functions/read.md +47 -0
- package/docs-src/api/functions/requestContext.md +51 -0
- package/docs-src/api/functions/resolve.md +40 -0
- package/docs-src/api/functions/runEffects.md +51 -0
- package/docs-src/api/functions/runElementEffects.md +57 -0
- package/docs-src/api/functions/schedule.md +33 -0
- package/docs-src/api/functions/setAttribute.md +48 -0
- package/docs-src/api/functions/setProperty.md +52 -0
- package/docs-src/api/functions/setStyle.md +48 -0
- package/docs-src/api/functions/setText.md +42 -0
- package/docs-src/api/functions/show.md +42 -0
- package/docs-src/api/functions/toSignal.md +37 -0
- package/docs-src/api/functions/toggleAttribute.md +48 -0
- package/docs-src/api/functions/toggleClass.md +48 -0
- package/docs-src/api/functions/updateElement.md +53 -0
- package/docs-src/api/globals.md +131 -0
- package/docs-src/api/type-aliases/Cleanup.md +27 -0
- package/docs-src/api/type-aliases/Collection.md +91 -0
- package/docs-src/api/type-aliases/CollectionListener.md +27 -0
- package/docs-src/api/type-aliases/Component.md +17 -0
- package/docs-src/api/type-aliases/ComponentProp.md +11 -0
- package/docs-src/api/type-aliases/ComponentProps.md +11 -0
- package/docs-src/api/type-aliases/ComponentSetup.md +31 -0
- package/docs-src/api/type-aliases/ComponentUI.md +27 -0
- package/docs-src/api/type-aliases/Computed.md +49 -0
- package/docs-src/api/type-aliases/ComputedCallback.md +29 -0
- package/docs-src/api/type-aliases/Context.md +33 -0
- package/docs-src/api/type-aliases/ContextType.md +19 -0
- package/docs-src/api/type-aliases/DangerouslySetInnerHTMLOptions.md +27 -0
- package/docs-src/api/type-aliases/DiffResult.md +61 -0
- package/docs-src/api/type-aliases/Effect.md +35 -0
- package/docs-src/api/type-aliases/EffectCallback.md +23 -0
- package/docs-src/api/type-aliases/Effects.md +21 -0
- package/docs-src/api/type-aliases/ElementEffects.md +21 -0
- package/docs-src/api/type-aliases/ElementFromKey.md +21 -0
- package/docs-src/api/type-aliases/ElementQueries.md +27 -0
- package/docs-src/api/type-aliases/ElementUpdater.md +131 -0
- package/docs-src/api/type-aliases/EventHandler.md +31 -0
- package/docs-src/api/type-aliases/EventType.md +17 -0
- package/docs-src/api/type-aliases/Fallback.md +21 -0
- package/docs-src/api/type-aliases/Initializers.md +21 -0
- package/docs-src/api/type-aliases/LooseReader.md +31 -0
- package/docs-src/api/type-aliases/MatchHandlers.md +77 -0
- package/docs-src/api/type-aliases/MaybeCleanup.md +23 -0
- package/docs-src/api/type-aliases/MaybeSignal.md +17 -0
- package/docs-src/api/type-aliases/Parser.md +39 -0
- package/docs-src/api/type-aliases/ParserOrFallback.md +21 -0
- package/docs-src/api/type-aliases/PassedProp.md +25 -0
- package/docs-src/api/type-aliases/PassedProps.md +21 -0
- package/docs-src/api/type-aliases/Reactive.md +25 -0
- package/docs-src/api/type-aliases/Reader.md +31 -0
- package/docs-src/api/type-aliases/ReservedWords.md +11 -0
- package/docs-src/api/type-aliases/ResolveResult.md +29 -0
- package/docs-src/api/type-aliases/SensorEvents.md +25 -0
- package/docs-src/api/type-aliases/Signal.md +41 -0
- package/docs-src/api/type-aliases/State.md +85 -0
- package/docs-src/api/type-aliases/Store.md +29 -0
- package/docs-src/api/type-aliases/UI.md +11 -0
- package/docs-src/api/type-aliases/UnknownContext.md +13 -0
- package/docs-src/api/variables/CONTEXT_REQUEST.md +11 -0
- package/docs-src/api/variables/UNSET.md +23 -0
- package/docs-src/api/variables/batch.md +25 -0
- package/docs-src/api/variables/createComputed.md +41 -0
- package/docs-src/api/variables/createEffect.md +35 -0
- package/docs-src/api/variables/createState.md +37 -0
- package/docs-src/api/variables/createStore.md +42 -0
- package/docs-src/api/variables/diff.md +43 -0
- package/docs-src/api/variables/isAbortError.md +33 -0
- package/docs-src/api/variables/isAsyncFunction.md +39 -0
- package/docs-src/api/variables/isComputed.md +37 -0
- package/docs-src/api/variables/isEqual.md +49 -0
- package/docs-src/api/variables/isFunction.md +39 -0
- package/docs-src/api/variables/isMutableSignal.md +37 -0
- package/docs-src/api/variables/isNumber.md +33 -0
- package/docs-src/api/variables/isRecord.md +39 -0
- package/docs-src/api/variables/isRecordOrArray.md +39 -0
- package/docs-src/api/variables/isSignal.md +37 -0
- package/docs-src/api/variables/isState.md +37 -0
- package/docs-src/api/variables/isStore.md +37 -0
- package/docs-src/api/variables/isString.md +33 -0
- package/docs-src/api/variables/isSymbol.md +33 -0
- package/docs-src/api/variables/toError.md +33 -0
- package/docs-src/api/variables/valueString.md +33 -0
- package/docs-src/includes/menu.html +44 -0
- package/docs-src/pages/about.md +89 -0
- package/docs-src/pages/components.md +437 -0
- package/docs-src/pages/data-flow.md +449 -0
- package/docs-src/pages/getting-started.md +170 -0
- package/docs-src/pages/index.md +98 -0
- package/docs-src/pages/styling.md +165 -0
- package/eslint.config.js +64 -0
- package/examples/_common/clear.ts +49 -0
- package/examples/_common/fetch.ts +160 -0
- package/examples/_common/focus.ts +45 -0
- package/examples/_common/highlight.ts +5 -0
- package/examples/_global.css +463 -0
- package/examples/basic-button/basic-button.css +176 -0
- package/examples/basic-button/basic-button.html +46 -0
- package/examples/basic-button/basic-button.spec.ts +160 -0
- package/examples/basic-button/basic-button.ts +45 -0
- package/examples/basic-button/copyToClipboard.ts +37 -0
- package/examples/basic-counter/basic-counter.css +21 -0
- package/examples/basic-counter/basic-counter.html +24 -0
- package/examples/basic-counter/basic-counter.spec.ts +85 -0
- package/examples/basic-counter/basic-counter.ts +43 -0
- package/examples/basic-hello/basic-hello.html +34 -0
- package/examples/basic-hello/basic-hello.spec.ts +110 -0
- package/examples/basic-hello/basic-hello.ts +36 -0
- package/examples/basic-number/basic-number.html +79 -0
- package/examples/basic-number/basic-number.spec.ts +175 -0
- package/examples/basic-number/basic-number.ts +124 -0
- package/examples/basic-pluralize/basic-pluralize.html +64 -0
- package/examples/basic-pluralize/basic-pluralize.spec.ts +258 -0
- package/examples/basic-pluralize/basic-pluralize.ts +82 -0
- package/examples/card-callout/card-callout.css +79 -0
- package/examples/card-callout/card-callout.html +5 -0
- package/examples/card-mediaqueries/card-mediaqueries.html +29 -0
- package/examples/card-mediaqueries/card-mediaqueries.spec.ts +300 -0
- package/examples/card-mediaqueries/card-mediaqueries.ts +41 -0
- package/examples/context-media/context-media.html +3 -0
- package/examples/context-media/context-media.ts +127 -0
- package/examples/form-checkbox/form-checkbox.css +70 -0
- package/examples/form-checkbox/form-checkbox.html +13 -0
- package/examples/form-checkbox/form-checkbox.spec.ts +357 -0
- package/examples/form-checkbox/form-checkbox.ts +50 -0
- package/examples/form-checkbox/vanilla-checkbox.ts +101 -0
- package/examples/form-combobox/form-combobox.css +118 -0
- package/examples/form-combobox/form-combobox.html +74 -0
- package/examples/form-combobox/form-combobox.spec.ts +977 -0
- package/examples/form-combobox/form-combobox.ts +128 -0
- package/examples/form-listbox/form-listbox.css +71 -0
- package/examples/form-listbox/form-listbox.html +67 -0
- package/examples/form-listbox/form-listbox.spec.ts +1050 -0
- package/examples/form-listbox/form-listbox.ts +196 -0
- package/examples/form-listbox/mocks/timezones.json +495 -0
- package/examples/form-radiogroup/form-radiogroup.css +87 -0
- package/examples/form-radiogroup/form-radiogroup.html +51 -0
- package/examples/form-radiogroup/form-radiogroup.spec.ts +515 -0
- package/examples/form-radiogroup/form-radiogroup.ts +58 -0
- package/examples/form-spinbutton/form-spinbutton.css +95 -0
- package/examples/form-spinbutton/form-spinbutton.html +96 -0
- package/examples/form-spinbutton/form-spinbutton.spec.ts +688 -0
- package/examples/form-spinbutton/form-spinbutton.ts +111 -0
- package/examples/form-textbox/form-textbox.css +104 -0
- package/examples/form-textbox/form-textbox.html +53 -0
- package/examples/form-textbox/form-textbox.spec.ts +542 -0
- package/examples/form-textbox/form-textbox.ts +104 -0
- package/examples/main.css +22 -0
- package/examples/main.ts +23 -0
- package/examples/module-carousel/module-carousel.css +113 -0
- package/examples/module-carousel/module-carousel.html +208 -0
- package/examples/module-carousel/module-carousel.spec.ts +523 -0
- package/examples/module-carousel/module-carousel.ts +131 -0
- package/examples/module-catalog/module-catalog.css +22 -0
- package/examples/module-catalog/module-catalog.html +82 -0
- package/examples/module-catalog/module-catalog.spec.ts +396 -0
- package/examples/module-catalog/module-catalog.ts +37 -0
- package/examples/module-codeblock/module-codeblock.css +95 -0
- package/examples/module-codeblock/module-codeblock.html +28 -0
- package/examples/module-codeblock/module-codeblock.ts +47 -0
- package/examples/module-demo/module-demo.css +13 -0
- package/examples/module-dialog/module-dialog.css +96 -0
- package/examples/module-dialog/module-dialog.html +66 -0
- package/examples/module-dialog/module-dialog.spec.ts +557 -0
- package/examples/module-dialog/module-dialog.ts +81 -0
- package/examples/module-lazyload/mocks/empty.html +1 -0
- package/examples/module-lazyload/mocks/module-with-type.html +27 -0
- package/examples/module-lazyload/mocks/nested-components.html +52 -0
- package/examples/module-lazyload/mocks/recursive.html +20 -0
- package/examples/module-lazyload/mocks/simple-text.html +3 -0
- package/examples/module-lazyload/mocks/snippet.html +57 -0
- package/examples/module-lazyload/mocks/with-styles.html +39 -0
- package/examples/module-lazyload/module-lazyload.html +132 -0
- package/examples/module-lazyload/module-lazyload.spec.ts +734 -0
- package/examples/module-lazyload/module-lazyload.ts +89 -0
- package/examples/module-list/module-list.html +30 -0
- package/examples/module-list/module-list.spec.ts +592 -0
- package/examples/module-list/module-list.ts +99 -0
- package/examples/module-pagination/module-pagination.css +79 -0
- package/examples/module-pagination/module-pagination.html +16 -0
- package/examples/module-pagination/module-pagination.spec.ts +701 -0
- package/examples/module-pagination/module-pagination.ts +88 -0
- package/examples/module-scrollarea/module-scrollarea.css +77 -0
- package/examples/module-scrollarea/module-scrollarea.html +189 -0
- package/examples/module-scrollarea/module-scrollarea.spec.ts +445 -0
- package/examples/module-scrollarea/module-scrollarea.ts +81 -0
- package/examples/module-tabgroup/module-tabgroup.css +55 -0
- package/examples/module-tabgroup/module-tabgroup.html +269 -0
- package/examples/module-tabgroup/module-tabgroup.spec.ts +631 -0
- package/examples/module-tabgroup/module-tabgroup.ts +102 -0
- package/examples/module-toc/module-toc.css +34 -0
- package/examples/module-todo/module-todo.css +84 -0
- package/examples/module-todo/module-todo.html +92 -0
- package/examples/module-todo/module-todo.spec.ts +528 -0
- package/examples/module-todo/module-todo.ts +91 -0
- package/examples/section-hero/section-hero.css +37 -0
- package/examples/section-menu/section-menu.css +81 -0
- package/examples/server.ts +95 -0
- package/examples/test-setup.md +314 -0
- package/index.dev.js +1688 -0
- package/index.dev.ts +127 -0
- package/index.js +3 -0
- package/index.js.map +42 -0
- package/index.ts +127 -0
- package/package.json +64 -0
- package/playwright.config.ts +31 -0
- package/server/BUILD_SYSTEM.md +428 -0
- package/server/SERVER.md +286 -0
- package/server/build.ts +91 -0
- package/server/config.ts +130 -0
- package/server/effects/api.ts +28 -0
- package/server/effects/css.ts +31 -0
- package/server/effects/examples.ts +109 -0
- package/server/effects/js.ts +32 -0
- package/server/effects/menu.ts +34 -0
- package/server/effects/pages.ts +178 -0
- package/server/effects/service-worker.ts +57 -0
- package/server/effects/sitemap.ts +27 -0
- package/server/file-signals.ts +361 -0
- package/server/file-watcher.ts +77 -0
- package/server/io.ts +174 -0
- package/server/layout-engine.ts +470 -0
- package/server/layout-utils.ts +615 -0
- package/server/layouts/api.html +76 -0
- package/server/layouts/base.html +37 -0
- package/server/layouts/blog.html +115 -0
- package/server/layouts/example.html +104 -0
- package/server/layouts/overview.html +165 -0
- package/server/layouts/page.html +36 -0
- package/server/layouts/test.html +24 -0
- package/server/markdoc-helpers.ts +217 -0
- package/server/markdoc.config.ts +29 -0
- package/server/schema/callout.markdoc.ts +17 -0
- package/server/schema/carousel.markdoc.ts +118 -0
- package/server/schema/demo.markdoc.ts +74 -0
- package/server/schema/fence.markdoc.ts +84 -0
- package/server/schema/heading.markdoc.ts +23 -0
- package/server/schema/hero.markdoc.ts +59 -0
- package/server/schema/section.markdoc.ts +10 -0
- package/server/schema/slide.markdoc.ts +17 -0
- package/server/schema/source.markdoc.ts +53 -0
- package/server/schema/tabgroup.markdoc.ts +102 -0
- package/server/serve.ts +635 -0
- package/server/templates/README.md +352 -0
- package/server/templates/constants.ts +236 -0
- package/server/templates/fragments.ts +159 -0
- package/server/templates/hmr.ts +269 -0
- package/server/templates/menu.ts +33 -0
- package/server/templates/performance-hints.ts +94 -0
- package/server/templates/service-worker.ts +403 -0
- package/server/templates/sitemap.ts +57 -0
- package/server/templates/toc.ts +41 -0
- package/server/templates/utils.ts +378 -0
- package/src/component.ts +215 -0
- package/src/context.ts +156 -0
- package/src/effects/attribute.ts +82 -0
- package/src/effects/class.ts +28 -0
- package/src/effects/event.ts +67 -0
- package/src/effects/html.ts +60 -0
- package/src/effects/method.ts +57 -0
- package/src/effects/pass.ts +103 -0
- package/src/effects/property.ts +57 -0
- package/src/effects/style.ts +34 -0
- package/src/effects/text.ts +28 -0
- package/src/effects.ts +412 -0
- package/src/errors.ts +160 -0
- package/src/parsers/boolean.ts +14 -0
- package/src/parsers/json.ts +33 -0
- package/src/parsers/number.ts +55 -0
- package/src/parsers/string.ts +32 -0
- package/src/parsers.ts +90 -0
- package/src/scheduler.ts +47 -0
- package/src/signals/collection.ts +253 -0
- package/src/signals/sensor.ts +131 -0
- package/src/ui.ts +236 -0
- package/src/util.ts +187 -0
- package/tsconfig.json +34 -0
- package/types/examples/basic-button/basic-button.d.ts +16 -0
- package/types/examples/basic-hello/basic-hello.d.ts +18 -0
- package/types/index.d.ts +27 -0
- package/types/index.dev.d.ts +27 -0
- package/types/src/collection.d.ts +27 -0
- package/types/src/component.d.ts +32 -0
- package/types/src/context.d.ts +85 -0
- package/types/src/effects/attribute.d.ts +23 -0
- package/types/src/effects/callMethod.d.ts +23 -0
- package/types/src/effects/class.d.ts +13 -0
- package/types/src/effects/dangerouslySetInnerHTML.d.ts +18 -0
- package/types/src/effects/event.d.ts +18 -0
- package/types/src/effects/html.d.ts +17 -0
- package/types/src/effects/method.d.ts +22 -0
- package/types/src/effects/pass.d.ts +18 -0
- package/types/src/effects/property.d.ts +22 -0
- package/types/src/effects/setAttribute.d.ts +24 -0
- package/types/src/effects/setProperty.d.ts +23 -0
- package/types/src/effects/setStyle.d.ts +14 -0
- package/types/src/effects/setText.d.ts +13 -0
- package/types/src/effects/style.d.ts +13 -0
- package/types/src/effects/text.d.ts +12 -0
- package/types/src/effects/toggleClass.d.ts +14 -0
- package/types/src/effects.d.ts +153 -0
- package/types/src/errors.d.ts +99 -0
- package/types/src/events.d.ts +27 -0
- package/types/src/extractors.d.ts +23 -0
- package/types/src/parsers/boolean.d.ts +10 -0
- package/types/src/parsers/json.d.ts +13 -0
- package/types/src/parsers/number.d.ts +21 -0
- package/types/src/parsers/string.d.ts +19 -0
- package/types/src/parsers.d.ts +41 -0
- package/types/src/scheduler.d.ts +11 -0
- package/types/src/sensor.d.ts +27 -0
- package/types/src/signals/collection.d.ts +32 -0
- package/types/src/signals/sensor.d.ts +27 -0
- package/types/src/ui.d.ts +37 -0
- package/types/src/util.d.ts +65 -0
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
import { expect, test } from '@playwright/test'
|
|
2
|
+
|
|
3
|
+
test.describe('form-radiogroup component', () => {
|
|
4
|
+
test.beforeEach(async ({ page }) => {
|
|
5
|
+
page.on('console', msg => {
|
|
6
|
+
console.log(`[browser] ${msg.type()}: ${msg.text()}`)
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
await page.goto('http://localhost:3000/test/form-radiogroup.html')
|
|
10
|
+
await page.waitForSelector('form-radiogroup')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('renders initial state correctly', async ({ page }) => {
|
|
14
|
+
const radiogroupComponent = page.locator('form-radiogroup').first()
|
|
15
|
+
const radios = radiogroupComponent.locator('input[type="radio"]')
|
|
16
|
+
const legend = radiogroupComponent.locator('legend')
|
|
17
|
+
const labels = radiogroupComponent.locator('label')
|
|
18
|
+
|
|
19
|
+
// Should have the correct number of radio buttons
|
|
20
|
+
await expect(radios).toHaveCount(3)
|
|
21
|
+
await expect(labels).toHaveCount(3)
|
|
22
|
+
|
|
23
|
+
// Should display correct legend text
|
|
24
|
+
await expect(legend).toHaveText('Gender')
|
|
25
|
+
|
|
26
|
+
// Should have the initial selection based on the value attribute
|
|
27
|
+
const checkedRadio = radiogroupComponent.locator('input[value="other"]')
|
|
28
|
+
await expect(checkedRadio).toBeChecked()
|
|
29
|
+
|
|
30
|
+
// Should have the selected class on the correct label
|
|
31
|
+
const selectedLabel = radiogroupComponent.locator('label.selected')
|
|
32
|
+
await expect(selectedLabel).toHaveCount(1)
|
|
33
|
+
await expect(selectedLabel.locator('span')).toHaveText('Other')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('selects radio button when clicked', async ({ page }) => {
|
|
37
|
+
const radiogroupComponent = page.locator('form-radiogroup').first()
|
|
38
|
+
const femaleRadio = radiogroupComponent.locator('input[value="female"]')
|
|
39
|
+
const maleRadio = radiogroupComponent.locator('input[value="male"]')
|
|
40
|
+
const otherRadio = radiogroupComponent.locator('input[value="other"]')
|
|
41
|
+
|
|
42
|
+
// Initially "other" is selected
|
|
43
|
+
await expect(otherRadio).toBeChecked()
|
|
44
|
+
await expect(femaleRadio).not.toBeChecked()
|
|
45
|
+
await expect(maleRadio).not.toBeChecked()
|
|
46
|
+
|
|
47
|
+
// Click female radio
|
|
48
|
+
await femaleRadio.click()
|
|
49
|
+
await expect(femaleRadio).toBeChecked()
|
|
50
|
+
await expect(otherRadio).not.toBeChecked()
|
|
51
|
+
await expect(maleRadio).not.toBeChecked()
|
|
52
|
+
|
|
53
|
+
// Click male radio
|
|
54
|
+
await maleRadio.click()
|
|
55
|
+
await expect(maleRadio).toBeChecked()
|
|
56
|
+
await expect(femaleRadio).not.toBeChecked()
|
|
57
|
+
await expect(otherRadio).not.toBeChecked()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('syncs value property with radio selection', async ({ page }) => {
|
|
61
|
+
const radiogroupComponent = page.locator('form-radiogroup').first()
|
|
62
|
+
const femaleRadio = radiogroupComponent.locator('input[value="female"]')
|
|
63
|
+
|
|
64
|
+
// Initially should have "other" value
|
|
65
|
+
let currentValue = await page.evaluate(() => {
|
|
66
|
+
const element = document.querySelector('form-radiogroup') as any
|
|
67
|
+
return element.value
|
|
68
|
+
})
|
|
69
|
+
expect(currentValue).toBe('other')
|
|
70
|
+
|
|
71
|
+
// Click female radio
|
|
72
|
+
await femaleRadio.click()
|
|
73
|
+
|
|
74
|
+
currentValue = await page.evaluate(() => {
|
|
75
|
+
const element = document.querySelector('form-radiogroup') as any
|
|
76
|
+
return element.value
|
|
77
|
+
})
|
|
78
|
+
expect(currentValue).toBe('female')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('updates selected class when selection changes', async ({ page }) => {
|
|
82
|
+
const radiogroupComponent = page.locator('form-radiogroup').first()
|
|
83
|
+
const femaleRadio = radiogroupComponent.locator('input[value="female"]')
|
|
84
|
+
const femaleLabel = radiogroupComponent
|
|
85
|
+
.locator('label')
|
|
86
|
+
.filter({ has: page.locator('input[value="female"]') })
|
|
87
|
+
const otherLabel = radiogroupComponent
|
|
88
|
+
.locator('label')
|
|
89
|
+
.filter({ has: page.locator('input[value="other"]') })
|
|
90
|
+
|
|
91
|
+
// Initially "other" label should have selected class
|
|
92
|
+
const initialSelectedLabel = radiogroupComponent.locator('label.selected')
|
|
93
|
+
await expect(initialSelectedLabel).toHaveCount(1)
|
|
94
|
+
await expect(femaleLabel).not.toHaveClass(/selected/)
|
|
95
|
+
|
|
96
|
+
// Click female radio
|
|
97
|
+
await femaleRadio.click()
|
|
98
|
+
|
|
99
|
+
// Female label should now have selected class, other should not
|
|
100
|
+
await expect(femaleLabel).toHaveClass(/selected/)
|
|
101
|
+
await expect(otherLabel).not.toHaveClass(/selected/)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('handles keyboard navigation with arrow keys', async ({ page }) => {
|
|
105
|
+
const radiogroupComponent = page.locator('form-radiogroup').first()
|
|
106
|
+
const femaleRadio = radiogroupComponent.locator('input[value="female"]')
|
|
107
|
+
const maleRadio = radiogroupComponent.locator('input[value="male"]')
|
|
108
|
+
const otherRadio = radiogroupComponent.locator('input[value="other"]')
|
|
109
|
+
|
|
110
|
+
// Start with focus on the initially checked radio (other is at index 2)
|
|
111
|
+
await otherRadio.focus()
|
|
112
|
+
await expect(otherRadio).toBeFocused()
|
|
113
|
+
|
|
114
|
+
// Arrow right should wrap around to index 0 (female)
|
|
115
|
+
await page.keyboard.press('ArrowRight')
|
|
116
|
+
await expect(femaleRadio).toBeFocused()
|
|
117
|
+
|
|
118
|
+
// Arrow right should move to index 1 (male)
|
|
119
|
+
await page.keyboard.press('ArrowRight')
|
|
120
|
+
await expect(maleRadio).toBeFocused()
|
|
121
|
+
|
|
122
|
+
// Arrow right should move to index 2 (other)
|
|
123
|
+
await page.keyboard.press('ArrowRight')
|
|
124
|
+
await expect(otherRadio).toBeFocused()
|
|
125
|
+
|
|
126
|
+
// Arrow left should move backwards to index 1 (male)
|
|
127
|
+
await page.keyboard.press('ArrowLeft')
|
|
128
|
+
await expect(maleRadio).toBeFocused()
|
|
129
|
+
|
|
130
|
+
// Arrow left should move to index 0 (female)
|
|
131
|
+
await page.keyboard.press('ArrowLeft')
|
|
132
|
+
await expect(femaleRadio).toBeFocused()
|
|
133
|
+
|
|
134
|
+
// Arrow left from first item should wrap around to last item (other)
|
|
135
|
+
await page.keyboard.press('ArrowLeft')
|
|
136
|
+
await expect(otherRadio).toBeFocused()
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('handles keyboard navigation with up/down arrows', async ({ page }) => {
|
|
140
|
+
const radiogroupComponent = page.locator('form-radiogroup').first()
|
|
141
|
+
const femaleRadio = radiogroupComponent.locator('input[value="female"]')
|
|
142
|
+
const maleRadio = radiogroupComponent.locator('input[value="male"]')
|
|
143
|
+
const otherRadio = radiogroupComponent.locator('input[value="other"]')
|
|
144
|
+
|
|
145
|
+
// First select male radio to establish checked state (needed for focus management)
|
|
146
|
+
await maleRadio.click()
|
|
147
|
+
await expect(maleRadio).toBeChecked()
|
|
148
|
+
await expect(maleRadio).toBeFocused()
|
|
149
|
+
|
|
150
|
+
// Arrow down should move focus (same as arrow right) to next index (other)
|
|
151
|
+
await page.keyboard.press('ArrowDown')
|
|
152
|
+
await expect(otherRadio).toBeFocused()
|
|
153
|
+
|
|
154
|
+
// Arrow down should wrap around to first item (female)
|
|
155
|
+
await page.keyboard.press('ArrowDown')
|
|
156
|
+
await expect(femaleRadio).toBeFocused()
|
|
157
|
+
|
|
158
|
+
// Arrow up should move focus backwards (same as arrow left) to last item (other)
|
|
159
|
+
await page.keyboard.press('ArrowUp')
|
|
160
|
+
await expect(otherRadio).toBeFocused()
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test('handles Home and End key navigation', async ({ page }) => {
|
|
164
|
+
const radiogroupComponent = page.locator('form-radiogroup').first()
|
|
165
|
+
const femaleRadio = radiogroupComponent.locator('input[value="female"]')
|
|
166
|
+
const otherRadio = radiogroupComponent.locator('input[value="other"]')
|
|
167
|
+
const maleRadio = radiogroupComponent.locator('input[value="male"]')
|
|
168
|
+
|
|
169
|
+
// Focus somewhere in the middle
|
|
170
|
+
await maleRadio.focus()
|
|
171
|
+
await expect(maleRadio).toBeFocused()
|
|
172
|
+
|
|
173
|
+
// Home should move to first radio (female is first in DOM)
|
|
174
|
+
await page.keyboard.press('Home')
|
|
175
|
+
await expect(femaleRadio).toBeFocused()
|
|
176
|
+
|
|
177
|
+
// End should move to last radio (other is last in DOM)
|
|
178
|
+
await page.keyboard.press('End')
|
|
179
|
+
await expect(otherRadio).toBeFocused()
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
test('handles Enter key to select focused radio', async ({ page }) => {
|
|
183
|
+
const radiogroupComponent = page.locator('form-radiogroup').first()
|
|
184
|
+
const femaleRadio = radiogroupComponent.locator('input[value="female"]')
|
|
185
|
+
const otherRadio = radiogroupComponent.locator('input[value="other"]')
|
|
186
|
+
|
|
187
|
+
// Initially "other" is checked
|
|
188
|
+
await expect(otherRadio).toBeChecked()
|
|
189
|
+
await expect(femaleRadio).not.toBeChecked()
|
|
190
|
+
|
|
191
|
+
// Focus the currently checked radio (other), then navigate to female
|
|
192
|
+
await otherRadio.focus()
|
|
193
|
+
await expect(otherRadio).toBeFocused()
|
|
194
|
+
|
|
195
|
+
// Navigate to female radio using keyboard
|
|
196
|
+
await page.keyboard.press('ArrowDown') // Move to next (female)
|
|
197
|
+
await expect(femaleRadio).toBeFocused()
|
|
198
|
+
await expect(femaleRadio).not.toBeChecked() // Still not selected
|
|
199
|
+
|
|
200
|
+
// Press Enter to select
|
|
201
|
+
await page.keyboard.press('Enter')
|
|
202
|
+
await expect(femaleRadio).toBeChecked()
|
|
203
|
+
await expect(otherRadio).not.toBeChecked()
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
test('manages tabindex correctly for roving tabindex pattern', async ({
|
|
207
|
+
page,
|
|
208
|
+
}) => {
|
|
209
|
+
const radiogroupComponent = page.locator('form-radiogroup').first()
|
|
210
|
+
const femaleRadio = radiogroupComponent.locator('input[value="female"]')
|
|
211
|
+
|
|
212
|
+
// Wait for component to initialize and set tabindex
|
|
213
|
+
|
|
214
|
+
// Initially checked radio should have tabindex 0, others -1
|
|
215
|
+
// Check the DOM property, not the attribute
|
|
216
|
+
const initialTabIndexes = await page.evaluate(() => {
|
|
217
|
+
const radios = Array.from(
|
|
218
|
+
document.querySelectorAll('form-radiogroup input[type="radio"]'),
|
|
219
|
+
)
|
|
220
|
+
return radios.map((r: any) => ({
|
|
221
|
+
value: r.value,
|
|
222
|
+
tabIndex: r.tabIndex,
|
|
223
|
+
}))
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
expect(initialTabIndexes.find(r => r.value === 'other')?.tabIndex).toBe(0)
|
|
227
|
+
expect(initialTabIndexes.find(r => r.value === 'female')?.tabIndex).toBe(-1)
|
|
228
|
+
expect(initialTabIndexes.find(r => r.value === 'male')?.tabIndex).toBe(-1)
|
|
229
|
+
|
|
230
|
+
// After selecting female, it should get tabindex 0
|
|
231
|
+
await femaleRadio.click()
|
|
232
|
+
|
|
233
|
+
// Check tabindex after selection change
|
|
234
|
+
const updatedTabIndexes = await page.evaluate(() => {
|
|
235
|
+
const radios = Array.from(
|
|
236
|
+
document.querySelectorAll('form-radiogroup input[type="radio"]'),
|
|
237
|
+
)
|
|
238
|
+
return radios.map((r: any) => ({
|
|
239
|
+
value: r.value,
|
|
240
|
+
tabIndex: r.tabIndex,
|
|
241
|
+
}))
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
expect(updatedTabIndexes.find(r => r.value === 'female')?.tabIndex).toBe(0)
|
|
245
|
+
expect(updatedTabIndexes.find(r => r.value === 'other')?.tabIndex).toBe(-1)
|
|
246
|
+
expect(updatedTabIndexes.find(r => r.value === 'male')?.tabIndex).toBe(-1)
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
test('handles clicking on label to select radio', async ({ page }) => {
|
|
250
|
+
const radiogroupComponent = page.locator('form-radiogroup').first()
|
|
251
|
+
const femaleRadio = radiogroupComponent.locator('input[value="female"]')
|
|
252
|
+
const femaleLabel = radiogroupComponent
|
|
253
|
+
.locator('label')
|
|
254
|
+
.filter({ has: page.locator('input[value="female"]') })
|
|
255
|
+
|
|
256
|
+
// Initially not checked
|
|
257
|
+
await expect(femaleRadio).not.toBeChecked()
|
|
258
|
+
|
|
259
|
+
// Click on label should select the radio
|
|
260
|
+
await femaleLabel.click()
|
|
261
|
+
await expect(femaleRadio).toBeChecked()
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
test('handles multiple radiogroup instances independently', async ({
|
|
265
|
+
page,
|
|
266
|
+
}) => {
|
|
267
|
+
const firstRadiogroup = page.locator('form-radiogroup').first()
|
|
268
|
+
const secondRadiogroup = page.locator('form-radiogroup.split-button')
|
|
269
|
+
|
|
270
|
+
// First radiogroup should have "other" selected
|
|
271
|
+
const firstOtherRadio = firstRadiogroup.locator('input[value="other"]')
|
|
272
|
+
await expect(firstOtherRadio).toBeChecked()
|
|
273
|
+
|
|
274
|
+
// Second radiogroup should have "all" selected
|
|
275
|
+
const secondAllRadio = secondRadiogroup.locator('input[value="all"]')
|
|
276
|
+
await expect(secondAllRadio).toBeChecked()
|
|
277
|
+
|
|
278
|
+
// Change selection in first radiogroup
|
|
279
|
+
const firstFemaleRadio = firstRadiogroup.locator('input[value="female"]')
|
|
280
|
+
await firstFemaleRadio.click()
|
|
281
|
+
await expect(firstFemaleRadio).toBeChecked()
|
|
282
|
+
await expect(firstOtherRadio).not.toBeChecked()
|
|
283
|
+
|
|
284
|
+
// Second radiogroup should be unaffected
|
|
285
|
+
await expect(secondAllRadio).toBeChecked()
|
|
286
|
+
|
|
287
|
+
// Change selection in second radiogroup (use label click for visually hidden inputs)
|
|
288
|
+
const secondActiveLabel = secondRadiogroup
|
|
289
|
+
.locator('label')
|
|
290
|
+
.filter({ has: page.locator('input[value="active"]') })
|
|
291
|
+
const secondActiveRadio = secondRadiogroup.locator('input[value="active"]')
|
|
292
|
+
await secondActiveLabel.click()
|
|
293
|
+
await expect(secondActiveRadio).toBeChecked()
|
|
294
|
+
await expect(secondAllRadio).not.toBeChecked()
|
|
295
|
+
|
|
296
|
+
// First radiogroup should still have female selected
|
|
297
|
+
await expect(firstFemaleRadio).toBeChecked()
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
test('handles split-button variant styling', async ({ page }) => {
|
|
301
|
+
const splitButtonRadiogroup = page.locator('form-radiogroup.split-button')
|
|
302
|
+
const labels = splitButtonRadiogroup.locator('label')
|
|
303
|
+
const selectedLabel = splitButtonRadiogroup.locator('label.selected')
|
|
304
|
+
|
|
305
|
+
// Should have split-button class
|
|
306
|
+
await expect(splitButtonRadiogroup).toHaveClass(/split-button/)
|
|
307
|
+
|
|
308
|
+
// Should have correct number of labels
|
|
309
|
+
await expect(labels).toHaveCount(3)
|
|
310
|
+
|
|
311
|
+
// Should have one selected label initially
|
|
312
|
+
await expect(selectedLabel).toHaveCount(1)
|
|
313
|
+
await expect(selectedLabel.locator('span')).toHaveText('All')
|
|
314
|
+
|
|
315
|
+
// Visually hidden inputs should be present
|
|
316
|
+
const hiddenInputs = splitButtonRadiogroup.locator('input.visually-hidden')
|
|
317
|
+
await expect(hiddenInputs).toHaveCount(3)
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
test('value property is readonly (sensor-based)', async ({ page }) => {
|
|
321
|
+
// Test that the value property reflects radio state but doesn't control it
|
|
322
|
+
const initialValue = await page.evaluate(() => {
|
|
323
|
+
const element = document.querySelector('form-radiogroup') as any
|
|
324
|
+
return element.value
|
|
325
|
+
})
|
|
326
|
+
expect(initialValue).toBe('other')
|
|
327
|
+
|
|
328
|
+
// Click a different radio
|
|
329
|
+
const femaleRadio = page
|
|
330
|
+
.locator('form-radiogroup input[value="female"]')
|
|
331
|
+
.first()
|
|
332
|
+
await femaleRadio.click()
|
|
333
|
+
|
|
334
|
+
// Property should now reflect the selected value
|
|
335
|
+
const valueAfterClick = await page.evaluate(() => {
|
|
336
|
+
const element = document.querySelector('form-radiogroup') as any
|
|
337
|
+
return element.value
|
|
338
|
+
})
|
|
339
|
+
expect(valueAfterClick).toBe('female')
|
|
340
|
+
|
|
341
|
+
// Verify that trying to set the value property doesn't change the selection
|
|
342
|
+
// (since it's a readonly sensor)
|
|
343
|
+
await page.evaluate(() => {
|
|
344
|
+
const element = document.querySelector('form-radiogroup') as any
|
|
345
|
+
try {
|
|
346
|
+
element.value = 'male'
|
|
347
|
+
} catch (_e) {
|
|
348
|
+
// Expected - property might be readonly
|
|
349
|
+
}
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
// Radio should still be female
|
|
353
|
+
await expect(femaleRadio).toBeChecked()
|
|
354
|
+
|
|
355
|
+
// The property should still reflect female because it reads from DOM
|
|
356
|
+
const valueAfterAttemptedChange = await page.evaluate(() => {
|
|
357
|
+
const element = document.querySelector('form-radiogroup') as any
|
|
358
|
+
return element.value
|
|
359
|
+
})
|
|
360
|
+
expect(valueAfterAttemptedChange).toBe('female')
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
test('sensor updates when radio state changes programmatically', async ({
|
|
364
|
+
page,
|
|
365
|
+
}) => {
|
|
366
|
+
const radiogroupComponent = page.locator('form-radiogroup').first()
|
|
367
|
+
const maleRadio = radiogroupComponent.locator('input[value="male"]')
|
|
368
|
+
|
|
369
|
+
// Change the radio state directly via DOM
|
|
370
|
+
await page.evaluate(() => {
|
|
371
|
+
const input = document.querySelector(
|
|
372
|
+
'form-radiogroup input[value="male"]',
|
|
373
|
+
) as HTMLInputElement
|
|
374
|
+
input.checked = true
|
|
375
|
+
input.dispatchEvent(new Event('change', { bubbles: true }))
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
// Component should reflect the change
|
|
379
|
+
await expect(maleRadio).toBeChecked()
|
|
380
|
+
|
|
381
|
+
const valueProperty = await page.evaluate(() => {
|
|
382
|
+
const element = document.querySelector('form-radiogroup') as any
|
|
383
|
+
return element.value
|
|
384
|
+
})
|
|
385
|
+
expect(valueProperty).toBe('male')
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
test('fires change events on radio interaction', async ({ page }) => {
|
|
389
|
+
// Set up event listener
|
|
390
|
+
await page.evaluate(() => {
|
|
391
|
+
;(window as any).changeEventCount = 0
|
|
392
|
+
const radiogroup = document.querySelector('form-radiogroup')
|
|
393
|
+
radiogroup?.addEventListener('change', () => {
|
|
394
|
+
;(window as any).changeEventCount++
|
|
395
|
+
})
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
const femaleRadio = page
|
|
399
|
+
.locator('form-radiogroup input[value="female"]')
|
|
400
|
+
.first()
|
|
401
|
+
|
|
402
|
+
// Click should fire change event
|
|
403
|
+
await femaleRadio.click()
|
|
404
|
+
|
|
405
|
+
let changeEventCount = await page.evaluate(
|
|
406
|
+
() => (window as any).changeEventCount,
|
|
407
|
+
)
|
|
408
|
+
expect(changeEventCount).toBe(1)
|
|
409
|
+
|
|
410
|
+
// Click different radio should fire another change event
|
|
411
|
+
const maleRadio = page
|
|
412
|
+
.locator('form-radiogroup input[value="male"]')
|
|
413
|
+
.first()
|
|
414
|
+
await maleRadio.click()
|
|
415
|
+
|
|
416
|
+
changeEventCount = await page.evaluate(
|
|
417
|
+
() => (window as any).changeEventCount,
|
|
418
|
+
)
|
|
419
|
+
expect(changeEventCount).toBe(2)
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
test('handles form integration', async ({ page }) => {
|
|
423
|
+
// Add a form wrapper and test form data
|
|
424
|
+
await page.evaluate(() => {
|
|
425
|
+
const form = document.createElement('form')
|
|
426
|
+
const radiogroup = document.querySelector('form-radiogroup')
|
|
427
|
+
if (radiogroup) {
|
|
428
|
+
radiogroup.parentNode?.insertBefore(form, radiogroup)
|
|
429
|
+
form.appendChild(radiogroup)
|
|
430
|
+
}
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
const femaleRadio = page
|
|
434
|
+
.locator('form-radiogroup input[value="female"]')
|
|
435
|
+
.first()
|
|
436
|
+
await femaleRadio.click()
|
|
437
|
+
|
|
438
|
+
// Test form data includes the selected radio value
|
|
439
|
+
const formData = await page.evaluate(() => {
|
|
440
|
+
const form = document.querySelector('form')
|
|
441
|
+
if (!form) return null
|
|
442
|
+
const data = new FormData(form)
|
|
443
|
+
return Object.fromEntries(data.entries())
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
expect(formData).toEqual({ gender: 'female' })
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
test('handles keyboard navigation without affecting page scroll', async ({
|
|
450
|
+
page,
|
|
451
|
+
}) => {
|
|
452
|
+
const radiogroupComponent = page.locator('form-radiogroup').first()
|
|
453
|
+
const otherRadio = radiogroupComponent.locator('input[value="other"]')
|
|
454
|
+
const femaleRadio = radiogroupComponent.locator('input[value="female"]')
|
|
455
|
+
|
|
456
|
+
// Focus the initially checked radio
|
|
457
|
+
await otherRadio.focus()
|
|
458
|
+
await expect(otherRadio).toBeFocused()
|
|
459
|
+
|
|
460
|
+
// Get initial scroll position
|
|
461
|
+
const initialScrollY = await page.evaluate(() => window.scrollY)
|
|
462
|
+
|
|
463
|
+
// Navigate with arrow keys (these should not scroll the page)
|
|
464
|
+
await page.keyboard.press('ArrowRight')
|
|
465
|
+
await expect(femaleRadio).toBeFocused()
|
|
466
|
+
|
|
467
|
+
await page.keyboard.press('Home')
|
|
468
|
+
await expect(femaleRadio).toBeFocused() // Should stay at first
|
|
469
|
+
|
|
470
|
+
await page.keyboard.press('End')
|
|
471
|
+
await expect(otherRadio).toBeFocused() // Should move to last
|
|
472
|
+
|
|
473
|
+
// Verify page didn't scroll (meaning default behavior was prevented)
|
|
474
|
+
const finalScrollY = await page.evaluate(() => window.scrollY)
|
|
475
|
+
expect(finalScrollY).toBe(initialScrollY)
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
test('handles rapid selection changes', async ({ page }) => {
|
|
479
|
+
const radiogroupComponent = page.locator('form-radiogroup').first()
|
|
480
|
+
const femaleRadio = radiogroupComponent.locator('input[value="female"]')
|
|
481
|
+
const maleRadio = radiogroupComponent.locator('input[value="male"]')
|
|
482
|
+
const otherRadio = radiogroupComponent.locator('input[value="other"]')
|
|
483
|
+
|
|
484
|
+
// Rapid clicks between different radios
|
|
485
|
+
await femaleRadio.click()
|
|
486
|
+
await maleRadio.click()
|
|
487
|
+
await otherRadio.click()
|
|
488
|
+
await femaleRadio.click()
|
|
489
|
+
|
|
490
|
+
// Should end up with female selected
|
|
491
|
+
await expect(femaleRadio).toBeChecked()
|
|
492
|
+
await expect(maleRadio).not.toBeChecked()
|
|
493
|
+
await expect(otherRadio).not.toBeChecked()
|
|
494
|
+
|
|
495
|
+
const finalValue = await page.evaluate(() => {
|
|
496
|
+
const element = document.querySelector('form-radiogroup') as any
|
|
497
|
+
return element.value
|
|
498
|
+
})
|
|
499
|
+
expect(finalValue).toBe('female')
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
test('maintains focus management after DOM changes', async ({ page }) => {
|
|
503
|
+
const radiogroupComponent = page.locator('form-radiogroup').first()
|
|
504
|
+
const femaleRadio = radiogroupComponent.locator('input[value="female"]')
|
|
505
|
+
|
|
506
|
+
// Focus and select female (index 0)
|
|
507
|
+
await femaleRadio.focus()
|
|
508
|
+
await femaleRadio.click()
|
|
509
|
+
|
|
510
|
+
// Verify focus management is still working - should move to next index
|
|
511
|
+
await page.keyboard.press('ArrowRight')
|
|
512
|
+
const maleRadio = radiogroupComponent.locator('input[value="male"]')
|
|
513
|
+
await expect(maleRadio).toBeFocused()
|
|
514
|
+
})
|
|
515
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Collection,
|
|
3
|
+
type Component,
|
|
4
|
+
createSensor,
|
|
5
|
+
defineComponent,
|
|
6
|
+
read,
|
|
7
|
+
setProperty,
|
|
8
|
+
toggleClass,
|
|
9
|
+
} from '../..'
|
|
10
|
+
import { manageFocus } from '../_common/focus'
|
|
11
|
+
|
|
12
|
+
export type FormRadiogroupProps = {
|
|
13
|
+
readonly value: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type FormRadiogroupUI = {
|
|
17
|
+
radios: Collection<HTMLInputElement>
|
|
18
|
+
labels: Collection<HTMLLabelElement>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
declare global {
|
|
22
|
+
interface HTMLElementTagNameMap {
|
|
23
|
+
'form-radiogroup': Component<FormRadiogroupProps>
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const getIndex = (radios: Collection<HTMLInputElement>) =>
|
|
28
|
+
radios.get().findIndex(radio => radio.checked)
|
|
29
|
+
|
|
30
|
+
export default defineComponent<FormRadiogroupProps, FormRadiogroupUI>(
|
|
31
|
+
'form-radiogroup',
|
|
32
|
+
{
|
|
33
|
+
value: createSensor(
|
|
34
|
+
read(({ radios }) => radios[getIndex(radios)]?.value, ''),
|
|
35
|
+
'radios',
|
|
36
|
+
{
|
|
37
|
+
change: ({ target }) => target.value,
|
|
38
|
+
},
|
|
39
|
+
),
|
|
40
|
+
},
|
|
41
|
+
({ all }) => ({
|
|
42
|
+
radios: all(
|
|
43
|
+
'input[type="radio"]',
|
|
44
|
+
'Add at least two native radio buttons.',
|
|
45
|
+
),
|
|
46
|
+
labels: all('label', 'Wrap radio buttons with labels.'),
|
|
47
|
+
}),
|
|
48
|
+
({ host, radios }) => ({
|
|
49
|
+
host: manageFocus(radios, getIndex),
|
|
50
|
+
radios: setProperty('tabIndex', target =>
|
|
51
|
+
target.value === host.value ? 0 : -1,
|
|
52
|
+
),
|
|
53
|
+
labels: toggleClass(
|
|
54
|
+
'selected',
|
|
55
|
+
target => host.value === target.querySelector('input')?.value,
|
|
56
|
+
),
|
|
57
|
+
}),
|
|
58
|
+
)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
form-spinbutton {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
max-width: 7rem;
|
|
4
|
+
|
|
5
|
+
> input {
|
|
6
|
+
box-sizing: border-box;
|
|
7
|
+
margin: 0;
|
|
8
|
+
border-width: 1px 0;
|
|
9
|
+
border-style: solid;
|
|
10
|
+
border-color: var(--color-border);
|
|
11
|
+
height: var(--input-height);
|
|
12
|
+
min-width: 3rem;
|
|
13
|
+
text-align: center;
|
|
14
|
+
font-size: var(--font-size-s);
|
|
15
|
+
padding: 0 var(--space-s);
|
|
16
|
+
line-height: 2;
|
|
17
|
+
flex-grow: 1;
|
|
18
|
+
appearance: textfield;
|
|
19
|
+
-moz-appearance: textfield;
|
|
20
|
+
|
|
21
|
+
&::-webkit-outer-spin-button,
|
|
22
|
+
&::-webkit-inner-spin-button {
|
|
23
|
+
-webkit-appearance: none;
|
|
24
|
+
margin: 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
&:first-child {
|
|
28
|
+
border-width: 1px;
|
|
29
|
+
min-width: 2.5rem;
|
|
30
|
+
margin-right: 0.5rem;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
> button {
|
|
35
|
+
flex-grow: 0;
|
|
36
|
+
box-sizing: border-box;
|
|
37
|
+
height: var(--input-height);
|
|
38
|
+
min-width: var(--input-height);
|
|
39
|
+
border: 1px solid var(--color-border);
|
|
40
|
+
background-color: var(--color-secondary);
|
|
41
|
+
color: var(--color-text);
|
|
42
|
+
padding: 0 var(--space-s);
|
|
43
|
+
font-size: var(--font-size-s);
|
|
44
|
+
line-height: var(--line-height-s);
|
|
45
|
+
white-space: nowrap;
|
|
46
|
+
cursor: pointer;
|
|
47
|
+
transition: all var(--transition-shorter) var(--easing-inout);
|
|
48
|
+
|
|
49
|
+
&:disabled {
|
|
50
|
+
opacity: var(--opacity-translucent);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
&:not(:disabled) {
|
|
54
|
+
cursor: pointer;
|
|
55
|
+
opacity: var(--opacity-solid);
|
|
56
|
+
|
|
57
|
+
&:hover {
|
|
58
|
+
background-color: var(--color-secondary-hover);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
&:active {
|
|
62
|
+
background-color: var(--color-secondary-active);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
&:first-of-type {
|
|
67
|
+
border-radius: var(--space-xs) 0 0 var(--space-xs);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
&:last-of-type {
|
|
71
|
+
border-radius: 0 var(--space-xs) var(--space-xs) 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
&:not([hidden]) + button:last-of-type {
|
|
75
|
+
border-left-width: 0;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
[hidden] + button {
|
|
80
|
+
flex-grow: 1;
|
|
81
|
+
min-width: 7rem;
|
|
82
|
+
border-radius: var(--space-xs);
|
|
83
|
+
color: var(--color-text-inverted);
|
|
84
|
+
background-color: var(--color-primary);
|
|
85
|
+
border-color: var(--color-primary-active);
|
|
86
|
+
|
|
87
|
+
&:hover {
|
|
88
|
+
background-color: var(--color-primary-hover);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
&:active {
|
|
92
|
+
background-color: var(--color-primary-active);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|