diff --git a/.changeset/nine-snails-beg.md b/.changeset/nine-snails-beg.md new file mode 100644 index 0000000000..b49c6974ba --- /dev/null +++ b/.changeset/nine-snails-beg.md @@ -0,0 +1,5 @@ +--- +'@baloise/ds-core': patch +--- + +**tabs**: only show line when value exists diff --git a/.changeset/olive-olives-peel.md b/.changeset/olive-olives-peel.md new file mode 100644 index 0000000000..3d01fa7a4c --- /dev/null +++ b/.changeset/olive-olives-peel.md @@ -0,0 +1,5 @@ +--- +'@baloise/ds-core': patch +--- + +**carousel**: improve keyboard inputs and a11y criterias diff --git a/.changeset/slow-lies-guess.md b/.changeset/slow-lies-guess.md new file mode 100644 index 0000000000..a4846d0b13 --- /dev/null +++ b/.changeset/slow-lies-guess.md @@ -0,0 +1,5 @@ +--- +'@baloise/ds-core': patch +--- + +**tabs**: improve keyboard navigation according to a11y criterias diff --git a/.changeset/small-scissors-sleep.md b/.changeset/small-scissors-sleep.md new file mode 100644 index 0000000000..d8c51591c6 --- /dev/null +++ b/.changeset/small-scissors-sleep.md @@ -0,0 +1,5 @@ +--- +'@baloise/ds-core': patch +--- + +**carousel**: implement role list and listitem to improve screenreaders diff --git a/docs/.storybook/blocks/css-utils/CssElevation.tsx b/docs/.storybook/blocks/css-utils/CssElevation.tsx index c1022b9903..7975b282ac 100644 --- a/docs/.storybook/blocks/css-utils/CssElevation.tsx +++ b/docs/.storybook/blocks/css-utils/CssElevation.tsx @@ -18,7 +18,6 @@ export const CssElevationShadow = () => ( if (item.property === 'box-shadow') { return
} - console.log(item) return
Aa
}} /> diff --git a/e2e/cypress/e2e/a11y/bal-tabs.a11y.cy.ts b/e2e/cypress/e2e/a11y/bal-tabs.a11y.cy.ts index d48c221c31..50a613552f 100644 --- a/e2e/cypress/e2e/a11y/bal-tabs.a11y.cy.ts +++ b/e2e/cypress/e2e/a11y/bal-tabs.a11y.cy.ts @@ -6,10 +6,6 @@ describe('bal-tabs', () => { it('tabs basic', () => { cy.getByTestId('basic').testA11y() }) - - it('tabs vertical', () => { - cy.getByTestId('vertical').testA11y() - }) }) }) }) diff --git a/packages/core/package.json b/packages/core/package.json index fcea1c84c7..8bb590c741 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -25,7 +25,7 @@ "loader/" ], "scripts": { - "bubu": "vitest --ui" + "test:ui": "vitest --ui" }, "dependencies": { "@baloise/ds-styles": "16.3.0", diff --git a/packages/core/src/components.d.ts b/packages/core/src/components.d.ts index a08dc2e8a4..67d11bc68b 100644 --- a/packages/core/src/components.d.ts +++ b/packages/core/src/components.d.ts @@ -7,7 +7,7 @@ import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; import { BalConfigState } from "./utils/config"; import { AccordionState, BalAriaForm as BalAriaForm1, BalConfigState as BalConfigState1 } from "./interfaces"; -import { BalCarouselItemData } from "./components/bal-carousel/bal-carousel.type"; +import { BalCarouselItemData, BalSlide } from "./components/bal-carousel/bal-carousel.type"; import { BalCheckboxOption } from "./components/bal-checkbox/bal-checkbox.type"; import { BalAriaForm } from "./utils/form"; import { BalOption } from "./utils/dropdown"; @@ -18,7 +18,7 @@ import { BalStepOption } from "./components/bal-steps/bal-step.type"; import { BalTabOption } from "./components/bal-tabs/bal-tab.type"; export { BalConfigState } from "./utils/config"; export { AccordionState, BalAriaForm as BalAriaForm1, BalConfigState as BalConfigState1 } from "./interfaces"; -export { BalCarouselItemData } from "./components/bal-carousel/bal-carousel.type"; +export { BalCarouselItemData, BalSlide } from "./components/bal-carousel/bal-carousel.type"; export { BalCheckboxOption } from "./components/bal-checkbox/bal-checkbox.type"; export { BalAriaForm } from "./utils/form"; export { BalOption } from "./utils/dropdown"; @@ -378,6 +378,10 @@ export namespace Components { * If `true` the carousel uses the full height */ "fullHeight": boolean; + /** + * Defines the role of the carousel. + */ + "htmlRole": 'tablist' | 'list' | ''; /** * Defines special looks. */ @@ -390,11 +394,11 @@ export namespace Components { * Defines how many slides are visible in the container for the user. `auto` will use the size of the actual item content */ "itemsPerView": 'auto' | 1 | 2 | 3 | 4; - "next": (steps?: number) => Promise; + "next": (steps?: number) => Promise; /** * PUBLIC METHODS ------------------------------------------------------ */ - "previous": (steps?: number) => Promise; + "previous": (steps?: number) => Promise; /** * If `true` vertical scrolling on mobile is enabled. */ @@ -426,6 +430,10 @@ export namespace Components { * Specifies the URL of the page the link goes to */ "href"?: string; + /** + * Defines the role of the carousel. + */ + "htmlRole": 'tab' | 'listitem' | ''; /** * Label of the slide which will be used for pagination tabs */ @@ -438,6 +446,7 @@ export namespace Components { * Specifies the relationship of the target object to the link object. The value is a space-separated list of [link types](https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types). */ "rel"?: string; + "setFocus": () => Promise; /** * Src path to the image */ @@ -3148,7 +3157,7 @@ export namespace Components { */ "border": boolean; /** - * If `true` the tabs or steps can be clicked. + * If `true` the tabs or tabs can be clicked. */ "clickable": boolean; "closeAccordion": () => Promise; @@ -3190,7 +3199,7 @@ export namespace Components { */ "optionalTabSelection": boolean; /** - * Steps can be passed as a property or through HTML markup. + * Tabs can be passed as a property or through HTML markup. */ "options": BalTabOption[]; /** @@ -5402,6 +5411,10 @@ declare namespace LocalJSX { * If `true` the carousel uses the full height */ "fullHeight"?: boolean; + /** + * Defines the role of the carousel. + */ + "htmlRole"?: 'tablist' | 'list' | ''; /** * Defines special looks. */ @@ -5448,6 +5461,10 @@ declare namespace LocalJSX { * Specifies the URL of the page the link goes to */ "href"?: string; + /** + * Defines the role of the carousel. + */ + "htmlRole"?: 'tab' | 'listitem' | ''; /** * Label of the slide which will be used for pagination tabs */ @@ -8179,7 +8196,7 @@ declare namespace LocalJSX { */ "border"?: boolean; /** - * If `true` the tabs or steps can be clicked. + * If `true` the tabs or tabs can be clicked. */ "clickable"?: boolean; /** @@ -8227,7 +8244,7 @@ declare namespace LocalJSX { */ "optionalTabSelection"?: boolean; /** - * Steps can be passed as a property or through HTML markup. + * Tabs can be passed as a property or through HTML markup. */ "options"?: BalTabOption[]; /** diff --git a/packages/core/src/components/bal-carousel/bal-carousel-item/bal-carousel-item.tsx b/packages/core/src/components/bal-carousel/bal-carousel-item/bal-carousel-item.tsx index 9038b100a5..5ae7be6882 100644 --- a/packages/core/src/components/bal-carousel/bal-carousel-item/bal-carousel-item.tsx +++ b/packages/core/src/components/bal-carousel/bal-carousel-item/bal-carousel-item.tsx @@ -2,6 +2,7 @@ import { Component, ComponentInterface, h, Host, Method, Element, Prop, Event, E import { BEM } from '../../../utils/bem' import { BalCarouselItemData } from '../bal-carousel.type' import { Attributes } from '../../../interfaces' +import { waitAfterFramePaint } from '../../../utils/helpers' import { inheritAttributes } from '../../../utils/attributes' @Component({ @@ -9,6 +10,7 @@ import { inheritAttributes } from '../../../utils/attributes' }) export class CarouselItem implements ComponentInterface { private imageInheritAttributes: Attributes = {} + private buttonEl: HTMLButtonElement | HTMLLinkElement @Element() el!: HTMLElement @@ -22,6 +24,11 @@ export class CarouselItem implements ComponentInterface { */ @Prop({ reflect: true }) label = '' + /** + * Defines the role of the carousel. + */ + @Prop() htmlRole: 'tab' | 'listitem' | '' = 'listitem' + /** * The type of button. */ @@ -93,6 +100,14 @@ export class CarouselItem implements ComponentInterface { } } + @Method() + async setFocus(): Promise { + await waitAfterFramePaint() + if (this.buttonEl) { + this.buttonEl.focus() + } + } + private onClick = (ev: MouseEvent) => { if (this.href !== undefined) { this.balNavigate.emit(ev) @@ -115,7 +130,7 @@ export class CarouselItem implements ComponentInterface { if (!isProduct) { return ( - + {this.src !== undefined ? ( false} src={this.src} {...this.imageInheritAttributes} /> ) : ( @@ -144,7 +159,7 @@ export class CarouselItem implements ComponentInterface { } return ( - + (this.buttonEl = el)} > {this.src !== undefined ? ( false} + aria-hidden="true" src={this.src} {...this.imageInheritAttributes} /> diff --git a/packages/core/src/components/bal-carousel/bal-carousel.tsx b/packages/core/src/components/bal-carousel/bal-carousel.tsx index f2a16f2018..b6ee653870 100644 --- a/packages/core/src/components/bal-carousel/bal-carousel.tsx +++ b/packages/core/src/components/bal-carousel/bal-carousel.tsx @@ -42,6 +42,7 @@ export class Carousel private previousTransformValue = 0 private currentRaf: number | undefined private carouselId = `bal-carousel-${CarouselIds++}` + private carouselContainerId = `bal-carousel-${CarouselIds++}-container` @State() isLastSlideVisible = true @State() isMobile = balBreakpoints.isMobile @@ -75,6 +76,11 @@ export class Carousel */ @Prop() controls: 'small' | 'large' | 'dots' | 'tabs' | 'none' = 'none' + /** + * Defines the role of the carousel. + */ + @Prop() htmlRole: 'tablist' | 'list' | '' = 'list' + /** * If `true` items move under the controls, instead of having a gap */ @@ -165,6 +171,19 @@ export class Carousel this.itemsChanged() } + @Listen('keydown') + listenToKeyDown(ev: KeyboardEvent) { + if (this.htmlRole !== 'tablist') { + if (ev.code === 'Tab') { + if (ev.shiftKey) { + this.focusPreviousItem(ev) + } else { + this.focusNextItem(ev) + } + } + } + } + /** * @internal define config for the component */ @@ -180,7 +199,7 @@ export class Carousel */ @Method() - async previous(steps = this.steps): Promise { + async previous(steps = this.steps): Promise { let previousValue = this.value - steps if (previousValue < 0) { previousValue = 0 @@ -190,19 +209,17 @@ export class Carousel if (activeSlide) { const didAnimate = await this.animate(activeSlide.transformActive, true) - if (this.value > 0) { + if (didAnimate || this.value !== previousValue) { this.value = previousValue - if (!didAnimate) { - this.previous() - } else { - this.balChange.emit(this.value) - } + this.balChange.emit(this.value) } } + + return activeSlide } @Method() - async next(steps = this.steps): Promise { + async next(steps = this.steps): Promise { const items = this.getAllItemElements() const length = items.length let nextValue = this.value + steps @@ -215,11 +232,13 @@ export class Carousel if (activeSlide) { const didAnimate = await this.animate(activeSlide.transformActive, true) - if (didAnimate) { + if (didAnimate || this.value !== nextValue) { this.value = nextValue this.balChange.emit(this.value) } } + + return activeSlide } /** @@ -321,6 +340,26 @@ export class Carousel } } + private async focusNextItem(ev: KeyboardEvent) { + if (!this.isLast) { + const slide = await this.next(1) + if (slide && slide.el) { + stopEventBubbling(ev) + await slide.el.setFocus() + } + } + } + + private async focusPreviousItem(ev: KeyboardEvent) { + if (!this.isFirst) { + const slide = await this.previous(1) + if (slide && slide.el) { + stopEventBubbling(ev) + await slide.el.setFocus() + } + } + } + /** * GETTERS * ------------------------------------------------------ @@ -407,6 +446,7 @@ export class Carousel this.onControlChange(item.value)} > ) : ( @@ -425,6 +465,9 @@ export class Carousel ref={el => (this.innerEl = el)} >