From 611832b0d51da295c1bf2897972c4e8baf6e23a3 Mon Sep 17 00:00:00 2001 From: Sean Perkins Date: Thu, 21 Apr 2022 10:50:56 -0400 Subject: [PATCH] fix(core): inherit aria attributes on host elements (#25156) Resolves #20127 --- .../components/back-button/back-button.tsx | 4 +- core/src/components/breadcrumb/breadcrumb.tsx | 4 +- core/src/components/button/button.tsx | 4 +- core/src/components/header/header.tsx | 4 +- core/src/components/input/input.tsx | 7 +- .../components/menu-button/menu-button.tsx | 4 +- core/src/components/menu/menu.tsx | 4 +- core/src/components/range/range.tsx | 4 +- core/src/components/textarea/textarea.tsx | 7 +- core/src/utils/helpers.ts | 68 +++++++++++++++++++ core/src/utils/test/attributes.spec.ts | 28 +++++++- 11 files changed, 119 insertions(+), 19 deletions(-) diff --git a/core/src/components/back-button/back-button.tsx b/core/src/components/back-button/back-button.tsx index 165c9e525b..d7140f0811 100644 --- a/core/src/components/back-button/back-button.tsx +++ b/core/src/components/back-button/back-button.tsx @@ -7,7 +7,7 @@ import { getIonMode } from '../../global/ionic-global'; import type { AnimationBuilder, Color } from '../../interface'; import type { ButtonInterface } from '../../utils/element-interface'; import type { Attributes } from '../../utils/helpers'; -import { inheritAttributes } from '../../utils/helpers'; +import { inheritAriaAttributes } from '../../utils/helpers'; import { createColorClasses, hostContext, openURL } from '../../utils/theme'; /** @@ -70,7 +70,7 @@ export class BackButton implements ComponentInterface, ButtonInterface { @Prop() routerAnimation: AnimationBuilder | undefined; componentWillLoad() { - this.inheritedAttributes = inheritAttributes(this.el, ['aria-label']); + this.inheritedAttributes = inheritAriaAttributes(this.el); if (this.defaultHref === undefined) { this.defaultHref = config.get('backButtonDefaultHref'); diff --git a/core/src/components/breadcrumb/breadcrumb.tsx b/core/src/components/breadcrumb/breadcrumb.tsx index 0d4f206ff8..a565d089d1 100644 --- a/core/src/components/breadcrumb/breadcrumb.tsx +++ b/core/src/components/breadcrumb/breadcrumb.tsx @@ -5,7 +5,7 @@ import { chevronForwardOutline, ellipsisHorizontal } from 'ionicons/icons'; import { getIonMode } from '../../global/ionic-global'; import type { AnimationBuilder, BreadcrumbCollapsedClickEventDetail, Color, RouterDirection } from '../../interface'; import type { Attributes } from '../../utils/helpers'; -import { inheritAttributes } from '../../utils/helpers'; +import { inheritAriaAttributes } from '../../utils/helpers'; import { createColorClasses, hostContext, openURL } from '../../utils/theme'; /** @@ -124,7 +124,7 @@ export class Breadcrumb implements ComponentInterface { @Event() collapsedClick!: EventEmitter; componentWillLoad() { - this.inheritedAttributes = inheritAttributes(this.el, ['aria-label']); + this.inheritedAttributes = inheritAriaAttributes(this.el); } private isClickable(): boolean { diff --git a/core/src/components/button/button.tsx b/core/src/components/button/button.tsx index 2da127a7ab..428038ab12 100644 --- a/core/src/components/button/button.tsx +++ b/core/src/components/button/button.tsx @@ -5,7 +5,7 @@ import { getIonMode } from '../../global/ionic-global'; import type { AnimationBuilder, Color, RouterDirection } from '../../interface'; import type { AnchorInterface, ButtonInterface } from '../../utils/element-interface'; import type { Attributes } from '../../utils/helpers'; -import { hasShadowDom, inheritAttributes } from '../../utils/helpers'; +import { inheritAriaAttributes, hasShadowDom } from '../../utils/helpers'; import { createColorClasses, hostContext, openURL } from '../../utils/theme'; /** @@ -137,7 +137,7 @@ export class Button implements ComponentInterface, AnchorInterface, ButtonInterf this.inToolbar = !!this.el.closest('ion-buttons'); this.inListHeader = !!this.el.closest('ion-list-header'); this.inItem = !!this.el.closest('ion-item') || !!this.el.closest('ion-item-divider'); - this.inheritedAttributes = inheritAttributes(this.el, ['aria-label']); + this.inheritedAttributes = inheritAriaAttributes(this.el); } private get hasIconOnly() { diff --git a/core/src/components/header/header.tsx b/core/src/components/header/header.tsx index 3c014f39c5..9ebc2f9579 100644 --- a/core/src/components/header/header.tsx +++ b/core/src/components/header/header.tsx @@ -4,7 +4,7 @@ import { Component, Element, Host, Prop, h, writeTask } from '@stencil/core'; import { getIonMode } from '../../global/ionic-global'; import { findIonContent, getScrollElement, printIonContentErrorMsg } from '../../utils/content'; import type { Attributes } from '../../utils/helpers'; -import { inheritAttributes } from '../../utils/helpers'; +import { inheritAriaAttributes } from '../../utils/helpers'; import { hostContext } from '../../utils/theme'; import { @@ -55,7 +55,7 @@ export class Header implements ComponentInterface { @Prop() translucent = false; componentWillLoad() { - this.inheritedAttributes = inheritAttributes(this.el, ['role']); + this.inheritedAttributes = inheritAriaAttributes(this.el); } componentDidLoad() { diff --git a/core/src/components/input/input.tsx b/core/src/components/input/input.tsx index 9326cf84cc..d58b9c5c45 100644 --- a/core/src/components/input/input.tsx +++ b/core/src/components/input/input.tsx @@ -10,7 +10,7 @@ import type { TextFieldTypes, } from '../../interface'; import type { Attributes } from '../../utils/helpers'; -import { debounceEvent, findItemLabel, inheritAttributes } from '../../utils/helpers'; +import { inheritAriaAttributes, debounceEvent, findItemLabel, inheritAttributes } from '../../utils/helpers'; import { createColorClasses } from '../../utils/theme'; /** @@ -257,7 +257,10 @@ export class Input implements ComponentInterface { } componentWillLoad() { - this.inheritedAttributes = inheritAttributes(this.el, ['aria-label', 'tabindex', 'title']); + this.inheritedAttributes = { + ...inheritAriaAttributes(this.el), + ...inheritAttributes(this.el, ['tabindex', 'title']), + }; } connectedCallback() { diff --git a/core/src/components/menu-button/menu-button.tsx b/core/src/components/menu-button/menu-button.tsx index f7a88b24c1..fd7c60108d 100644 --- a/core/src/components/menu-button/menu-button.tsx +++ b/core/src/components/menu-button/menu-button.tsx @@ -7,7 +7,7 @@ import { getIonMode } from '../../global/ionic-global'; import type { Color } from '../../interface'; import type { ButtonInterface } from '../../utils/element-interface'; import type { Attributes } from '../../utils/helpers'; -import { inheritAttributes } from '../../utils/helpers'; +import { inheritAriaAttributes } from '../../utils/helpers'; import { menuController } from '../../utils/menu-controller'; import { createColorClasses, hostContext } from '../../utils/theme'; import { updateVisibility } from '../menu-toggle/menu-toggle-util'; @@ -61,7 +61,7 @@ export class MenuButton implements ComponentInterface, ButtonInterface { @Prop() type: 'submit' | 'reset' | 'button' = 'button'; componentWillLoad() { - this.inheritedAttributes = inheritAttributes(this.el, ['aria-label']); + this.inheritedAttributes = inheritAriaAttributes(this.el); } componentDidLoad() { diff --git a/core/src/components/menu/menu.tsx b/core/src/components/menu/menu.tsx index c30108dcf5..6491c74a03 100644 --- a/core/src/components/menu/menu.tsx +++ b/core/src/components/menu/menu.tsx @@ -7,7 +7,7 @@ import type { Animation, Gesture, GestureDetail, MenuChangeEventDetail, MenuI, S import { getTimeGivenProgression } from '../../utils/animation/cubic-bezier'; import { GESTURE_CONTROLLER } from '../../utils/gesture'; import type { Attributes } from '../../utils/helpers'; -import { assert, clamp, inheritAttributes, isEndSide as isEnd } from '../../utils/helpers'; +import { inheritAriaAttributes, assert, clamp, isEndSide as isEnd } from '../../utils/helpers'; import { menuController } from '../../utils/menu-controller'; import { getOverlay } from '../../utils/overlays'; @@ -226,7 +226,7 @@ export class Menu implements ComponentInterface, MenuI { } componentWillLoad() { - this.inheritedAttributes = inheritAttributes(this.el, ['aria-label']); + this.inheritedAttributes = inheritAriaAttributes(this.el); } async componentDidLoad() { diff --git a/core/src/components/range/range.tsx b/core/src/components/range/range.tsx index c9e95b3128..86dd925d65 100644 --- a/core/src/components/range/range.tsx +++ b/core/src/components/range/range.tsx @@ -14,7 +14,7 @@ import type { StyleEventDetail, } from '../../interface'; import type { Attributes } from '../../utils/helpers'; -import { clamp, debounceEvent, getAriaLabel, inheritAttributes, renderHiddenInput } from '../../utils/helpers'; +import { inheritAriaAttributes, clamp, debounceEvent, getAriaLabel, renderHiddenInput } from '../../utils/helpers'; import { isRTL } from '../../utils/rtl'; import { createColorClasses, hostContext } from '../../utils/theme'; @@ -237,7 +237,7 @@ export class Range implements ComponentInterface { */ this.rangeId = this.el.hasAttribute('id') ? this.el.getAttribute('id')! : `ion-r-${rangeIds++}`; - this.inheritedAttributes = inheritAttributes(this.el, ['aria-label']); + this.inheritedAttributes = inheritAriaAttributes(this.el); } componentDidLoad() { diff --git a/core/src/components/textarea/textarea.tsx b/core/src/components/textarea/textarea.tsx index d93f78dc8b..41d34c02e7 100644 --- a/core/src/components/textarea/textarea.tsx +++ b/core/src/components/textarea/textarea.tsx @@ -4,7 +4,7 @@ import { Build, Component, Element, Event, Host, Method, Prop, State, Watch, h, import { getIonMode } from '../../global/ionic-global'; import type { Color, StyleEventDetail, TextareaChangeEventDetail } from '../../interface'; import type { Attributes } from '../../utils/helpers'; -import { debounceEvent, findItemLabel, inheritAttributes, raf } from '../../utils/helpers'; +import { inheritAriaAttributes, debounceEvent, findItemLabel, inheritAttributes, raf } from '../../utils/helpers'; import { createColorClasses } from '../../utils/theme'; /** @@ -220,7 +220,10 @@ export class Textarea implements ComponentInterface { } componentWillLoad() { - this.inheritedAttributes = inheritAttributes(this.el, ['title']); + this.inheritedAttributes = { + ...inheritAriaAttributes(this.el), + ...inheritAttributes(this.el, ['title']), + }; } componentDidLoad() { diff --git a/core/src/utils/helpers.ts b/core/src/utils/helpers.ts index 5c11033ac4..e0d9897f39 100644 --- a/core/src/utils/helpers.ts +++ b/core/src/utils/helpers.ts @@ -103,6 +103,74 @@ export const inheritAttributes = (el: HTMLElement, attributes: string[] = []) => return attributeObject; }; +/** + * List of available ARIA attributes + `role`. + * Removed deprecated attributes. + * https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes + */ +const ariaAttributes = [ + 'role', + 'aria-activedescendant', + 'aria-atomic', + 'aria-autocomplete', + 'aria-braillelabel', + 'aria-brailleroledescription', + 'aria-busy', + 'aria-checked', + 'aria-colcount', + 'aria-colindex', + 'aria-colindextext', + 'aria-colspan', + 'aria-controls', + 'aria-current', + 'aria-describedby', + 'aria-description', + 'aria-details', + 'aria-disabled', + 'aria-errormessage', + 'aria-expanded', + 'aria-flowto', + 'aria-haspopup', + 'aria-hidden', + 'aria-invalid', + 'aria-keyshortcuts', + 'aria-label', + 'aria-labelledby', + 'aria-level', + 'aria-live', + 'aria-multiline', + 'aria-multiselectable', + 'aria-orientation', + 'aria-owns', + 'aria-placeholder', + 'aria-posinset', + 'aria-pressed', + 'aria-readonly', + 'aria-relevant', + 'aria-required', + 'aria-roledescription', + 'aria-rowcount', + 'aria-rowindex', + 'aria-rowindextext', + 'aria-rowspan', + 'aria-selected', + 'aria-setsize', + 'aria-sort', + 'aria-valuemax', + 'aria-valuemin', + 'aria-valuenow', + 'aria-valuetext', +]; + +/** + * Returns an array of aria attributes that should be copied from + * the shadow host element to a target within the light DOM. + * @param el The element that the attributes should be copied from. + */ +export const inheritAriaAttributes = (el: HTMLElement) => { + return inheritAttributes(el, ariaAttributes); +}; + export const addEventListener = (el: any, eventName: string, callback: any, opts?: any) => { if (typeof (window as any) !== 'undefined') { const win = window as any; diff --git a/core/src/utils/test/attributes.spec.ts b/core/src/utils/test/attributes.spec.ts index c40ac73241..ba3dd28d2a 100644 --- a/core/src/utils/test/attributes.spec.ts +++ b/core/src/utils/test/attributes.spec.ts @@ -1,4 +1,4 @@ -import { inheritAttributes } from '../helpers'; +import { inheritAttributes, inheritAriaAttributes } from '../helpers'; describe('inheritAttributes()', () => { it('should create an attribute inheritance object', () => { @@ -37,3 +37,29 @@ describe('inheritAttributes()', () => { }); }); }); + +describe('inheritAriaAttributes()', () => { + it('should inherit ARIA attributes defined on the HTML element', () => { + const el = document.createElement('div'); + el.setAttribute('aria-label', 'myLabel'); + el.setAttribute('aria-describedby', 'myDescription'); + + const attributeObject = inheritAriaAttributes(el); + + expect(attributeObject).toEqual({ + 'aria-label': 'myLabel', + 'aria-describedby': 'myDescription', + }); + }); + + it('should inherit the role attribute defined on the HTML element', () => { + const el = document.createElement('div'); + el.setAttribute('role', 'button'); + + const attributeObject = inheritAriaAttributes(el); + + expect(attributeObject).toEqual({ + role: 'button', + }); + }); +});