@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,523 @@
|
|
|
1
|
+
import { expect, test } from '@playwright/test'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Test Suite: module-carousel Component
|
|
5
|
+
*
|
|
6
|
+
* Comprehensive tests for the Le Truc module-carousel component, which provides
|
|
7
|
+
* an accessible carousel/slideshow interface with multiple navigation methods:
|
|
8
|
+
* - Button navigation (prev/next)
|
|
9
|
+
* - Dot navigation (direct slide selection)
|
|
10
|
+
* - Keyboard navigation (arrow keys, Home/End)
|
|
11
|
+
* - Scroll-based navigation (intersection observer)
|
|
12
|
+
*
|
|
13
|
+
* Key Features Tested:
|
|
14
|
+
* - ✅ Initial state rendering and ARIA compliance
|
|
15
|
+
* - ✅ Navigation wrapping (first ↔ last slide)
|
|
16
|
+
* - ✅ Reactive index property (writable, not readonly sensor)
|
|
17
|
+
* - ✅ Smooth scroll animations and intersection observer updates
|
|
18
|
+
* - ✅ ARIA attributes synchronization (aria-current, aria-selected, tabindex)
|
|
19
|
+
* - ✅ Keyboard accessibility (roving tab focus pattern)
|
|
20
|
+
* - ✅ State consistency across different navigation methods
|
|
21
|
+
* - ✅ Edge case handling (sequential navigation, timing issues)
|
|
22
|
+
*
|
|
23
|
+
* Architecture Notes:
|
|
24
|
+
* - Uses `asInteger` parser with DOM reader function (not readonly sensor)
|
|
25
|
+
* - Smooth scroll animations require timing considerations in tests
|
|
26
|
+
* - IntersectionObserver updates index based on scroll position
|
|
27
|
+
* - Supports proper ARIA carousel patterns for accessibility
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
test.describe('module-carousel component', () => {
|
|
31
|
+
test.beforeEach(async ({ page }) => {
|
|
32
|
+
page.on('console', msg => {
|
|
33
|
+
console.log(`[browser] ${msg.type()}: ${msg.text()}`)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
await page.goto('http://localhost:3000/test/module-carousel.html')
|
|
37
|
+
await page.waitForSelector('module-carousel')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test.describe('Initial State', () => {
|
|
41
|
+
test('renders carousel with correct initial state', async ({ page }) => {
|
|
42
|
+
const carousel = page.locator('module-carousel')
|
|
43
|
+
const slides = carousel.locator('[role="tabpanel"]')
|
|
44
|
+
const dots = carousel.locator('[role="tab"]')
|
|
45
|
+
const prevButton = carousel.locator('button.prev')
|
|
46
|
+
const nextButton = carousel.locator('button.next')
|
|
47
|
+
|
|
48
|
+
// Should have correct number of slides and dots
|
|
49
|
+
await expect(slides).toHaveCount(3)
|
|
50
|
+
await expect(dots).toHaveCount(3)
|
|
51
|
+
|
|
52
|
+
// Should have navigation buttons
|
|
53
|
+
await expect(prevButton).toBeVisible()
|
|
54
|
+
await expect(nextButton).toBeVisible()
|
|
55
|
+
|
|
56
|
+
// First slide should be current
|
|
57
|
+
const firstSlide = slides.first()
|
|
58
|
+
await expect(firstSlide).toHaveAttribute('aria-current', 'true')
|
|
59
|
+
|
|
60
|
+
// First dot should be selected
|
|
61
|
+
const firstDot = dots.first()
|
|
62
|
+
await expect(firstDot).toHaveAttribute('aria-selected', 'true')
|
|
63
|
+
await expect(firstDot).toHaveAttribute('tabindex', '0')
|
|
64
|
+
|
|
65
|
+
// Other dots should not be selected
|
|
66
|
+
const secondDot = dots.nth(1)
|
|
67
|
+
const thirdDot = dots.nth(2)
|
|
68
|
+
await expect(secondDot).toHaveAttribute('aria-selected', 'false')
|
|
69
|
+
await expect(thirdDot).toHaveAttribute('aria-selected', 'false')
|
|
70
|
+
await expect(secondDot).toHaveAttribute('tabindex', '-1')
|
|
71
|
+
await expect(thirdDot).toHaveAttribute('tabindex', '-1')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('reads initial index from aria-current attribute', async ({
|
|
75
|
+
page,
|
|
76
|
+
}) => {
|
|
77
|
+
// Get the initial index from the component
|
|
78
|
+
const initialIndex = await page.evaluate(() => {
|
|
79
|
+
const carousel = document.querySelector('module-carousel')
|
|
80
|
+
return carousel?.index
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
// Should start at index 0 (first slide has aria-current="true")
|
|
84
|
+
expect(initialIndex).toBe(0)
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test.describe('Button Navigation', () => {
|
|
89
|
+
test('navigates forward with next button', async ({ page }) => {
|
|
90
|
+
const carousel = page.locator('module-carousel')
|
|
91
|
+
const nextButton = carousel.locator('button.next')
|
|
92
|
+
const slides = carousel.locator('[role="tabpanel"]')
|
|
93
|
+
const dots = carousel.locator('[role="tab"]')
|
|
94
|
+
|
|
95
|
+
// Click next button
|
|
96
|
+
await nextButton.click()
|
|
97
|
+
|
|
98
|
+
// Wait for navigation
|
|
99
|
+
|
|
100
|
+
// Second slide should be current
|
|
101
|
+
const secondSlide = slides.nth(1)
|
|
102
|
+
await expect(secondSlide).toHaveAttribute('aria-current', 'true')
|
|
103
|
+
|
|
104
|
+
// First slide should not be current
|
|
105
|
+
const firstSlide = slides.first()
|
|
106
|
+
await expect(firstSlide).toHaveAttribute('aria-current', 'false')
|
|
107
|
+
|
|
108
|
+
// Second dot should be selected
|
|
109
|
+
const secondDot = dots.nth(1)
|
|
110
|
+
await expect(secondDot).toHaveAttribute('aria-selected', 'true')
|
|
111
|
+
await expect(secondDot).toHaveAttribute('tabindex', '0')
|
|
112
|
+
|
|
113
|
+
// First dot should not be selected
|
|
114
|
+
const firstDot = dots.first()
|
|
115
|
+
await expect(firstDot).toHaveAttribute('aria-selected', 'false')
|
|
116
|
+
await expect(firstDot).toHaveAttribute('tabindex', '-1')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('navigates backward with prev button', async ({ page }) => {
|
|
120
|
+
const carousel = page.locator('module-carousel')
|
|
121
|
+
const nextButton = carousel.locator('button.next')
|
|
122
|
+
const prevButton = carousel.locator('button.prev')
|
|
123
|
+
const slides = carousel.locator('[role="tabpanel"]')
|
|
124
|
+
|
|
125
|
+
// Go to second slide first
|
|
126
|
+
await nextButton.click()
|
|
127
|
+
|
|
128
|
+
// Click prev button
|
|
129
|
+
await prevButton.click()
|
|
130
|
+
|
|
131
|
+
// First slide should be current again
|
|
132
|
+
const firstSlide = slides.first()
|
|
133
|
+
await expect(firstSlide).toHaveAttribute('aria-current', 'true')
|
|
134
|
+
|
|
135
|
+
// Component index should be 0
|
|
136
|
+
const currentIndex = await page.evaluate(() => {
|
|
137
|
+
const carousel = document.querySelector('module-carousel')
|
|
138
|
+
return carousel?.index
|
|
139
|
+
})
|
|
140
|
+
expect(currentIndex).toBe(0)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test('wraps around from last to first slide', async ({ page }) => {
|
|
144
|
+
const carousel = page.locator('module-carousel')
|
|
145
|
+
const nextButton = carousel.locator('button.next')
|
|
146
|
+
const slides = carousel.locator('[role="tabpanel"]')
|
|
147
|
+
|
|
148
|
+
// Navigate to last slide (index 2)
|
|
149
|
+
await nextButton.click()
|
|
150
|
+
await nextButton.click()
|
|
151
|
+
|
|
152
|
+
// Third slide should be current
|
|
153
|
+
const thirdSlide = slides.nth(2)
|
|
154
|
+
await expect(thirdSlide).toHaveAttribute('aria-current', 'true')
|
|
155
|
+
|
|
156
|
+
// Click next again to wrap around
|
|
157
|
+
await nextButton.click()
|
|
158
|
+
|
|
159
|
+
// Should wrap to first slide
|
|
160
|
+
const firstSlide = slides.first()
|
|
161
|
+
await expect(firstSlide).toHaveAttribute('aria-current', 'true')
|
|
162
|
+
|
|
163
|
+
const currentIndex = await page.evaluate(() => {
|
|
164
|
+
const carousel = document.querySelector('module-carousel')
|
|
165
|
+
return carousel?.index
|
|
166
|
+
})
|
|
167
|
+
expect(currentIndex).toBe(0)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
test('wraps around from first to last slide', async ({ page }) => {
|
|
171
|
+
const carousel = page.locator('module-carousel')
|
|
172
|
+
const prevButton = carousel.locator('button.prev')
|
|
173
|
+
const slides = carousel.locator('[role="tabpanel"]')
|
|
174
|
+
|
|
175
|
+
// Click prev from first slide to wrap around
|
|
176
|
+
await prevButton.click()
|
|
177
|
+
|
|
178
|
+
// Should wrap to last slide
|
|
179
|
+
const thirdSlide = slides.nth(2)
|
|
180
|
+
await expect(thirdSlide).toHaveAttribute('aria-current', 'true')
|
|
181
|
+
|
|
182
|
+
const currentIndex = await page.evaluate(() => {
|
|
183
|
+
const carousel = document.querySelector('module-carousel')
|
|
184
|
+
return carousel?.index
|
|
185
|
+
})
|
|
186
|
+
expect(currentIndex).toBe(2)
|
|
187
|
+
})
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
test.describe('Dot Navigation', () => {
|
|
191
|
+
test('navigates to specific slide when dot is clicked', async ({
|
|
192
|
+
page,
|
|
193
|
+
}) => {
|
|
194
|
+
const carousel = page.locator('module-carousel')
|
|
195
|
+
const dots = carousel.locator('[role="tab"]')
|
|
196
|
+
const slides = carousel.locator('[role="tabpanel"]')
|
|
197
|
+
|
|
198
|
+
// Click third dot (index 2)
|
|
199
|
+
const thirdDot = dots.nth(2)
|
|
200
|
+
await thirdDot.click()
|
|
201
|
+
|
|
202
|
+
// Third slide should be current
|
|
203
|
+
const thirdSlide = slides.nth(2)
|
|
204
|
+
await expect(thirdSlide).toHaveAttribute('aria-current', 'true')
|
|
205
|
+
|
|
206
|
+
// Third dot should be selected
|
|
207
|
+
await expect(thirdDot).toHaveAttribute('aria-selected', 'true')
|
|
208
|
+
await expect(thirdDot).toHaveAttribute('tabindex', '0')
|
|
209
|
+
|
|
210
|
+
// Other dots should not be selected
|
|
211
|
+
const firstDot = dots.first()
|
|
212
|
+
const secondDot = dots.nth(1)
|
|
213
|
+
await expect(firstDot).toHaveAttribute('aria-selected', 'false')
|
|
214
|
+
await expect(secondDot).toHaveAttribute('aria-selected', 'false')
|
|
215
|
+
await expect(firstDot).toHaveAttribute('tabindex', '-1')
|
|
216
|
+
await expect(secondDot).toHaveAttribute('tabindex', '-1')
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
test('updates component index when dot is clicked', async ({ page }) => {
|
|
220
|
+
const carousel = page.locator('module-carousel')
|
|
221
|
+
const dots = carousel.locator('[role="tab"]')
|
|
222
|
+
|
|
223
|
+
// Click second dot
|
|
224
|
+
const secondDot = dots.nth(1)
|
|
225
|
+
await secondDot.click()
|
|
226
|
+
|
|
227
|
+
const currentIndex = await page.evaluate(() => {
|
|
228
|
+
const carousel = document.querySelector('module-carousel')
|
|
229
|
+
return carousel?.index
|
|
230
|
+
})
|
|
231
|
+
expect(currentIndex).toBe(1)
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
test.describe('Keyboard Navigation', () => {
|
|
236
|
+
test('navigates with arrow keys', async ({ page }) => {
|
|
237
|
+
const carousel = page.locator('module-carousel')
|
|
238
|
+
const slides = carousel.locator('[role="tabpanel"]')
|
|
239
|
+
|
|
240
|
+
// Focus on navigation buttons area and use arrow keys
|
|
241
|
+
const nextButton = carousel.locator('button.next')
|
|
242
|
+
await nextButton.focus()
|
|
243
|
+
|
|
244
|
+
// Press right arrow
|
|
245
|
+
await page.keyboard.press('ArrowRight')
|
|
246
|
+
|
|
247
|
+
// Second slide should be current
|
|
248
|
+
const secondSlide = slides.nth(1)
|
|
249
|
+
await expect(secondSlide).toHaveAttribute('aria-current', 'true')
|
|
250
|
+
|
|
251
|
+
// Press left arrow
|
|
252
|
+
await page.keyboard.press('ArrowLeft')
|
|
253
|
+
|
|
254
|
+
// First slide should be current again
|
|
255
|
+
const firstSlide = slides.first()
|
|
256
|
+
await expect(firstSlide).toHaveAttribute('aria-current', 'true')
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
test('navigates with Home and End keys', async ({ page }) => {
|
|
260
|
+
const carousel = page.locator('module-carousel')
|
|
261
|
+
const nextButton = carousel.locator('button.next')
|
|
262
|
+
const slides = carousel.locator('[role="tabpanel"]')
|
|
263
|
+
|
|
264
|
+
// Go to middle slide first
|
|
265
|
+
await nextButton.click()
|
|
266
|
+
|
|
267
|
+
// Focus and press End key
|
|
268
|
+
await nextButton.focus()
|
|
269
|
+
await page.keyboard.press('End')
|
|
270
|
+
|
|
271
|
+
// Last slide should be current
|
|
272
|
+
const thirdSlide = slides.nth(2)
|
|
273
|
+
await expect(thirdSlide).toHaveAttribute('aria-current', 'true')
|
|
274
|
+
|
|
275
|
+
// Press Home key
|
|
276
|
+
await page.keyboard.press('Home')
|
|
277
|
+
|
|
278
|
+
// First slide should be current
|
|
279
|
+
const firstSlide = slides.first()
|
|
280
|
+
await expect(firstSlide).toHaveAttribute('aria-current', 'true')
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
test('wraps around with arrow key navigation', async ({ page }) => {
|
|
284
|
+
const carousel = page.locator('module-carousel')
|
|
285
|
+
const nextButton = carousel.locator('button.next')
|
|
286
|
+
const slides = carousel.locator('[role="tabpanel"]')
|
|
287
|
+
|
|
288
|
+
// Navigate to last slide
|
|
289
|
+
await nextButton.click()
|
|
290
|
+
await nextButton.click()
|
|
291
|
+
|
|
292
|
+
// Focus and press right arrow to wrap around
|
|
293
|
+
await nextButton.focus()
|
|
294
|
+
await page.keyboard.press('ArrowRight')
|
|
295
|
+
|
|
296
|
+
// Should wrap to first slide
|
|
297
|
+
const firstSlide = slides.first()
|
|
298
|
+
await expect(firstSlide).toHaveAttribute('aria-current', 'true')
|
|
299
|
+
|
|
300
|
+
// Press left arrow to wrap around backwards
|
|
301
|
+
await page.keyboard.press('ArrowLeft')
|
|
302
|
+
|
|
303
|
+
// Should wrap to last slide
|
|
304
|
+
const thirdSlide = slides.nth(2)
|
|
305
|
+
await expect(thirdSlide).toHaveAttribute('aria-current', 'true')
|
|
306
|
+
})
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
test.describe('Scroll-based Navigation', () => {
|
|
310
|
+
test('updates index when slide is scrolled into view', async ({ page }) => {
|
|
311
|
+
const carousel = page.locator('module-carousel')
|
|
312
|
+
const slides = carousel.locator('[role="tabpanel"]')
|
|
313
|
+
|
|
314
|
+
// Use button navigation to trigger scroll behavior
|
|
315
|
+
const nextButton = carousel.locator('button.next')
|
|
316
|
+
await nextButton.click()
|
|
317
|
+
|
|
318
|
+
// Component index should be updated
|
|
319
|
+
const currentIndex = await page.evaluate(() => {
|
|
320
|
+
const carousel = document.querySelector('module-carousel')
|
|
321
|
+
return carousel?.index
|
|
322
|
+
})
|
|
323
|
+
expect(currentIndex).toBe(1)
|
|
324
|
+
|
|
325
|
+
// Second slide should have aria-current="true"
|
|
326
|
+
const secondSlide = slides.nth(1)
|
|
327
|
+
await expect(secondSlide).toHaveAttribute('aria-current', 'true')
|
|
328
|
+
})
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
test.describe('Component Properties', () => {
|
|
332
|
+
test('index property is writable and reactive', async ({ page }) => {
|
|
333
|
+
const carousel = page.locator('module-carousel')
|
|
334
|
+
const slides = carousel.locator('[role="tabpanel"]')
|
|
335
|
+
|
|
336
|
+
// Get initial index
|
|
337
|
+
const initialIndex = await page.evaluate(() => {
|
|
338
|
+
const carousel = document.querySelector('module-carousel')
|
|
339
|
+
return carousel?.index
|
|
340
|
+
})
|
|
341
|
+
expect(initialIndex).toBe(0)
|
|
342
|
+
|
|
343
|
+
// Set index directly (should work - writable property)
|
|
344
|
+
await page.evaluate(() => {
|
|
345
|
+
const carousel = document.querySelector('module-carousel')
|
|
346
|
+
carousel!.index = 2
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
// Wait for reactive updates
|
|
350
|
+
|
|
351
|
+
// Index should be updated
|
|
352
|
+
const updatedIndex = await page.evaluate(() => {
|
|
353
|
+
const carousel = document.querySelector('module-carousel')
|
|
354
|
+
return carousel?.index
|
|
355
|
+
})
|
|
356
|
+
expect(updatedIndex).toBe(2)
|
|
357
|
+
|
|
358
|
+
// Third slide should be current
|
|
359
|
+
const thirdSlide = slides.nth(2)
|
|
360
|
+
await expect(thirdSlide).toHaveAttribute('aria-current', 'true')
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
test('index property reflects DOM state', async ({ page }) => {
|
|
364
|
+
const carousel = page.locator('module-carousel')
|
|
365
|
+
const dots = carousel.locator('[role="tab"]')
|
|
366
|
+
|
|
367
|
+
// Click different dots and verify index updates
|
|
368
|
+
for (let i = 0; i < 3; i++) {
|
|
369
|
+
await dots.nth(i).click()
|
|
370
|
+
|
|
371
|
+
const currentIndex = await page.evaluate(() => {
|
|
372
|
+
const carousel = document.querySelector('module-carousel')
|
|
373
|
+
return carousel?.index
|
|
374
|
+
})
|
|
375
|
+
expect(currentIndex).toBe(i)
|
|
376
|
+
}
|
|
377
|
+
})
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
test.describe('ARIA and Accessibility', () => {
|
|
381
|
+
test('maintains proper ARIA attributes', async ({ page }) => {
|
|
382
|
+
const carousel = page.locator('module-carousel')
|
|
383
|
+
const slides = carousel.locator('[role="tabpanel"]')
|
|
384
|
+
const dots = carousel.locator('[role="tab"]')
|
|
385
|
+
const nextButton = carousel.locator('button.next')
|
|
386
|
+
|
|
387
|
+
// Check initial ARIA states
|
|
388
|
+
await expect(slides.first()).toHaveAttribute('aria-current', 'true')
|
|
389
|
+
await expect(slides.nth(1)).toHaveAttribute('aria-current', 'false')
|
|
390
|
+
await expect(slides.nth(2)).toHaveAttribute('aria-current', 'false')
|
|
391
|
+
|
|
392
|
+
await expect(dots.first()).toHaveAttribute('aria-selected', 'true')
|
|
393
|
+
await expect(dots.nth(1)).toHaveAttribute('aria-selected', 'false')
|
|
394
|
+
await expect(dots.nth(2)).toHaveAttribute('aria-selected', 'false')
|
|
395
|
+
|
|
396
|
+
// Navigate and check ARIA states update
|
|
397
|
+
await nextButton.click()
|
|
398
|
+
|
|
399
|
+
await expect(slides.first()).toHaveAttribute('aria-current', 'false')
|
|
400
|
+
await expect(slides.nth(1)).toHaveAttribute('aria-current', 'true')
|
|
401
|
+
await expect(slides.nth(2)).toHaveAttribute('aria-current', 'false')
|
|
402
|
+
|
|
403
|
+
await expect(dots.first()).toHaveAttribute('aria-selected', 'false')
|
|
404
|
+
await expect(dots.nth(1)).toHaveAttribute('aria-selected', 'true')
|
|
405
|
+
await expect(dots.nth(2)).toHaveAttribute('aria-selected', 'false')
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
test('maintains proper tabindex for roving tab focus', async ({ page }) => {
|
|
409
|
+
const carousel = page.locator('module-carousel')
|
|
410
|
+
const dots = carousel.locator('[role="tab"]')
|
|
411
|
+
const nextButton = carousel.locator('button.next')
|
|
412
|
+
|
|
413
|
+
// Check initial tabindex values
|
|
414
|
+
await expect(dots.first()).toHaveAttribute('tabindex', '0')
|
|
415
|
+
await expect(dots.nth(1)).toHaveAttribute('tabindex', '-1')
|
|
416
|
+
await expect(dots.nth(2)).toHaveAttribute('tabindex', '-1')
|
|
417
|
+
|
|
418
|
+
// Navigate and check tabindex updates
|
|
419
|
+
await nextButton.click()
|
|
420
|
+
|
|
421
|
+
await expect(dots.first()).toHaveAttribute('tabindex', '-1')
|
|
422
|
+
await expect(dots.nth(1)).toHaveAttribute('tabindex', '0')
|
|
423
|
+
await expect(dots.nth(2)).toHaveAttribute('tabindex', '-1')
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
test('has proper ARIA labels and controls', async ({ page }) => {
|
|
427
|
+
const carousel = page.locator('module-carousel')
|
|
428
|
+
const dots = carousel.locator('[role="tab"]')
|
|
429
|
+
const prevButton = carousel.locator('button.prev')
|
|
430
|
+
const nextButton = carousel.locator('button.next')
|
|
431
|
+
const nav = carousel.locator('nav')
|
|
432
|
+
|
|
433
|
+
// Check navigation has aria-label
|
|
434
|
+
await expect(nav).toHaveAttribute('aria-label', 'Carousel Navigation')
|
|
435
|
+
|
|
436
|
+
// Check buttons have labels
|
|
437
|
+
await expect(prevButton).toHaveAttribute('aria-label', 'Previous')
|
|
438
|
+
await expect(nextButton).toHaveAttribute('aria-label', 'Next')
|
|
439
|
+
|
|
440
|
+
// Check dots have proper labels and controls
|
|
441
|
+
for (let i = 0; i < 3; i++) {
|
|
442
|
+
const dot = dots.nth(i)
|
|
443
|
+
await expect(dot).toHaveAttribute('aria-label', `Slide ${i + 1}`)
|
|
444
|
+
await expect(dot).toHaveAttribute('aria-controls', `slide${i + 1}`)
|
|
445
|
+
}
|
|
446
|
+
})
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
test.describe('Edge Cases', () => {
|
|
450
|
+
test('handles empty or single slide gracefully', async ({ page }) => {
|
|
451
|
+
// This test assumes the fixture always has 3 slides
|
|
452
|
+
// In a real implementation, we might test with different fixtures
|
|
453
|
+
const slides = page.locator('module-carousel [role="tabpanel"]')
|
|
454
|
+
const slideCount = await slides.count()
|
|
455
|
+
|
|
456
|
+
// With our current fixture, should have 3 slides
|
|
457
|
+
expect(slideCount).toBe(3)
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
test('handles sequential navigation correctly', async ({ page }) => {
|
|
461
|
+
const carousel = page.locator('module-carousel')
|
|
462
|
+
const nextButton = carousel.locator('button.next')
|
|
463
|
+
|
|
464
|
+
// Track index through sequential navigation
|
|
465
|
+
let expectedIndex = 0
|
|
466
|
+
|
|
467
|
+
// Navigate through all slides sequentially
|
|
468
|
+
for (let i = 0; i < 5; i++) {
|
|
469
|
+
await nextButton.click()
|
|
470
|
+
|
|
471
|
+
expectedIndex = (expectedIndex + 1) % 3 // Wrap around at 3
|
|
472
|
+
|
|
473
|
+
const currentIndex = await page.evaluate(() => {
|
|
474
|
+
const carousel = document.querySelector('module-carousel')
|
|
475
|
+
return carousel?.index
|
|
476
|
+
})
|
|
477
|
+
expect(currentIndex).toBe(expectedIndex)
|
|
478
|
+
}
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
test('maintains state consistency across different navigation methods', async ({
|
|
482
|
+
page,
|
|
483
|
+
}) => {
|
|
484
|
+
const carousel = page.locator('module-carousel')
|
|
485
|
+
const nextButton = carousel.locator('button.next')
|
|
486
|
+
const dots = carousel.locator('[role="tab"]')
|
|
487
|
+
const slides = carousel.locator('[role="tabpanel"]')
|
|
488
|
+
|
|
489
|
+
// Use button navigation
|
|
490
|
+
await nextButton.click()
|
|
491
|
+
|
|
492
|
+
let currentIndex = await page.evaluate(() => {
|
|
493
|
+
const carousel = document.querySelector('module-carousel')
|
|
494
|
+
return carousel?.index
|
|
495
|
+
})
|
|
496
|
+
expect(currentIndex).toBe(1)
|
|
497
|
+
|
|
498
|
+
// Use dot navigation
|
|
499
|
+
await dots.nth(2).click()
|
|
500
|
+
|
|
501
|
+
currentIndex = await page.evaluate(() => {
|
|
502
|
+
const carousel = document.querySelector('module-carousel')
|
|
503
|
+
return carousel?.index
|
|
504
|
+
})
|
|
505
|
+
expect(currentIndex).toBe(2)
|
|
506
|
+
|
|
507
|
+
// Use keyboard navigation
|
|
508
|
+
await nextButton.focus()
|
|
509
|
+
await page.keyboard.press('Home')
|
|
510
|
+
|
|
511
|
+
currentIndex = await page.evaluate(() => {
|
|
512
|
+
const carousel = document.querySelector('module-carousel')
|
|
513
|
+
return carousel?.index
|
|
514
|
+
})
|
|
515
|
+
expect(currentIndex).toBe(0)
|
|
516
|
+
|
|
517
|
+
// Verify all UI elements are in sync
|
|
518
|
+
await expect(slides.first()).toHaveAttribute('aria-current', 'true')
|
|
519
|
+
await expect(dots.first()).toHaveAttribute('aria-selected', 'true')
|
|
520
|
+
await expect(dots.first()).toHaveAttribute('tabindex', '0')
|
|
521
|
+
})
|
|
522
|
+
})
|
|
523
|
+
})
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import {
|
|
2
|
+
asInteger,
|
|
3
|
+
type Collection,
|
|
4
|
+
type Component,
|
|
5
|
+
defineComponent,
|
|
6
|
+
on,
|
|
7
|
+
setProperty,
|
|
8
|
+
} from '../..'
|
|
9
|
+
|
|
10
|
+
export type ModuleCarouselProps = {
|
|
11
|
+
index: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type ModuleCarouselUI = {
|
|
15
|
+
dots: Collection<HTMLElement>
|
|
16
|
+
slides: Collection<HTMLElement>
|
|
17
|
+
buttons: Collection<HTMLElement>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
declare global {
|
|
21
|
+
interface HTMLElementTagNameMap {
|
|
22
|
+
'module-carousel': Component<ModuleCarouselProps>
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const wrapAround = (index: number, total: number) => (index + total) % total
|
|
27
|
+
|
|
28
|
+
export default defineComponent<ModuleCarouselProps, ModuleCarouselUI>(
|
|
29
|
+
'module-carousel',
|
|
30
|
+
{
|
|
31
|
+
index: asInteger(ui =>
|
|
32
|
+
Math.max(
|
|
33
|
+
ui.slides.get().findIndex(slide => slide.ariaCurrent === 'true'),
|
|
34
|
+
0,
|
|
35
|
+
),
|
|
36
|
+
),
|
|
37
|
+
},
|
|
38
|
+
({ all }) => ({
|
|
39
|
+
dots: all('[role="tab"]'),
|
|
40
|
+
slides: all('[role="tabpanel"]'),
|
|
41
|
+
buttons: all('nav button'),
|
|
42
|
+
}),
|
|
43
|
+
({ host, slides }) => {
|
|
44
|
+
const isCurrentDot = (target: HTMLElement) =>
|
|
45
|
+
target.dataset.index === String(host.index)
|
|
46
|
+
const scrollToCurrentSlide = () => {
|
|
47
|
+
slides[host.index].scrollIntoView({
|
|
48
|
+
behavior: 'smooth',
|
|
49
|
+
block: 'nearest',
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
// Register IntersectionObserver to update index based on scroll position
|
|
55
|
+
component: [
|
|
56
|
+
() => {
|
|
57
|
+
const observer = new IntersectionObserver(
|
|
58
|
+
entries => {
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
if (entry.isIntersecting) {
|
|
61
|
+
host.index = slides
|
|
62
|
+
.get()
|
|
63
|
+
.findIndex(slide => slide === entry.target)
|
|
64
|
+
break
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
root: host,
|
|
70
|
+
threshold: 0.5,
|
|
71
|
+
},
|
|
72
|
+
)
|
|
73
|
+
slides.get().forEach(slide => {
|
|
74
|
+
observer.observe(slide)
|
|
75
|
+
})
|
|
76
|
+
return () => {
|
|
77
|
+
observer.disconnect()
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
|
|
82
|
+
// Handle navigation button click and keyup events
|
|
83
|
+
buttons: [
|
|
84
|
+
on('click', ({ target }) => {
|
|
85
|
+
if (!(target instanceof HTMLElement)) return
|
|
86
|
+
const total = slides.length
|
|
87
|
+
const nextIndex = target.classList.contains('prev')
|
|
88
|
+
? host.index - 1
|
|
89
|
+
: target.classList.contains('next')
|
|
90
|
+
? host.index + 1
|
|
91
|
+
: parseInt(target.dataset.index || '0')
|
|
92
|
+
host.index = Number.isInteger(nextIndex)
|
|
93
|
+
? wrapAround(nextIndex, total)
|
|
94
|
+
: 0
|
|
95
|
+
scrollToCurrentSlide()
|
|
96
|
+
}),
|
|
97
|
+
on('keyup', e => {
|
|
98
|
+
const { key } = e
|
|
99
|
+
if (['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(key)) {
|
|
100
|
+
e.preventDefault()
|
|
101
|
+
e.stopPropagation()
|
|
102
|
+
const total = slides.length
|
|
103
|
+
const nextIndex =
|
|
104
|
+
key === 'Home'
|
|
105
|
+
? 0
|
|
106
|
+
: key === 'End'
|
|
107
|
+
? total - 1
|
|
108
|
+
: wrapAround(
|
|
109
|
+
host.index + (key === 'ArrowLeft' ? -1 : 1),
|
|
110
|
+
total,
|
|
111
|
+
)
|
|
112
|
+
slides[nextIndex].focus()
|
|
113
|
+
host.index = nextIndex
|
|
114
|
+
scrollToCurrentSlide()
|
|
115
|
+
}
|
|
116
|
+
}),
|
|
117
|
+
],
|
|
118
|
+
|
|
119
|
+
// Set the active slide in the navigation
|
|
120
|
+
dots: [
|
|
121
|
+
setProperty('ariaSelected', target => String(isCurrentDot(target))),
|
|
122
|
+
setProperty('tabIndex', target => (isCurrentDot(target) ? 0 : -1)),
|
|
123
|
+
],
|
|
124
|
+
|
|
125
|
+
// Set the active slide in the slides
|
|
126
|
+
slides: setProperty('ariaCurrent', target =>
|
|
127
|
+
String(target.id === slides[host.index].id),
|
|
128
|
+
),
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module-catalog {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: var(--space-l);
|
|
5
|
+
|
|
6
|
+
> header,
|
|
7
|
+
p {
|
|
8
|
+
margin: 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
& ul {
|
|
12
|
+
padding: 0;
|
|
13
|
+
margin: 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
& header,
|
|
17
|
+
li {
|
|
18
|
+
display: flex;
|
|
19
|
+
gap: var(--space-m);
|
|
20
|
+
justify-content: space-between;
|
|
21
|
+
}
|
|
22
|
+
}
|