Skip to content

fix(icon): respond to changes of document dir #1210

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

Merged
merged 13 commits into from
May 19, 2023
55 changes: 48 additions & 7 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, inheritAttributes, isRTL } from './utils';
import { getName, getUrl, inheritAttributes } from './utils';

@Component({
tag: 'ion-icon',
Expand All @@ -12,11 +12,21 @@ export class Icon {
private io?: IntersectionObserver;
private iconName: string | null = null;
private inheritedAttributes: { [k: string]: any } = {};
private documentDirObserver?: MutationObserver;

// arrows and chevrons should flip based on `dir` *unless* the flipRtl prop is set to `false`
private shouldAutoFlip?: boolean;

// if true, the icon should change direction when `dir` changes
private shouldBeFlippable?: boolean;

@Element() el!: HTMLElement;

@State() private svgContent?: string;
@State() private isVisible = false;
// note: `dir` only gets set/changed if conditions are right for the MutationObserver to get added
// in most cases, the dir attribute on the document and element are irrelevant for displaying the icons
@State() private dir?: string;

/**
* The mode determines which platform styles to use.
Expand Down Expand Up @@ -82,8 +92,40 @@ export class Icon {

componentWillLoad() {
this.inheritedAttributes = inheritAttributes(this.el, ['aria-label']);
this.shouldAutoFlip =
(this.iconName?.includes('arrow') || this.iconName?.includes('chevron')) && this.flipRtl !== false;
this.shouldBeFlippable = this.flipRtl || this.shouldAutoFlip || this.el.hasAttribute('dir');

if (this.shouldBeFlippable) {
// initialize `dir`. after this, it will be updated via documentDirObserver
this.dir = this.el.dir.toLowerCase() || document.dir.toLowerCase();
}
}

componentDidLoad() {
if (this.shouldBeFlippable && Build.isBrowser) {
this.documentDirObserver = new MutationObserver(() => {
// if the element has a dir, it should override the document's dir
this.dir = this.el.dir.toLowerCase() || document.dir.toLowerCase();
});

// if the icon should flip when dir changes, watch for changes to dir
if (this.flipRtl || this.shouldAutoFlip) {
this.documentDirObserver.observe(document.documentElement!, {
attributes: true,
attributeFilter: ['dir'],
});
}

// if the element has a `dir` attribute set, watch for changes to it
if (this.el.hasAttribute('dir')) {
this.documentDirObserver.observe(this.el, {
attributes: true,
attributeFilter: ['dir'],
});
}
}
}
connectedCallback() {
// purposely do not return the promise here because loading
// the svg file should not hold up loading the app
Expand All @@ -99,8 +141,10 @@ export class Icon {
this.io.disconnect();
this.io = undefined;
}
if (this.documentDirObserver) {
this.documentDirObserver.disconnect();
}
}

private waitUntilVisible(el: HTMLElement, rootMargin: string, cb: () => void) {
if (Build.isBrowser && this.lazy && typeof window !== 'undefined' && (window as any).IntersectionObserver) {
const io = (this.io = new (window as any).IntersectionObserver(
Expand Down Expand Up @@ -146,11 +190,8 @@ export class Icon {
}

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

return (
<Host
Expand All @@ -159,7 +200,7 @@ export class Icon {
[mode]: true,
...createColorClasses(this.color),
[`icon-${this.size}`]: !!this.size,
'flip-rtl': !!flipRtl && isRTL(el),
'flip-rtl': this.dir === 'rtl',
}}
{...inheritedAttributes}
>
Expand Down
26 changes: 24 additions & 2 deletions src/components/icon/test/icon.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,32 @@ import { test } from '@utils/test/playwright';
test.describe('icon: basic', () => {
test('should not have visual regressions', async ({ page }) => {
await page.goto(`/`);

// Wait for all SVGs to be lazily loaded before taking screenshots
await page.waitForLoadState('networkidle');

expect(await page.screenshot({ fullPage: true })).toMatchSnapshot(`icon-diff.png`);
});
});

test('some icons should flip when rtl', async ({ page }) => {
await page.goto(`/`);

const autoflip = page.locator('.auto-flip-chevrons [name=chevron-forward]');
await expect(autoflip).not.toHaveClass(/flip-rtl/);

const unflip = page.locator('.un-flip-chevrons [name=chevron-forward]');
await expect(unflip).not.toHaveClass(/flip-rtl/);

await page.evaluate(() => {
document.dir = 'rtl';
});

await expect(autoflip).toHaveClass(/flip-rtl/);
await expect(unflip).not.toHaveClass(/flip-rtl/);

// Wait for all SVGs to be lazily loaded before taking screenshots
await page.waitForLoadState('networkidle');

expect(await page.screenshot({ fullPage: true })).toMatchSnapshot(`icon-rtl-diff.png`);
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 0 additions & 14 deletions src/components/icon/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,17 +140,3 @@ export const inheritAttributes = (el: HTMLElement, attributes: string[] = []) =>

return attributeObject;
}

/**
* Returns `true` if the document or host element
* has a `dir` set to `rtl`. The host value will always
* take priority over the root document value.
*/
export const isRTL = (hostEl?: Pick<HTMLElement, 'dir'>) => {
if (hostEl) {
if (hostEl.dir !== '') {
return hostEl.dir.toLowerCase() === 'rtl';
}
}
return document?.dir.toLowerCase() === 'rtl';
};
20 changes: 12 additions & 8 deletions src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -113,16 +113,20 @@ <h2>Un-flip: arrows</h2>
<ion-icon name="arrow-back" flip-rtl="false"></ion-icon>

<h2>Auto Flip: chevrons</h2>
<ion-icon name="chevron-up"></ion-icon>
<ion-icon name="chevron-forward"></ion-icon>
<ion-icon name="chevron-down"></ion-icon>
<ion-icon name="chevron-back"></ion-icon>
<div class="auto-flip-chevrons">
<ion-icon name="chevron-up"></ion-icon>
<ion-icon name="chevron-forward"></ion-icon>
<ion-icon name="chevron-down"></ion-icon>
<ion-icon name="chevron-back"></ion-icon>
</div>

<h2>Un-flip: chevrons</h2>
<ion-icon name="chevron-up" flip-rtl="false"></ion-icon>
<ion-icon name="chevron-forward" flip-rtl="false"></ion-icon>
<ion-icon name="chevron-down" flip-rtl="false"></ion-icon>
<ion-icon name="chevron-back" flip-rtl="false"></ion-icon>
<div class="un-flip-chevrons">
<ion-icon name="chevron-up" flip-rtl="false"></ion-icon>
<ion-icon name="chevron-forward" flip-rtl="false"></ion-icon>
<ion-icon name="chevron-down" flip-rtl="false"></ion-icon>
<ion-icon name="chevron-back" flip-rtl="false"></ion-icon>
</div>

<h2>Auto Flip, RTL on components</h2>
<ion-icon name="arrow-up" dir="rtl" flip-rtl></ion-icon>
Expand Down