From eb97f91e9d6c945b0a1b6e22581aa8223309d164 Mon Sep 17 00:00:00 2001 From: Lukas Maurer Date: Tue, 22 Oct 2024 10:04:52 +0200 Subject: [PATCH] fix(core): improve component a11y (#1497) Co-authored-by: matthiashader <144090716+matthiashader@users.noreply.github.com> Co-authored-by: matthias --- .changeset/eight-windows-shout.md | 7 ++ packages/angular/src/components.ts | 4 +- packages/core/component-doc.json | 49 +++++++++--- packages/core/src/components.d.ts | 28 ++++--- .../core/src/components/avatar/avatar.scss | 2 +- .../core/src/components/avatar/avatar.tsx | 39 ++++++---- .../category-filter/category-filter.scss | 6 +- .../category-filter/category-filter.tsx | 20 +++-- .../event-list-item/event-list-item.tsx | 17 ++-- .../src/components/event-list/event-list.scss | 7 -- .../src/components/event-list/event-list.tsx | 8 +- .../event-list/test/event-list.spec.tsx | 75 ------------------ .../src/components/menu-item/menu-item.tsx | 11 ++- packages/core/src/components/menu/menu.tsx | 58 +++++++------- packages/core/src/components/pane/pane.tsx | 78 ++++++++++--------- .../core/src/components/utils/make-ref.ts | 25 +++--- packages/vue/src/components.ts | 1 + 17 files changed, 209 insertions(+), 226 deletions(-) create mode 100644 .changeset/eight-windows-shout.md delete mode 100644 packages/core/src/components/event-list/test/event-list.spec.tsx diff --git a/.changeset/eight-windows-shout.md b/.changeset/eight-windows-shout.md new file mode 100644 index 00000000000..5849541ebac --- /dev/null +++ b/.changeset/eight-windows-shout.md @@ -0,0 +1,7 @@ +--- +"@siemens/ix-angular": minor +"@siemens/ix": minor +"@siemens/ix-vue": minor +--- + +feat(core): improve component a11y diff --git a/packages/angular/src/components.ts b/packages/angular/src/components.ts index 9eb6d100afe..2bf486f7404 100644 --- a/packages/angular/src/components.ts +++ b/packages/angular/src/components.ts @@ -350,14 +350,14 @@ export declare interface IxCardTitle extends Components.IxCardTitle {} @ProxyCmp({ - inputs: ['categories', 'disabled', 'filterState', 'hideIcon', 'i18nPlainText', 'icon', 'labelCategories', 'nonSelectableCategories', 'placeholder', 'readonly', 'repeatCategories', 'staticOperator', 'suggestions'] + inputs: ['ariaLabel', 'categories', 'disabled', 'filterState', 'hideIcon', 'i18nPlainText', 'icon', 'labelCategories', 'nonSelectableCategories', 'placeholder', 'readonly', 'repeatCategories', 'staticOperator', 'suggestions'] }) @Component({ selector: 'ix-category-filter', changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['categories', 'disabled', 'filterState', 'hideIcon', 'i18nPlainText', 'icon', 'labelCategories', 'nonSelectableCategories', 'placeholder', 'readonly', 'repeatCategories', 'staticOperator', 'suggestions'], + inputs: ['ariaLabel', 'categories', 'disabled', 'filterState', 'hideIcon', 'i18nPlainText', 'icon', 'labelCategories', 'nonSelectableCategories', 'placeholder', 'readonly', 'repeatCategories', 'staticOperator', 'suggestions'], }) export class IxCategoryFilter { protected el: HTMLElement; diff --git a/packages/core/component-doc.json b/packages/core/component-doc.json index 4297aec4e69..106b9a8ef0a 100644 --- a/packages/core/component-doc.json +++ b/packages/core/component-doc.json @@ -583,7 +583,7 @@ "type": "string" } ], - "optional": false, + "optional": true, "required": false }, { @@ -604,7 +604,7 @@ "type": "string" } ], - "optional": false, + "optional": true, "required": false }, { @@ -625,7 +625,7 @@ "type": "string" } ], - "optional": false, + "optional": true, "required": false }, { @@ -651,7 +651,7 @@ "type": "string" } ], - "optional": false, + "optional": true, "required": false } ], @@ -2154,6 +2154,33 @@ ] }, "props": [ + { + "name": "ariaLabel", + "type": "string", + "complexType": { + "original": "string", + "resolved": "string", + "references": {} + }, + "mutable": false, + "attr": "aria-label", + "reflectToAttr": false, + "docs": "Aria label for the filter input field", + "docsTags": [ + { + "name": "since", + "text": "2.6.0" + } + ], + "default": "'Filter'", + "values": [ + { + "type": "string" + } + ], + "optional": false, + "required": false + }, { "name": "categories", "type": "{ [id: string]: { label: string; options: string[]; }; }", @@ -6302,6 +6329,7 @@ "reflectToAttr": false, "docs": "Display a chevron icon in list items. Defaults to 'false'", "docsTags": [], + "default": "false", "values": [ { "type": "boolean" @@ -6399,6 +6427,7 @@ "reflectToAttr": false, "docs": "Show chevron on right side of the event list item", "docsTags": [], + "default": "false", "values": [ { "type": "boolean" @@ -6435,7 +6464,7 @@ "type": "string" } ], - "optional": false, + "optional": true, "required": false }, { @@ -6451,6 +6480,7 @@ "reflectToAttr": false, "docs": "Disable event list item", "docsTags": [], + "default": "false", "values": [ { "type": "boolean" @@ -6482,7 +6512,7 @@ "type": "string" } ], - "optional": false, + "optional": true, "required": false }, { @@ -6498,6 +6528,7 @@ "reflectToAttr": false, "docs": "Show event list item as selected", "docsTags": [], + "default": "false", "values": [ { "type": "boolean" @@ -9281,7 +9312,7 @@ "type": "string" } ], - "optional": false, + "optional": true, "required": false }, { @@ -12186,7 +12217,7 @@ "type": "string" } ], - "optional": false, + "optional": true, "required": false }, { @@ -12229,7 +12260,7 @@ "type": "string" } ], - "optional": false, + "optional": true, "required": false }, { diff --git a/packages/core/src/components.d.ts b/packages/core/src/components.d.ts index 4291b42557c..f315b252d37 100644 --- a/packages/core/src/components.d.ts +++ b/packages/core/src/components.d.ts @@ -163,20 +163,20 @@ export namespace Components { * Optional description text that will be displayed underneath the username. Note: Only working if avatar is part of the ix-application-header * @since 2.1.0 */ - "extra": string; + "extra"?: string; /** * Display an avatar image */ - "image": string; + "image"?: string; /** * Display the initials of the user. Will be overwritten by image */ - "initials": string; + "initials"?: string; /** * If set an info card displaying the username will be placed inside the dropdown. Note: Only working if avatar is part of the ix-application-header * @since 2.1.0 */ - "username": string; + "username"?: string; } interface IxBasicNavigation { /** @@ -366,6 +366,11 @@ export namespace Components { interface IxCardTitle { } interface IxCategoryFilter { + /** + * Aria label for the filter input field + * @since 2.6.0 + */ + "ariaLabel": string; /** * Configuration object hash used to populate the dropdown menu for type-ahead and quick selection functionality. Each ID maps to an object with a label and an array of options to select from. */ @@ -987,7 +992,7 @@ export namespace Components { * @link https://ix.siemens.io/docs/theming/colors/ * @deprecated since 2.1.0 use `item-color` */ - "color": string; + "color"?: string; /** * Disable event list item */ @@ -996,7 +1001,7 @@ export namespace Components { * Color of the status indicator. You can find a list of all available colors in our documentation. Example values are `--theme-color-alarm` or `color-alarm` * @link https://ix.siemens.io/docs/theming/colors/ */ - "itemColor": string; + "itemColor"?: string; /** * Show event list item as selected */ @@ -1348,7 +1353,7 @@ export namespace Components { /** * Should only be set if you use ix-menu standalone */ - "applicationName": string; + "applicationName"?: string; /** * Internal */ @@ -1740,7 +1745,7 @@ export namespace Components { /** * Title of the side panel */ - "heading": string; + "heading"?: string; /** * Define if the pane should have a collapsed state */ @@ -1748,7 +1753,7 @@ export namespace Components { /** * Name of the icon */ - "icon": string; + "icon"?: string; "ignoreLayoutSettings": boolean; "isMobile": boolean; /** @@ -4442,6 +4447,11 @@ declare namespace LocalJSX { interface IxCardTitle { } interface IxCategoryFilter { + /** + * Aria label for the filter input field + * @since 2.6.0 + */ + "ariaLabel"?: string; /** * Configuration object hash used to populate the dropdown menu for type-ahead and quick selection functionality. Each ID maps to an object with a label and an array of options to select from. */ diff --git a/packages/core/src/components/avatar/avatar.scss b/packages/core/src/components/avatar/avatar.scss index 5a69d88c526..d1f668bea67 100644 --- a/packages/core/src/components/avatar/avatar.scss +++ b/packages/core/src/components/avatar/avatar.scss @@ -106,7 +106,7 @@ @include btn-base-variant('invisible-primary', false); - li { + .avatar { transform: scale(0.8); } } diff --git a/packages/core/src/components/avatar/avatar.tsx b/packages/core/src/components/avatar/avatar.tsx index 04b1f4f2b5e..1e6cbfc55c6 100644 --- a/packages/core/src/components/avatar/avatar.tsx +++ b/packages/core/src/components/avatar/avatar.tsx @@ -53,23 +53,23 @@ function DefaultAvatar(props: { initials?: string }) { ); } -function AvatarImage(props: { image: string; initials: string }) { +function AvatarImage(props: { image?: string; initials?: string }) { return ( -
  • +
    {props.image ? ( ) : ( )} -
  • + ); } function UserInfo(props: { - image: string; - initials: string; + image?: string; + initials?: string; userName: string; - extra: string; + extra?: string; }) { return ( @@ -97,19 +97,19 @@ function UserInfo(props: { shadow: true, }) export class Avatar { - @Element() hostElement: HTMLIxAvatarElement; + @Element() hostElement!: HTMLIxAvatarElement; /** * Display an avatar image * */ - @Prop() image: string; + @Prop() image?: string; /** * Display the initials of the user. Will be overwritten by image * */ - @Prop() initials: string; + @Prop() initials?: string; /** * If set an info card displaying the username will be placed inside the dropdown. @@ -117,7 +117,7 @@ export class Avatar { * * @since 2.1.0 */ - @Prop() username: string; + @Prop() username?: string; /** * Optional description text that will be displayed underneath the username. @@ -125,13 +125,13 @@ export class Avatar { * * @since 2.1.0 */ - @Prop() extra: string; + @Prop() extra?: string; @State() isClosestApplicationHeader = false; @State() hasSlottedElements = false; - private slotElement: HTMLSlotElement; - private dropdownElement: HTMLIxDropdownElement; + private slotElement?: HTMLSlotElement; + private dropdownElement?: HTMLIxDropdownElement; componentWillLoad() { const closest = closestElement('ix-application-header', this.hostElement); @@ -143,10 +143,15 @@ export class Avatar { } private resolveAvatarTrigger() { - return new Promise((resolve) => { - readTask(() => - resolve(this.hostElement.shadowRoot.querySelector('button')) - ); + return new Promise((resolve, reject) => { + readTask(() => { + const button = this.hostElement.shadowRoot!.querySelector('button'); + if (button) { + resolve(button); + } else { + reject(new Error('ix-avatar - trigger element not found')); + } + }); }); } diff --git a/packages/core/src/components/category-filter/category-filter.scss b/packages/core/src/components/category-filter/category-filter.scss index 61b2fa31a97..7cecadf8d92 100644 --- a/packages/core/src/components/category-filter/category-filter.scss +++ b/packages/core/src/components/category-filter/category-filter.scss @@ -95,9 +95,7 @@ .list-unstyled { display: flex; flex-wrap: wrap; - list-style: none; - padding: 0; - margin: 0; + height: 100%; overflow-y: auto; } @@ -126,7 +124,7 @@ height: 100%; } - ul > li, + .list-unstyled > span:not(.category-preview), input { padding-inline-start: 0; padding-top: 2px; diff --git a/packages/core/src/components/category-filter/category-filter.tsx b/packages/core/src/components/category-filter/category-filter.tsx index 6a572a05459..105f54c94ad 100644 --- a/packages/core/src/components/category-filter/category-filter.tsx +++ b/packages/core/src/components/category-filter/category-filter.tsx @@ -138,6 +138,13 @@ export class CategoryFilter { */ @Prop() i18nPlainText = 'Filter by text'; + /** + * Aria label for the filter input field + * + * @since 2.6.0 + */ + @Prop() ariaLabel = 'Filter'; + /** * Event dispatched whenever a category gets selected in the dropdown */ @@ -710,9 +717,9 @@ export class CategoryFilter { size="16" >
    -
      +
      {this.filterTokens.map((value, index) => ( -
    • {this.getFilterChipLabel(value)} -
    • + ))} {this.categories === undefined ? ( '' ) : ( -
    • {this.categories[this.category]?.label} -
    • + )} (this.textInput = el)} type="text" placeholder={this.placeholder} + aria-label={this.ariaLabel} > -
    +
    {!this.readonly && !this.disabled && this.getResetButton()} diff --git a/packages/core/src/components/event-list-item/event-list-item.tsx b/packages/core/src/components/event-list-item/event-list-item.tsx index 4519e60db6b..d86eddd65f5 100644 --- a/packages/core/src/components/event-list-item/event-list-item.tsx +++ b/packages/core/src/components/event-list-item/event-list-item.tsx @@ -34,7 +34,7 @@ export class EventListItem { * @deprecated since 2.1.0 use `item-color` */ // eslint-disable-next-line @stencil-community/reserved-member-names - @Prop() color: string; + @Prop() color?: string; /** * Color of the status indicator. @@ -43,27 +43,27 @@ export class EventListItem { * * @link https://ix.siemens.io/docs/theming/colors/ */ - @Prop() itemColor: string; + @Prop() itemColor?: string; /** * Show event list item as selected */ - @Prop() selected: boolean; + @Prop() selected = false; /** * Disable event list item */ - @Prop() disabled: boolean; + @Prop() disabled = false; /** * Show chevron on right side of the event list item */ - @Prop() chevron: boolean; + @Prop() chevron = false; /** * Event list item click */ - @Event() itemClick: EventEmitter; + @Event() itemClick!: EventEmitter; @Listen('click', { passive: true }) handleItemClick() { @@ -87,7 +87,8 @@ export class EventListItem { disabled: this.disabled, }} > -
  • )} -
  • + ); } diff --git a/packages/core/src/components/event-list/event-list.scss b/packages/core/src/components/event-list/event-list.scss index f88c0acd2a6..3210978083a 100644 --- a/packages/core/src/components/event-list/event-list.scss +++ b/packages/core/src/components/event-list/event-list.scss @@ -14,13 +14,6 @@ display: block; position: relative; - - ul { - list-style: none; - padding: 0; - margin-top: 0; - margin-bottom: 0; - } } :host(.item-size-l) { diff --git a/packages/core/src/components/event-list/event-list.tsx b/packages/core/src/components/event-list/event-list.tsx index e4a87d43c29..115e9923faf 100644 --- a/packages/core/src/components/event-list/event-list.tsx +++ b/packages/core/src/components/event-list/event-list.tsx @@ -48,7 +48,7 @@ export class EventList { /** * Display a chevron icon in list items. Defaults to 'false' */ - @Prop() chevron: boolean; + @Prop() chevron = false; @Watch('chevron') watchChevron(chevron: boolean | undefined) { @@ -111,7 +111,7 @@ export class EventList { const keyframes = [{ opacity: 1, easing: 'easeInSine' }, { opacity: 0 }]; - const listElement = this.hostElement.shadowRoot.querySelector('ul'); + const listElement = this.hostElement.shadowRoot!.querySelector('ul'); anime({ targets: listElement, @@ -166,9 +166,9 @@ export class EventList { compact: this.compact, }} > -
      +
      -
    + ); } diff --git a/packages/core/src/components/event-list/test/event-list.spec.tsx b/packages/core/src/components/event-list/test/event-list.spec.tsx deleted file mode 100644 index 513c9cdc0ad..00000000000 --- a/packages/core/src/components/event-list/test/event-list.spec.tsx +++ /dev/null @@ -1,75 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 Siemens AG - * - * SPDX-License-Identifier: MIT - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import { newSpecPage } from '@stencil/core/testing'; -//@ts-ignore -import { createMutationObserver } from '../../utils/mutation-observer'; -import { EventList } from '../event-list'; - -jest.mock('../../utils/mutation-observer'); - -describe('event-list', () => { - beforeEach(() => { - //@ts-ignore - createMutationObserver = jest.fn(() => ({ - observe: jest.fn(), - })); - }); - - it('renders', async () => { - const page = await newSpecPage({ - components: [EventList], - html: ` - - - - `, - }); - expect(page.root).toEqualHtml(` - - -
      - -
    -
    - -
    - `); - }); - - it('compact', async () => { - const page = await newSpecPage({ - components: [EventList], - html: ` - - - - `, - }); - - const eventList = page.doc.querySelector( - 'ix-event-list' - ) as HTMLIxEventListElement; - - eventList.compact = true; - - await page.waitForChanges(); - - expect(page.root).toEqualHtml(` - - -
      - -
    -
    - -
    - `); - }); -}); diff --git a/packages/core/src/components/menu-item/menu-item.tsx b/packages/core/src/components/menu-item/menu-item.tsx index e5c3573a880..328bcc5575c 100644 --- a/packages/core/src/components/menu-item/menu-item.tsx +++ b/packages/core/src/components/menu-item/menu-item.tsx @@ -76,11 +76,11 @@ export class MenuItem { @State() tooltip?: string; @State() menuExpanded: boolean = false; - private buttonRef = makeRef(); + private readonly buttonRef = makeRef(); private isHostedInsideCategory = false; - private menuExpandedDisposer: Disposable; + private menuExpandedDisposer?: Disposable; - private observer: MutationObserver = createMutationObserver(() => { + private readonly observer: MutationObserver = createMutationObserver(() => { this.setTooltip(); }); @@ -91,7 +91,7 @@ export class MenuItem { this.onIconChange(); this.onTabIconChange(); - this.menuExpanded = menuController.nativeElement.expand; + this.menuExpanded = menuController.nativeElement?.expand || false; this.menuExpandedDisposer = menuController.expandChange.on( (expand) => (this.menuExpanded = expand) ); @@ -102,7 +102,7 @@ export class MenuItem { } setTooltip() { - this.tooltip = this.label ?? this.hostElement.textContent; + this.tooltip = this.label ?? this.hostElement.textContent ?? undefined; } connectedCallback() { @@ -172,7 +172,6 @@ export class MenuItem {