Skip to content

Commit d8c2bc4

Browse files
♿ a11y(tabs, carousel): keyboard interaction (#1432)
* Create PR for #1425 * fix(tabs): only show line when value exists * fix(tabs): only show line when value exists * fix(tabs): improve keyboard navigation according to a11y criterias * chore: fix typo * Create PR for #1417 * fix(carousel): implement role list and listitem to improve screenreaders * fix(carousel): add missing aria controls * fix(carousel): improve keyboard inputs and a11y criterias * fix(carousel): improve keyboard inputs and a11y criterias * chore: add missing unit test --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Gery Hirschfeld <gerhard.hirschfeld@baloise.ch>
1 parent ac9a631 commit d8c2bc4

26 files changed

Lines changed: 422 additions & 97 deletions

.changeset/nine-snails-beg.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@baloise/ds-core': patch
3+
---
4+
5+
**tabs**: only show line when value exists

.changeset/olive-olives-peel.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@baloise/ds-core': patch
3+
---
4+
5+
**carousel**: improve keyboard inputs and a11y criterias

.changeset/slow-lies-guess.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@baloise/ds-core': patch
3+
---
4+
5+
**tabs**: improve keyboard navigation according to a11y criterias

.changeset/small-scissors-sleep.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@baloise/ds-core': patch
3+
---
4+
5+
**carousel**: implement role list and listitem to improve screenreaders

docs/.storybook/blocks/css-utils/CssElevation.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ export const CssElevationShadow = () => (
1818
if (item.property === 'box-shadow') {
1919
return <div className={`bg-green ${item.class} p-small`}></div>
2020
}
21-
console.log(item)
2221
return <div className={`font-weight-bold text-large ${item.class} px-small`}>Aa</div>
2322
}}
2423
/>

