Skip to content

Commit

Permalink
feat: Vue 3 compatibility & better a11y
Browse files Browse the repository at this point in the history
BREAKING CHANGE: This PR reverts the changes to make these components
functional, making them usable in both Vue 2 and Vue 3. If you're using
loads of icons on one page, you may see minor performance regressions in
Vue 2.

This patch also removes the default title, encouraging better
accessibility by removing unhelpful titles that dont indicate usage. The
`decorative` prop has been removed and any icons that do not have a
meaningful title will be hidden from screen readers.
  • Loading branch information
robcresswell committed Nov 30, 2021
1 parent ff7e570 commit c65d8ea
Show file tree
Hide file tree
Showing 9 changed files with 376 additions and 258 deletions.
15 changes: 3 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,25 +61,16 @@ easier.
## Props

- `title` - This changes the hover tooltip as well as the title shown to screen
readers. By default, those values are a "human readable" conversion of the
icon names; for example `chevron-down-icon` becomes "Chevron down icon".
readers. For accessibility purposes, if a `title` is not provided, then the
icon is hidden from screen readers. This is to force developers to use
meaningful titles for their icon usage.

Example:

```html
<android-icon title="this is an icon!" />
```

- `decorative` - This denotes whether an icon is purely decorative, or has some
meaninfgul value. If an icon is decorative, it will be hidden from screen
readers. By default, this is `false`.

Example:

```html
<android-icon decorative />
```

- `fillColor` - This property allows you to set the fill colour of an icon via
JS instead of requiring CSS changes. Note that any CSS values, such as
`fill: currentColor;` provided by the optional CSS file, may override colours
Expand Down
2 changes: 1 addition & 1 deletion __tests__/__snapshots__/icon.js.snap
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Icon renders an icon 1`] = `<span aria-label="Android icon" role="img" class="material-design-icon android-icon"><svg fill="currentColor" width="24" height="24" viewBox="0 0 24 24" class="material-design-icon__svg"><path d="M16.61 15.15C16.15 15.15 15.77 14.78 15.77 14.32S16.15 13.5 16.61 13.5H16.61C17.07 13.5 17.45 13.86 17.45 14.32C17.45 14.78 17.07 15.15 16.61 15.15M7.41 15.15C6.95 15.15 6.57 14.78 6.57 14.32C6.57 13.86 6.95 13.5 7.41 13.5H7.41C7.87 13.5 8.24 13.86 8.24 14.32C8.24 14.78 7.87 15.15 7.41 15.15M16.91 10.14L18.58 7.26C18.67 7.09 18.61 6.88 18.45 6.79C18.28 6.69 18.07 6.75 18 6.92L16.29 9.83C14.95 9.22 13.5 8.9 12 8.91C10.47 8.91 9 9.24 7.73 9.82L6.04 6.91C5.95 6.74 5.74 6.68 5.57 6.78C5.4 6.87 5.35 7.08 5.44 7.25L7.1 10.13C4.25 11.69 2.29 14.58 2 18H22C21.72 14.59 19.77 11.7 16.91 10.14H16.91Z"><title>Android icon</title></path></svg></span>`;
exports[`Icon renders an icon 1`] = `<span aria-hidden="true" role="img" class="material-design-icon android-icon"><svg fill="currentColor" width="24" height="24" viewBox="0 0 24 24" class="material-design-icon__svg"><path d="M16.61 15.15C16.15 15.15 15.77 14.78 15.77 14.32S16.15 13.5 16.61 13.5H16.61C17.07 13.5 17.45 13.86 17.45 14.32C17.45 14.78 17.07 15.15 16.61 15.15M7.41 15.15C6.95 15.15 6.57 14.78 6.57 14.32C6.57 13.86 6.95 13.5 7.41 13.5H7.41C7.87 13.5 8.24 13.86 8.24 14.32C8.24 14.78 7.87 15.15 7.41 15.15M16.91 10.14L18.58 7.26C18.67 7.09 18.61 6.88 18.45 6.79C18.28 6.69 18.07 6.75 18 6.92L16.29 9.83C14.95 9.22 13.5 8.9 12 8.91C10.47 8.91 9 9.24 7.73 9.82L6.04 6.91C5.95 6.74 5.74 6.68 5.57 6.78C5.4 6.87 5.35 7.08 5.44 7.25L7.1 10.13C4.25 11.69 2.29 14.58 2 18H22C21.72 14.59 19.77 11.7 16.91 10.14H16.91Z"><!----></path></svg></span>`;
10 changes: 1 addition & 9 deletions __tests__/icon.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,13 @@ describe('Icon', () => {
});

it('accepts a "title" property', async () => {
expect(icon.attributes()['aria-label']).toEqual('Android icon');
expect(icon.attributes()['aria-label']).toBeUndefined();

await icon.setProps({ title: 'foo' });

expect(icon.attributes()['aria-label']).toEqual('foo');
});

it('accepts a "decorative" property', async () => {
expect(icon.attributes()['aria-hidden']).toBeFalsy();

await icon.setProps({ decorative: true });

expect(icon.attributes()['aria-hidden']).toBeTruthy();
});

it('accepts a "fillColor" property', async () => {
const svg = icon.find('.material-design-icon__svg');

Expand Down
59 changes: 0 additions & 59 deletions build.js

This file was deleted.

92 changes: 92 additions & 0 deletions build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#!/usr/bin/env -S node -r ts-node/register/transpile-only

// Imports
import { mkdir, writeFile } from 'fs/promises';
import path from 'path';
import pMap from 'p-map';
import * as icons from '@mdi/js/commonjs/mdi.js';
import { existsSync } from 'fs';

const dist = path.resolve(__dirname, 'dist');

function renderTemplate(title: string, svgPathData: string, name: string) {
return `<template>
<span :aria-hidden="!title"
:aria-label="title"
class="material-design-icon ${title}-icon"
role="img"
v-bind="$attrs"
@click="$emit('click', $event)">
<svg :fill="fillColor"
class="material-design-icon__svg"
:width="size"
:height="size"
viewBox="0 0 24 24">
<path d="${svgPathData}">
<title v-if="title">{{ title }}</title>
</path>
</svg>
</span>
</template>
<script>
export default {
name: "${name}Icon",
props: {
title: {
type: String,
},
fillColor: {
type: String,
default: "currentColor"
},
size: {
type: Number,
default: 24
}
}
}
</script>`;
}

function getTemplateData(id: string) {
const splitID = id.split(/(?=[A-Z])/).slice(1);

const name = splitID.join('');

// This is a hacky way to remove the 'mdi' prefix, so "mdiAndroid" becomes
// "android", for example
const title = splitID.join('-').toLowerCase();

return {
name,
title,
svgPathData: icons[id],
};
}

async function build() {
const iconIDs = Object.keys(icons);

if (!existsSync(dist)) {
await mkdir(dist);
}

const templateData = iconIDs.map(getTemplateData);

// Batch process promises to avoid overloading memory
await pMap(
templateData,
async ({ name, title, svgPathData }) => {
const component = renderTemplate(title, svgPathData, name);
const filename = `${name}.vue`;

return writeFile(path.resolve(dist, filename), component);
},
{ concurrency: 20 },
);
}

build().catch((err: unknown) => {
console.log(err);
});
Loading

0 comments on commit c65d8ea

Please sign in to comment.