Skip to content

Commit

Permalink
fix(item): inherit aria attributes before render (#26546)
Browse files Browse the repository at this point in the history
Resolves #26538
  • Loading branch information
sean-perkins committed Jan 27, 2023
1 parent 227b2ae commit 95a3c69
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 46 deletions.
19 changes: 9 additions & 10 deletions core/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 4 additions & 1 deletion core/src/components/item/item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
Expand Down
13 changes: 13 additions & 0 deletions core/src/components/item/test/a11y/item.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,17 @@ test.describe('item: axe', () => {
.analyze();
expect(results.violations).toEqual([]);
});

test('should reflect aria-label', async ({ page }) => {
await page.setContent(`
<ion-item id="item-1" aria-label="test"></ion-item>
<ion-item id="item-2" aria-label="test" button="true"></ion-item>
`);

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');
});
});
73 changes: 39 additions & 34 deletions packages/vue/src/vue-component-lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -30,26 +30,31 @@ const getComponentClasses = (classes: unknown) => {
return (classes as string)?.split(' ') || [];
};

const getElementClasses = (ref: Ref<HTMLElement | undefined>, componentClasses: Set<string>, 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<HTMLElement | undefined>,
componentClasses: Set<string>,
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 = <Props, VModelType=string|number|boolean>(
* 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 = <Props, VModelType = string | number | boolean>(
name: string,
defineCustomElement: any,
componentProps: string[] = [],
Expand All @@ -58,10 +63,10 @@ export const defineContainer = <Props, VModelType=string|number|boolean>(
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();
Expand Down Expand Up @@ -116,12 +121,12 @@ export const defineContainer = <Props, VModelType=string|number|boolean>(
} 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);
});

Expand All @@ -133,13 +138,13 @@ export const defineContainer = <Props, VModelType=string|number|boolean>(
if (!ev.defaultPrevented) {
handleRouterLink(ev);
}
}
};

let propsToAdd: any = {
ref: containerRef,
class: getElementClasses(containerRef, classes),
onClick: handleClick,
onVnodeBeforeMount: (modelUpdateEvent) ? onVnodeBeforeMount : undefined
onVnodeBeforeMount: modelUpdateEvent ? onVnodeBeforeMount : undefined,
};

/**
Expand All @@ -150,7 +155,7 @@ export const defineContainer = <Props, VModelType=string|number|boolean>(
*/
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;
}
}
Expand All @@ -165,27 +170,27 @@ export const defineContainer = <Props, VModelType=string|number|boolean>(
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;
});

Expand Down

0 comments on commit 95a3c69

Please sign in to comment.