From 95a3c69bbbe415cb5f14ac8e1faed287e91b4b40 Mon Sep 17 00:00:00 2001 From: Sean Perkins Date: Fri, 27 Jan 2023 16:02:45 -0500 Subject: [PATCH] fix(item): inherit aria attributes before render (#26546) Resolves #26538 --- core/package-lock.json | 19 +++-- core/package.json | 2 +- core/src/components/item/item.tsx | 5 +- .../src/components/item/test/a11y/item.e2e.ts | 13 ++++ packages/vue/src/vue-component-lib/utils.ts | 73 ++++++++++--------- 5 files changed, 66 insertions(+), 46 deletions(-) diff --git a/core/package-lock.json b/core/package-lock.json index a5b0230238..b25dedceea 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -24,7 +24,7 @@ "@stencil/angular-output-target": "^0.4.0", "@stencil/react-output-target": "^0.2.1", "@stencil/sass": "^2.0.0", - "@stencil/vue-output-target": "^0.6.2", + "@stencil/vue-output-target": "^0.7.0", "@types/jest": "^27.5.2", "@types/node": "^14.6.0", "@types/swiper": "5.4.0", @@ -1632,12 +1632,12 @@ } }, "node_modules/@stencil/vue-output-target": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@stencil/vue-output-target/-/vue-output-target-0.6.2.tgz", - "integrity": "sha512-Oh7SLFbOUchCSCbGe/Dqal2xSYPKCFQiVKnvzvS0dsHP/XS7rfHqp3qptW6JCp9lBoo3wmmBurHfldqxhLlnag==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@stencil/vue-output-target/-/vue-output-target-0.7.0.tgz", + "integrity": "sha512-iPEgnT2z6HsfWVRWVZk5C1AaMZnbJjB+c/hvtWoO7B3aErTJB8Up6oFk/t3IRsr12aNuZ4fUra0FEDx9WweH0Q==", "dev": true, "peerDependencies": { - "@stencil/core": "^2.9.0" + "@stencil/core": "^2.9.0 || ^3.0.0-beta.0" } }, "node_modules/@stylelint/postcss-css-in-js": { @@ -11638,11 +11638,10 @@ "requires": {} }, "@stencil/vue-output-target": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@stencil/vue-output-target/-/vue-output-target-0.6.2.tgz", - "integrity": "sha512-Oh7SLFbOUchCSCbGe/Dqal2xSYPKCFQiVKnvzvS0dsHP/XS7rfHqp3qptW6JCp9lBoo3wmmBurHfldqxhLlnag==", - "dev": true, - "requires": {} + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@stencil/vue-output-target/-/vue-output-target-0.7.0.tgz", + "integrity": "sha512-iPEgnT2z6HsfWVRWVZk5C1AaMZnbJjB+c/hvtWoO7B3aErTJB8Up6oFk/t3IRsr12aNuZ4fUra0FEDx9WweH0Q==", + "dev": true }, "@stylelint/postcss-css-in-js": { "version": "0.37.2", diff --git a/core/package.json b/core/package.json index b21a116fb6..bc82a5944d 100644 --- a/core/package.json +++ b/core/package.json @@ -46,7 +46,7 @@ "@stencil/angular-output-target": "^0.4.0", "@stencil/react-output-target": "^0.2.1", "@stencil/sass": "^2.0.0", - "@stencil/vue-output-target": "^0.6.2", + "@stencil/vue-output-target": "^0.7.0", "@types/jest": "^27.5.2", "@types/node": "^14.6.0", "@types/swiper": "5.4.0", diff --git a/core/src/components/item/item.tsx b/core/src/components/item/item.tsx index f68d8f94d1..06ef7b426f 100644 --- a/core/src/components/item/item.tsx +++ b/core/src/components/item/item.tsx @@ -226,9 +226,12 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac } } + componentWillLoad() { + this.inheritedAriaAttributes = inheritAttributes(this.el, ['aria-label']); + } + componentDidLoad() { raf(() => { - this.inheritedAriaAttributes = inheritAttributes(this.el, ['aria-label']); this.setMultipleInputs(); this.focusable = this.isFocusable(); }); diff --git a/core/src/components/item/test/a11y/item.e2e.ts b/core/src/components/item/test/a11y/item.e2e.ts index ff85a14d75..e958af90d5 100644 --- a/core/src/components/item/test/a11y/item.e2e.ts +++ b/core/src/components/item/test/a11y/item.e2e.ts @@ -12,4 +12,17 @@ test.describe('item: axe', () => { .analyze(); expect(results.violations).toEqual([]); }); + + test('should reflect aria-label', async ({ page }) => { + await page.setContent(` + + + `); + + const item1 = await page.locator('#item-1 .item-native'); + const item2 = await page.locator('#item-2 .item-native'); + + expect(await item1.getAttribute('aria-label')).toEqual('test'); + expect(await item2.getAttribute('aria-label')).toEqual('test'); + }); }); diff --git a/packages/vue/src/vue-component-lib/utils.ts b/packages/vue/src/vue-component-lib/utils.ts index e48debacfa..9381dc917b 100644 --- a/packages/vue/src/vue-component-lib/utils.ts +++ b/packages/vue/src/vue-component-lib/utils.ts @@ -9,7 +9,7 @@ const MODEL_VALUE = 'modelValue'; const ROUTER_LINK_VALUE = 'routerLink'; const NAV_MANAGER = 'navManager'; const ROUTER_PROP_PREFIX = 'router'; - +const ARIA_PROP_PREFIX = 'aria'; /** * Starting in Vue 3.1.0, all properties are * added as keys to the props object, even if @@ -30,26 +30,31 @@ const getComponentClasses = (classes: unknown) => { return (classes as string)?.split(' ') || []; }; -const getElementClasses = (ref: Ref, componentClasses: Set, defaultClasses: string[] = []) => { - return [ ...Array.from(ref.value?.classList || []), ...defaultClasses ] - .filter((c: string, i, self) => !componentClasses.has(c) && self.indexOf(c) === i); +const getElementClasses = ( + ref: Ref, + componentClasses: Set, + defaultClasses: string[] = [] +) => { + return [...Array.from(ref.value?.classList || []), ...defaultClasses].filter( + (c: string, i, self) => !componentClasses.has(c) && self.indexOf(c) === i + ); }; /** -* Create a callback to define a Vue component wrapper around a Web Component. -* -* @prop name - The component tag name (i.e. `ion-button`) -* @prop componentProps - An array of properties on the -* component. These usually match up with the @Prop definitions -* in each component's TSX file. -* @prop customElement - An option custom element instance to pass -* to customElements.define. Only set if `includeImportCustomElements: true` in your config. -* @prop modelProp - The prop that v-model binds to (i.e. value) -* @prop modelUpdateEvent - The event that is fired from your Web Component when the value changes (i.e. ionChange) -* @prop externalModelUpdateEvent - The external event to fire from your Vue component when modelUpdateEvent fires. This is used for ensuring that v-model references have been -* correctly updated when a user's event callback fires. -*/ -export const defineContainer = ( + * Create a callback to define a Vue component wrapper around a Web Component. + * + * @prop name - The component tag name (i.e. `ion-button`) + * @prop componentProps - An array of properties on the + * component. These usually match up with the @Prop definitions + * in each component's TSX file. + * @prop customElement - An option custom element instance to pass + * to customElements.define. Only set if `includeImportCustomElements: true` in your config. + * @prop modelProp - The prop that v-model binds to (i.e. value) + * @prop modelUpdateEvent - The event that is fired from your Web Component when the value changes (i.e. ionChange) + * @prop externalModelUpdateEvent - The external event to fire from your Vue component when modelUpdateEvent fires. This is used for ensuring that v-model references have been + * correctly updated when a user's event callback fires. + */ +export const defineContainer = ( name: string, defineCustomElement: any, componentProps: string[] = [], @@ -58,10 +63,10 @@ export const defineContainer = ( externalModelUpdateEvent?: string ) => { /** - * Create a Vue component wrapper around a Web Component. - * Note: The `props` here are not all properties on a component. - * They refer to whatever properties are set on an instance of a component. - */ + * Create a Vue component wrapper around a Web Component. + * Note: The `props` here are not all properties on a component. + * They refer to whatever properties are set on an instance of a component. + */ if (defineCustomElement !== undefined) { defineCustomElement(); @@ -116,12 +121,12 @@ export const defineContainer = ( } else { console.warn('Tried to navigate, but no router was found. Make sure you have mounted Vue Router.'); } - } + }; return () => { modelPropValue = props[modelProp]; - getComponentClasses(attrs.class).forEach(value => { + getComponentClasses(attrs.class).forEach((value) => { classes.add(value); }); @@ -133,13 +138,13 @@ export const defineContainer = ( if (!ev.defaultPrevented) { handleRouterLink(ev); } - } + }; let propsToAdd: any = { ref: containerRef, class: getElementClasses(containerRef, classes), onClick: handleClick, - onVnodeBeforeMount: (modelUpdateEvent) ? onVnodeBeforeMount : undefined + onVnodeBeforeMount: modelUpdateEvent ? onVnodeBeforeMount : undefined, }; /** @@ -150,7 +155,7 @@ export const defineContainer = ( */ for (const key in props) { const value = props[key]; - if (props.hasOwnProperty(key) && value !== EMPTY_PROP) { + if ((props.hasOwnProperty(key) && value !== EMPTY_PROP) || key.startsWith(ARIA_PROP_PREFIX)) { propsToAdd[key] = value; } } @@ -165,27 +170,27 @@ export const defineContainer = ( if (props[MODEL_VALUE] !== EMPTY_PROP) { propsToAdd = { ...propsToAdd, - [modelProp]: props[MODEL_VALUE] - } + [modelProp]: props[MODEL_VALUE], + }; } else if (modelPropValue !== EMPTY_PROP) { propsToAdd = { ...propsToAdd, - [modelProp]: modelPropValue - } + [modelProp]: modelPropValue, + }; } } return h(name, propsToAdd, slots.default && slots.default()); - } + }; }); Container.displayName = name; Container.props = { - [ROUTER_LINK_VALUE]: DEFAULT_EMPTY_PROP + [ROUTER_LINK_VALUE]: DEFAULT_EMPTY_PROP, }; - componentProps.forEach(componentProp => { + componentProps.forEach((componentProp) => { Container.props[componentProp] = DEFAULT_EMPTY_PROP; });