diff --git a/core/src/components/back-button/back-button.tsx b/core/src/components/back-button/back-button.tsx index 3942241b2c..03e8de19d4 100644 --- a/core/src/components/back-button/back-button.tsx +++ b/core/src/components/back-button/back-button.tsx @@ -2,9 +2,9 @@ import { Component, ComponentInterface, Element, Host, Prop, h } from '@stencil/ import { config } from '../../global/config'; import { getIonMode } from '../../global/ionic-global'; -import { AnimationBuilder, Color } from '../../interface'; -import { ButtonInterface } from '../../utils/element-interface'; -import { inheritAttributes } from '../../utils/helpers'; +import type { AnimationBuilder, Color } from '../../interface'; +import type { ButtonInterface } from '../../utils/element-interface'; +import { inheritAriaAttributes } from '../../utils/helpers'; import { createColorClasses, hostContext, openURL } from '../../utils/theme'; /** @@ -67,7 +67,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/button/button.tsx b/core/src/components/button/button.tsx index f53b4da7d2..ad6ce58d69 100644 --- a/core/src/components/button/button.tsx +++ b/core/src/components/button/button.tsx @@ -1,9 +1,9 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Prop, h } from '@stencil/core'; import { getIonMode } from '../../global/ionic-global'; -import { AnimationBuilder, Color, RouterDirection } from '../../interface'; -import { AnchorInterface, ButtonInterface } from '../../utils/element-interface'; -import { hasShadowDom, inheritAttributes } from '../../utils/helpers'; +import type { AnimationBuilder, Color, RouterDirection } from '../../interface'; +import type { AnchorInterface, ButtonInterface } from '../../utils/element-interface'; +import { hasShadowDom, inheritAriaAttributes } from '../../utils/helpers'; import { createColorClasses, hostContext, openURL } from '../../utils/theme'; /** @@ -135,7 +135,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 c538a7b5b5..f40d09b8ae 100644 --- a/core/src/components/header/header.tsx +++ b/core/src/components/header/header.tsx @@ -1,7 +1,7 @@ import { Component, ComponentInterface, Element, Host, Prop, h, writeTask } from '@stencil/core'; import { getIonMode } from '../../global/ionic-global'; -import { inheritAttributes } from '../../utils/helpers'; +import { inheritAriaAttributes } from '../../utils/helpers'; import { hostContext } from '../../utils/theme'; import { cloneElement, createHeaderIndex, handleContentScroll, handleToolbarIntersection, setHeaderActive, setToolbarBackgroundOpacity } from './header.utils'; @@ -46,7 +46,7 @@ export class Header implements ComponentInterface { @Prop() translucent = false; componentWillLoad() { - this.inheritedAttributes = inheritAttributes(this.el, ['role']); + this.inheritedAttributes = inheritAriaAttributes(this.el); } async componentDidLoad() { diff --git a/core/src/components/input/input.tsx b/core/src/components/input/input.tsx index 14868cb81b..6423bf873d 100644 --- a/core/src/components/input/input.tsx +++ b/core/src/components/input/input.tsx @@ -1,8 +1,14 @@ import { Build, Component, ComponentInterface, Element, Event, EventEmitter, Host, Method, Prop, State, Watch, h } from '@stencil/core'; import { getIonMode } from '../../global/ionic-global'; -import { AutocompleteTypes, Color, InputChangeEventDetail, StyleEventDetail, TextFieldTypes } from '../../interface'; -import { debounceEvent, findItemLabel, inheritAttributes } from '../../utils/helpers'; +import type { + AutocompleteTypes, + Color, + InputChangeEventDetail, + StyleEventDetail, + TextFieldTypes, +} from '../../interface'; +import { debounceEvent, findItemLabel, inheritAriaAttributes, inheritAttributes } from '../../utils/helpers'; import { createColorClasses } from '../../utils/theme'; /** @@ -234,7 +240,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 5e2af949f9..565718dede 100644 --- a/core/src/components/menu-button/menu-button.tsx +++ b/core/src/components/menu-button/menu-button.tsx @@ -2,9 +2,9 @@ import { Component, ComponentInterface, Element, Host, Listen, Prop, State, h } import { config } from '../../global/config'; import { getIonMode } from '../../global/ionic-global'; -import { Color } from '../../interface'; -import { ButtonInterface } from '../../utils/element-interface'; -import { inheritAttributes } from '../../utils/helpers'; +import type { Color } from '../../interface'; +import type { ButtonInterface } from '../../utils/element-interface'; +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'; @@ -58,7 +58,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 fe689a66c7..bd27716727 100644 --- a/core/src/components/menu/menu.tsx +++ b/core/src/components/menu/menu.tsx @@ -5,7 +5,7 @@ import { getIonMode } from '../../global/ionic-global'; import { Animation, Gesture, GestureDetail, MenuChangeEventDetail, MenuI, Side } from '../../interface'; import { getTimeGivenProgression } from '../../utils/animation/cubic-bezier'; import { GESTURE_CONTROLLER } from '../../utils/gesture'; -import { assert, clamp, inheritAttributes, isEndSide as isEnd } from '../../utils/helpers'; +import { assert, clamp, inheritAriaAttributes, isEndSide as isEnd } from '../../utils/helpers'; import { menuController } from '../../utils/menu-controller'; const iosEasing = 'cubic-bezier(0.32,0.72,0,1)'; @@ -213,7 +213,7 @@ AFTER: } 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 22fde6f893..dff566c376 100644 --- a/core/src/components/range/range.tsx +++ b/core/src/components/range/range.tsx @@ -1,8 +1,16 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Prop, State, Watch, h } from '@stencil/core'; import { getIonMode } from '../../global/ionic-global'; -import { Color, Gesture, GestureDetail, KnobName, RangeChangeEventDetail, RangeValue, StyleEventDetail } from '../../interface'; -import { clamp, debounceEvent, getAriaLabel, inheritAttributes, renderHiddenInput } from '../../utils/helpers'; +import type { + Color, + Gesture, + GestureDetail, + KnobName, + RangeChangeEventDetail, + RangeValue, + StyleEventDetail, +} from '../../interface'; +import { clamp, debounceEvent, getAriaLabel, inheritAriaAttributes, renderHiddenInput } from '../../utils/helpers'; import { createColorClasses, hostContext } from '../../utils/theme'; /** @@ -205,7 +213,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 25f3f16453..212784c75a 100644 --- a/core/src/components/textarea/textarea.tsx +++ b/core/src/components/textarea/textarea.tsx @@ -1,8 +1,8 @@ import { Build, Component, ComponentInterface, Element, Event, EventEmitter, Host, Method, Prop, State, Watch, h, readTask } from '@stencil/core'; import { getIonMode } from '../../global/ionic-global'; -import { Color, StyleEventDetail, TextareaChangeEventDetail } from '../../interface'; -import { debounceEvent, findItemLabel, inheritAttributes, raf } from '../../utils/helpers'; +import type { Color, StyleEventDetail, TextareaChangeEventDetail } from '../../interface'; +import { debounceEvent, findItemLabel, inheritAriaAttributes, inheritAttributes, raf } from '../../utils/helpers'; import { createColorClasses } from '../../utils/theme'; /** @@ -214,7 +214,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 1d617f2277..6b8bdcb794 100644 --- a/core/src/utils/helpers.ts +++ b/core/src/utils/helpers.ts @@ -51,6 +51,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; @@ -164,8 +232,8 @@ export const getAriaLabel = (componentEl: HTMLElement, inputId: string): { label labelText = label.textContent; label.setAttribute('aria-hidden', 'true'); - // if there is no label, check to see if the user has provided - // one by setting an id on the component and using the label element + // if there is no label, check to see if the user has provided + // one by setting an id on the component and using the label element } else if (componentId.trim() !== '') { label = document.querySelector(`label[for="${componentId}"]`); diff --git a/core/src/utils/test/attributes.spec.ts b/core/src/utils/test/attributes.spec.ts index efe59588b2..57d8d24239 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', + }); + }); +});