Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(icon): icons now compile with TypeScript 4.4 #1014

Merged
merged 1 commit into from
Nov 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 0 additions & 16 deletions src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,6 @@
import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";
export namespace Components {
interface IonIcon {
/**
* Set the icon to hidden, respectively `true`, to remove it from the accessibility tree.
*/
"ariaHidden"?: string;
/**
* Specifies the label to use for accessibility. Defaults to the icon name.
*/
"ariaLabel"?: string;
/**
* The color to use for the background of the item.
*/
Expand Down Expand Up @@ -75,14 +67,6 @@ declare global {
}
declare namespace LocalJSX {
interface IonIcon {
/**
* Set the icon to hidden, respectively `true`, to remove it from the accessibility tree.
*/
"ariaHidden"?: string;
/**
* Specifies the label to use for accessibility. Defaults to the icon name.
*/
"ariaLabel"?: string;
/**
* The color to use for the background of the item.
*/
Expand Down
47 changes: 29 additions & 18 deletions src/components/icon/icon.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Build, Component, Element, Host, Prop, State, Watch, h } from '@stencil/core';
import { getSvgContent, ioniconContent } from './request';
import { getName, getUrl } from './utils';
import { getName, getUrl, inheritAttributes } from './utils';

@Component({
tag: 'ion-icon',
Expand All @@ -11,11 +11,13 @@ import { getName, getUrl } from './utils';
export class Icon {
private io?: IntersectionObserver;
private iconName: string | null = null;
private inheritedAttributes: { [k: string]: any } = {};

@Element() el!: HTMLElement;

@State() private svgContent?: string;
@State() private isVisible = false;
@State() private ariaLabel?: string;

/**
* The mode determines which platform styles to use.
Expand All @@ -27,16 +29,6 @@ export class Icon {
*/
@Prop() color?: string;

/**
* Specifies the label to use for accessibility. Defaults to the icon name.
*/
@Prop({ mutable: true, reflect: true }) ariaLabel?: string;

/**
* Set the icon to hidden, respectively `true`, to remove it from the accessibility tree.
*/
@Prop({ reflect: true }) ariaHidden?: string;

/**
* Specifies which icon to use on `ios` mode.
*/
Expand Down Expand Up @@ -88,6 +80,10 @@ export class Icon {
* @default true
*/
@Prop() sanitize = true;

componentWillLoad() {
this.inheritedAttributes = inheritAttributes(this.el, ['aria-label']);
}

connectedCallback() {
// purposely do not return the promise here because loading
Expand Down Expand Up @@ -126,6 +122,12 @@ export class Icon {
cb();
}
}

private hasAriaHidden = () => {
const { el } = this;

return el.hasAttribute('aria-hidden') && el.getAttribute('aria-hidden') === 'true';
}

@Watch('name')
@Watch('src')
Expand All @@ -146,33 +148,42 @@ export class Icon {

const label = this.iconName = getName(this.name, this.icon, this.mode, this.ios, this.md);

if (!this.ariaLabel && this.ariaHidden !== 'true') {
// user did not provide a label
// come up with the label based on the icon name
if (label) {
this.ariaLabel = label.replace(/\-/g, ' ');
}
/**
* Come up with a default label
* in case user does not provide their own.
*/
if (label) {
this.ariaLabel = label.replace(/\-/g, ' ');
}
}

render() {
const { iconName } = this;
const { iconName, ariaLabel, inheritedAttributes } = this;
const mode = this.mode || 'md';
const flipRtl =
this.flipRtl ||
(iconName &&
(iconName.indexOf('arrow') > -1 || iconName.indexOf('chevron') > -1) &&
this.flipRtl !== false);

/**
* Only set the aria-label if a) we have generated
* one for the icon and if aria-hidden is not set to "true".
* If developer wants to set their own aria-label, then
* inheritedAttributes down below will override whatever
* default label we have set.
*/
return (
<Host
aria-label={ariaLabel !== undefined && !this.hasAriaHidden() ? ariaLabel : null}
role="img"
class={{
[mode]: true,
...createColorClasses(this.color),
[`icon-${this.size}`]: !!this.size,
'flip-rtl': !!flipRtl && (this.el.ownerDocument as Document).dir === 'rtl',
}}
{...inheritedAttributes}
>
{Build.isBrowser && this.svgContent ? (
<div class="icon-inner" innerHTML={this.svgContent}></div>
Expand Down
28 changes: 13 additions & 15 deletions src/components/icon/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,19 @@

## Properties

| Property | Attribute | Description | Type | Default |
| ------------ | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | -------------- |
| `ariaHidden` | `aria-hidden` | Set the icon to hidden, respectively `true`, to remove it from the accessibility tree. | `string \| undefined` | `undefined` |
| `ariaLabel` | `aria-label` | Specifies the label to use for accessibility. Defaults to the icon name. | `string \| undefined` | `undefined` |
| `color` | `color` | The color to use for the background of the item. | `string \| undefined` | `undefined` |
| `flipRtl` | `flip-rtl` | Specifies whether the icon should horizontally flip when `dir` is `"rtl"`. | `boolean \| undefined` | `undefined` |
| `icon` | `icon` | A combination of both `name` and `src`. If a `src` url is detected it will set the `src` property. Otherwise it assumes it's a built-in named SVG and set the `name` property. | `any` | `undefined` |
| `ios` | `ios` | Specifies which icon to use on `ios` mode. | `string \| undefined` | `undefined` |
| `lazy` | `lazy` | If enabled, ion-icon will be loaded lazily when it's visible in the viewport. Default, `false`. | `boolean` | `false` |
| `md` | `md` | Specifies which icon to use on `md` mode. | `string \| undefined` | `undefined` |
| `mode` | `mode` | The mode determines which platform styles to use. | `string` | `getIonMode()` |
| `name` | `name` | Specifies which icon to use from the built-in set of icons. | `string \| undefined` | `undefined` |
| `sanitize` | `sanitize` | When set to `false`, SVG content that is HTTP fetched will not be checked if the response SVG content has any `<script>` elements, or any attributes that start with `on`, such as `onclick`. | `boolean` | `true` |
| `size` | `size` | The size of the icon. Available options are: `"small"` and `"large"`. | `string \| undefined` | `undefined` |
| `src` | `src` | Specifies the exact `src` of an SVG file to use. | `string \| undefined` | `undefined` |
| Property | Attribute | Description | Type | Default |
| ---------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | -------------- |
| `color` | `color` | The color to use for the background of the item. | `string \| undefined` | `undefined` |
| `flipRtl` | `flip-rtl` | Specifies whether the icon should horizontally flip when `dir` is `"rtl"`. | `boolean \| undefined` | `undefined` |
| `icon` | `icon` | A combination of both `name` and `src`. If a `src` url is detected it will set the `src` property. Otherwise it assumes it's a built-in named SVG and set the `name` property. | `any` | `undefined` |
| `ios` | `ios` | Specifies which icon to use on `ios` mode. | `string \| undefined` | `undefined` |
| `lazy` | `lazy` | If enabled, ion-icon will be loaded lazily when it's visible in the viewport. Default, `false`. | `boolean` | `false` |
| `md` | `md` | Specifies which icon to use on `md` mode. | `string \| undefined` | `undefined` |
| `mode` | `mode` | The mode determines which platform styles to use. | `string` | `getIonMode()` |
| `name` | `name` | Specifies which icon to use from the built-in set of icons. | `string \| undefined` | `undefined` |
| `sanitize` | `sanitize` | When set to `false`, SVG content that is HTTP fetched will not be checked if the response SVG content has any `<script>` elements, or any attributes that start with `on`, such as `onclick`. | `boolean` | `true` |
| `size` | `size` | The size of the icon. Available options are: `"small"` and `"large"`. | `string \| undefined` | `undefined` |
| `src` | `src` | Specifies the exact `src` of an SVG file to use. | `string \| undefined` | `undefined` |


----------------------------------------------
Expand Down
86 changes: 86 additions & 0 deletions src/components/icon/test/icon.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,90 @@ describe('icon', () => {
</ion-icon>
`);
});

it('renders default aria-label', async () => {
const { root } = await newSpecPage({
components: [Icon],
html: `<ion-icon name="chevron-forward"></ion-icon>`,
});

expect(root).toEqualHtml(`
<ion-icon class="md" name="chevron-forward" role="img" aria-label="chevron forward">
<mock:shadow-root>
<div class="icon-inner"></div>
</mock:shadow-root>
</ion-icon>
`);
});

it('renders custom aria-label', async () => {
const { root } = await newSpecPage({
components: [Icon],
html: `<ion-icon name="chevron-forward" aria-label="custom label"></ion-icon>`,
});

expect(root).toEqualHtml(`
<ion-icon class="md" name="chevron-forward" role="img" aria-label="custom label">
<mock:shadow-root>
<div class="icon-inner"></div>
</mock:shadow-root>
</ion-icon>
`);
});

it('renders custom label after changing source', async () => {
const page = await newSpecPage({
components: [Icon],
html: `<ion-icon name="chevron-forward" aria-label="custom label"></ion-icon>`,
});

const icon = page.root;

expect(icon).toEqualHtml(`
<ion-icon class="md" name="chevron-forward" role="img" aria-label="custom label">
<mock:shadow-root>
<div class="icon-inner"></div>
</mock:shadow-root>
</ion-icon>
`);

icon.name = 'trash';
await page.waitForChanges();

expect(icon).toEqualHtml(`
<ion-icon class="md" name="trash" role="img" aria-label="custom label">
<mock:shadow-root>
<div class="icon-inner"></div>
</mock:shadow-root>
</ion-icon>
`);
});

it('renders default label after changing source', async () => {
const page = await newSpecPage({
components: [Icon],
html: `<ion-icon name="chevron-forward"></ion-icon>`,
});

const icon = page.root;

expect(icon).toEqualHtml(`
<ion-icon class="md" name="chevron-forward" role="img" aria-label="chevron forward">
<mock:shadow-root>
<div class="icon-inner"></div>
</mock:shadow-root>
</ion-icon>
`);

icon.name = 'trash';
await page.waitForChanges();

expect(icon).toEqualHtml(`
<ion-icon class="md" name="trash" role="img" aria-label="trash">
<mock:shadow-root>
<div class="icon-inner"></div>
</mock:shadow-root>
</ion-icon>
`);
});
});
28 changes: 27 additions & 1 deletion src/components/icon/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,30 @@ export const isSrc = (str: string) => str.length > 0 && /(\/|\.)/.test(str);

export const isStr = (val: any): val is string => typeof val === 'string';

export const toLower = (val: string) => val.toLowerCase();
export const toLower = (val: string) => val.toLowerCase();

/**
* Elements inside of web components sometimes need to inherit global attributes
* set on the host. For example, the inner input in `ion-input` should inherit
* the `title` attribute that developers set directly on `ion-input`. This
* helper function should be called in componentWillLoad and assigned to a variable
* that is later used in the render function.
*
* This does not need to be reactive as changing attributes on the host element
* does not trigger a re-render.
*/
export const inheritAttributes = (el: HTMLElement, attributes: string[] = []) => {
const attributeObject: { [k: string]: any } = {};

attributes.forEach(attr => {
if (el.hasAttribute(attr)) {
const value = el.getAttribute(attr);
if (value !== null) {
attributeObject[attr] = el.getAttribute(attr);
}
el.removeAttribute(attr);
}
});

return attributeObject;
}