e2e/cypress/e2e/a11y/bal-tabs.a11y.cy.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@ describe('bal-tabs', () => {
66
it('tabs basic', () => {
77
cy.getByTestId('basic').testA11y()
88
})
9-
10-
it('tabs vertical', () => {
11-
cy.getByTestId('vertical').testA11y()
12-
})
139
})
1410
})
1511
})

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"loader/"
2626
],
2727
"scripts": {
28-
"bubu": "vitest --ui"
28+
"test:ui": "vitest --ui"
2929
},
3030
"dependencies": {
3131
"@baloise/ds-styles": "16.3.0",

packages/core/src/components.d.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";
88
import { BalConfigState } from "./utils/config";
99
import { AccordionState, BalAriaForm as BalAriaForm1, BalConfigState as BalConfigState1 } from "./interfaces";
10-
import { BalCarouselItemData } from "./components/bal-carousel/bal-carousel.type";
10+
import { BalCarouselItemData, BalSlide } from "./components/bal-carousel/bal-carousel.type";
1111
import { BalCheckboxOption } from "./components/bal-checkbox/bal-checkbox.type";
1212
import { BalAriaForm } from "./utils/form";
1313
import { BalOption } from "./utils/dropdown";
@@ -18,7 +18,7 @@ import { BalStepOption } from "./components/bal-steps/bal-step.type";
1818
import { BalTabOption } from "./components/bal-tabs/bal-tab.type";
1919
export { BalConfigState } from "./utils/config";
2020
export { AccordionState, BalAriaForm as BalAriaForm1, BalConfigState as BalConfigState1 } from "./interfaces";
21-
export { BalCarouselItemData } from "./components/bal-carousel/bal-carousel.type";
21+
export { BalCarouselItemData, BalSlide } from "./components/bal-carousel/bal-carousel.type";
2222
export { BalCheckboxOption } from "./components/bal-checkbox/bal-checkbox.type";
2323
export { BalAriaForm } from "./utils/form";
2424
export { BalOption } from "./utils/dropdown";
@@ -378,6 +378,10 @@ export namespace Components {
378378
* If `true` the carousel uses the full height
379379
*/
380380
"fullHeight": boolean;
381+
/**
382+
* Defines the role of the carousel.
383+
*/
384+
"htmlRole": 'tablist' | 'list' | '';
381385
/**
382386
* Defines special looks.
383387
*/
@@ -390,11 +394,11 @@ export namespace Components {
390394
* Defines how many slides are visible in the container for the user. `auto` will use the size of the actual item content
391395
*/
392396
"itemsPerView": 'auto' | 1 | 2 | 3 | 4;
393-
"next": (steps?: number) => Promise<void>;
397+
"next": (steps?: number) => Promise<BalSlide | undefined>;
394398
/**
395399
* PUBLIC METHODS ------------------------------------------------------
396400
*/
397-
"previous": (steps?: number) => Promise<void>;
401+
"previous": (steps?: number) => Promise<BalSlide | undefined>;
398402
/**
399403
* If `true` vertical scrolling on mobile is enabled.
400404
*/
@@ -426,6 +430,10 @@ export namespace Components {
426430
* Specifies the URL of the page the link goes to
427431
*/
428432
"href"?: string;
433+
/**
434+
* Defines the role of the carousel.
435+
*/
436+
"htmlRole": 'tab' | 'listitem' | '';
429437
/**
430438
* Label of the slide which will be used for pagination tabs
431439
*/
@@ -438,6 +446,7 @@ export namespace Components {
438446
* 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).
439447
*/
440448
"rel"?: string;
449+
"setFocus": () => Promise<void>;
441450
/**
442451
* Src path to the image
443452
*/
@@ -3148,7 +3157,7 @@ export namespace Components {
31483157
*/
31493158
"border": boolean;
31503159
/**
3151-
* If `true` the tabs or steps can be clicked.
3160+
* If `true` the tabs or tabs can be clicked.
31523161
*/
31533162
"clickable": boolean;
31543163
"closeAccordion": () => Promise<void>;
@@ -3190,7 +3199,7 @@ export namespace Components {
31903199
*/
31913200
"optionalTabSelection": boolean;
31923201
/**
3193-
* Steps can be passed as a property or through HTML markup.
3202+
* Tabs can be passed as a property or through HTML markup.
31943203
*/
31953204
"options": BalTabOption[];
31963205
/**
@@ -5402,6 +5411,10 @@ declare namespace LocalJSX {
54025411
* If `true` the carousel uses the full height
54035412
*/
54045413
"fullHeight"?: boolean;
5414+
/**
5415+
* Defines the role of the carousel.
5416+
*/
5417+
"htmlRole"?: 'tablist' | 'list' | '';
54055418
/**
54065419
* Defines special looks.
54075420
*/
@@ -5448,6 +5461,10 @@ declare namespace LocalJSX {
54485461
* Specifies the URL of the page the link goes to
54495462
*/
54505463
"href"?: string;
5464+
/**
5465+
* Defines the role of the carousel.
5466+
*/
5467+
"htmlRole"?: 'tab' | 'listitem' | '';
54515468
/**
54525469
* Label of the slide which will be used for pagination tabs
54535470
*/
@@ -8179,7 +8196,7 @@ declare namespace LocalJSX {
81798196
*/
81808197
"border"?: boolean;
81818198
/**
8182-
* If `true` the tabs or steps can be clicked.
8199+
* If `true` the tabs or tabs can be clicked.
81838200
*/
81848201
"clickable"?: boolean;
81858202
/**
@@ -8227,7 +8244,7 @@ declare namespace LocalJSX {
82278244
*/
82288245
"optionalTabSelection"?: boolean;
82298246
/**
8230-
* Steps can be passed as a property or through HTML markup.
8247+
* Tabs can be passed as a property or through HTML markup.
82318248
*/
82328249
"options"?: BalTabOption[];
82338250
/**

packages/core/src/components/bal-carousel/bal-carousel-item/bal-carousel-item.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ import { Component, ComponentInterface, h, Host, Method, Element, Prop, Event, E
22
import { BEM } from '../../../utils/bem'
33
import { BalCarouselItemData } from '../bal-carousel.type'
44
import { Attributes } from '../../../interfaces'
5+
import { waitAfterFramePaint } from '../../../utils/helpers'
56
import { inheritAttributes } from '../../../utils/attributes'
67

78
@Component({
89
tag: 'bal-carousel-item',
910
})
1011
export class CarouselItem implements ComponentInterface {
1112
private imageInheritAttributes: Attributes = {}
13+
private buttonEl: HTMLButtonElement | HTMLLinkElement
1214

1315
@Element() el!: HTMLElement
1416

@@ -22,6 +24,11 @@ export class CarouselItem implements ComponentInterface {
2224
*/
2325
@Prop({ reflect: true }) label = ''
2426

27+
/**
28+
* Defines the role of the carousel.
29+
*/
30+
@Prop() htmlRole: 'tab' | 'listitem' | '' = 'listitem'
31+
2532
/**
2633
* The type of button.
2734
*/
@@ -93,6 +100,14 @@ export class CarouselItem implements ComponentInterface {
93100
}
94101
}
95102

103+
@Method()
104+
async setFocus(): Promise<void> {
105+
await waitAfterFramePaint()
106+
if (this.buttonEl) {
107+
this.buttonEl.focus()
108+
}
109+
}
110+
96111
private onClick = (ev: MouseEvent) => {
97112
if (this.href !== undefined) {
98113
this.balNavigate.emit(ev)
@@ -115,7 +130,7 @@ export class CarouselItem implements ComponentInterface {
115130

116131
if (!isProduct) {
117132
return (
118-
<Host class={{ ...itemEl.class() }}>
133+
<Host role={this.htmlRole} class={{ ...itemEl.class() }}>
119134
{this.src !== undefined ? (
120135
<img draggable={false} onDragStart={() => false} src={this.src} {...this.imageInheritAttributes} />
121136
) : (
@@ -144,21 +159,23 @@ export class CarouselItem implements ComponentInterface {
144159
}
145160

146161
return (
147-
<Host class={{ ...itemEl.class() }}>
162+
<Host role={this.htmlRole} class={{ ...itemEl.class() }}>
148163
<TagType
149164
{...attrs}
150165
class={{ ...button.class(), ...button.modifier(`color-${this.color}`).class() }}
151166
part="native"
152167
onFocus={this.onFocus}
153168
onBlur={this.onBlur}
154169
onClick={this.onClick}
170+
ref={el => (this.buttonEl = el)}
155171
>
156172
{this.src !== undefined ? (
157173
<img
158174
class={{ ...image.class() }}
159175
loading="lazy"
160176
draggable={false}
161177
onDragStart={() => false}
178+
aria-hidden="true"
162179
src={this.src}
163180
{...this.imageInheritAttributes}
164181
/>

0 commit comments

Comments
 (0)