From 222975b80b1ca98e5f6a4d01f69bca80c78e0dcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benny=20Powers=20-=20=D7=A2=D7=9D=20=D7=99=D7=A9=D7=A8?= =?UTF-8?q?=D7=90=D7=9C=20=D7=97=D7=99!?= Date: Sun, 2 Jun 2024 14:48:20 +0300 Subject: [PATCH] fix(icon)!: remove BaseIcon (#2636) * fix(icon)!: remove BaseIcon Closes #2621 * docs: jsdoc * fix(icon): base styles * docs: addIconSet jsdoc Co-authored-by: Steven Spriggs * docs: jsdoc for getIconUrl Co-authored-by: Steven Spriggs * style: lint * fix: remove baseicon from bad merge --------- Co-authored-by: Steven Spriggs --- .changeset/remove-base-icon.md | 4 + elements/package.json | 1 - elements/pf-badge/pf-badge.ts | 7 -- elements/pf-card/pf-card.ts | 13 +-- elements/pf-icon/BaseIcon.css | 22 ---- elements/pf-icon/BaseIcon.ts | 162 ----------------------------- elements/pf-icon/pf-icon.css | 22 ++++ elements/pf-icon/pf-icon.ts | 179 ++++++++++++++++++++++++++++++++- 8 files changed, 205 insertions(+), 205 deletions(-) create mode 100644 .changeset/remove-base-icon.md delete mode 100644 elements/pf-icon/BaseIcon.css delete mode 100644 elements/pf-icon/BaseIcon.ts diff --git a/.changeset/remove-base-icon.md b/.changeset/remove-base-icon.md new file mode 100644 index 0000000000..4ec3813241 --- /dev/null +++ b/.changeset/remove-base-icon.md @@ -0,0 +1,4 @@ +--- +"@patternfly/elements": major +--- +``: Removed `BaseIcon` class. Reimplement (recommended) or extend `PfIcon`. diff --git a/elements/package.json b/elements/package.json index 2df432b30a..179fcbfae9 100644 --- a/elements/package.json +++ b/elements/package.json @@ -33,7 +33,6 @@ "./pf-dropdown/pf-dropdown-group.js": "./pf-dropdown/pf-dropdown-group.ts", "./pf-dropdown/pf-dropdown-menu.js": "./pf-dropdown/pf-dropdown-menu.ts", "./pf-dropdown/pf-dropdown-item.js": "./pf-dropdown/pf-dropdown-item.ts", - "./pf-icon/BaseIcon.js": "./pf-icon/BaseIcon.js", "./pf-icon/pf-icon.js": "./pf-icon/pf-icon.js", "./pf-jump-links/pf-jump-links-item.js": "./pf-jump-links/pf-jump-links-item.js", "./pf-jump-links/pf-jump-links-list.js": "./pf-jump-links/pf-jump-links-list.js", diff --git a/elements/pf-badge/pf-badge.ts b/elements/pf-badge/pf-badge.ts index af5966c3b6..b2226e5692 100644 --- a/elements/pf-badge/pf-badge.ts +++ b/elements/pf-badge/pf-badge.ts @@ -6,24 +6,17 @@ import styles from './pf-badge.css'; /** * A **badge** is used to annotate other information like a label or an object name. - * * @cssprop {} --pf-c-badge--BorderRadius {@default `180em`} - * * @cssprop {} --pf-c-badge--MinWidth {@default `2rem`} - * * @cssprop {} --pf-c-badge--PaddingLeft {@default `0.5rem`} * @cssprop {} --pf-c-badge--PaddingRight {@default `0.5rem`} - * * @cssprop {} --pf-c-badge--FontSize {@default `0.85em`} * @cssprop {} --pf-c-badge--LineHeight {@default `1.5`} * @cssprop {} --pf-c-badge--FontWeight {@default `700`} - * * @cssprop {} --pf-c-badge--Color {@default `#151515`} * @cssprop {} --pf-c-badge--BackgroundColor {@default `#f0f0f0`} - * * @cssprop {} --pf-c-badge--m-read--Color {@default `#151515`} * @cssprop {} --pf-c-badge--m-read--BackgroundColor {@default `#f0f0f0`} - * * @cssprop {} --pf-c-badge--m-unread--Color {@default `#fff`} * @cssprop {} --pf-c-badge--m-unread--BackgroundColor {@default `#06c`} */ diff --git a/elements/pf-card/pf-card.ts b/elements/pf-card/pf-card.ts index 890744e900..b0fe1ea004 100644 --- a/elements/pf-card/pf-card.ts +++ b/elements/pf-card/pf-card.ts @@ -13,9 +13,7 @@ import style from './pf-card.css'; * users to access more details. For example, in dashboards and catalog views, cards * function as a preview of a detailed page. Cards may also be used in data displays * like card views, or for positioning content on a page. - * * @summary Gives a preview of information in a small layout - * * @slot header * When included, defines the contents of a card. Card headers can contain images as well as * the title of a card and an actions menu represented by the right-aligned kebab. @@ -28,12 +26,9 @@ import style from './pf-card.css'; * text and/or active content. * @slot footer * Contains external links, actions, or static text at the bottom of a card. - * * @csspart header - The container for *header* content * @csspart body - The container for *body* content * @csspart footer - The container for *footer* content - * - * * @cssproperty {} --pf-c-card--BackgroundColor {@default `#ffffff`} * @cssproperty {} --pf-c-card--BoxShadow {@default `0 0.0625rem 0.125rem 0 rgba(3, 3, 3, 0.12), 0 0 0.125rem 0 rgba(3, 3, 3, 0.06)`} * @cssproperty {} --pf-c-card--size-compact__body--FontSize {@default `.875rem`} @@ -71,13 +66,13 @@ export class PfCard extends LitElement { @property({ reflect: true }) size?: 'compact' | 'large'; /** - * Optionally apply a border radius for the drop shadow and/or border. - */ + * Optionally apply a border radius for the drop shadow and/or border. + */ @property({ type: Boolean, reflect: true }) rounded = false; /** - * Optionally allow the card to take up the full height of the parent element. - */ + * Optionally allow the card to take up the full height of the parent element. + */ @property({ type: Boolean, reflect: true, attribute: 'full-height' }) fullHeight = false; /** diff --git a/elements/pf-icon/BaseIcon.css b/elements/pf-icon/BaseIcon.css deleted file mode 100644 index 4dd0882446..0000000000 --- a/elements/pf-icon/BaseIcon.css +++ /dev/null @@ -1,22 +0,0 @@ -:host { - position: relative; - display: inline-block; - line-height: 0; - height: fit-content !important; - width: fit-content !important; -} - -#container { - display: grid; - grid-template: 1fr / 1fr; - place-content: center; -} - -#container.content ::slotted(*) { - display: none; -} - -svg { - fill: currentcolor; -} - diff --git a/elements/pf-icon/BaseIcon.ts b/elements/pf-icon/BaseIcon.ts deleted file mode 100644 index d0227ac6bc..0000000000 --- a/elements/pf-icon/BaseIcon.ts +++ /dev/null @@ -1,162 +0,0 @@ -import type { PropertyValues } from 'lit'; - -import { LitElement, html } from 'lit'; -import { property } from 'lit/decorators/property.js'; -import { state } from 'lit/decorators/state.js'; -import { Logger } from '@patternfly/pfe-core/controllers/logger.js'; - -import style from './BaseIcon.css'; - -export type URLGetter = (set: string, icon: string) => URL | string; - -/** requestIdleCallback when available, requestAnimationFrame when not */ -const ric = window.requestIdleCallback ?? window.requestAnimationFrame; - -/** Fired when an icon fails to load */ -class IconLoadError extends ErrorEvent { - constructor( - pathname: string, - /** The original error when importing the icon module */ - public originalError: Error - ) { - super('error', { message: `Could not load icon at ${pathname}` }); - } -} - -/** - * Icon component lazy-loads icons and allows custom icon sets - * @slot - Slotted content is used as a fallback in case the icon doesn't load - * @fires load - Fired when an icon is loaded and rendered - * @fires error - Fired when an icon fails to load - * @csspart fallback - Container for the fallback (i.e. slotted) content - */ -export abstract class BaseIcon extends LitElement { - public static readonly styles = [style]; - - public static addIconSet(setName: string, getter: typeof BaseIcon['getIconUrl']) { - if (typeof getter !== 'function') { - Logger.warn(`[${this.name}.addIconSet(setName, getter)]: getter must be a function`); - } else { - this.getters.set(setName, getter); - for (const instance of this.instances) { - instance.load(); - } - } - } - - public static getIconUrl: URLGetter = (set: string, icon: string) => - `@patternfly/icons/${set}/${icon}.js`; - - private static onIntersect: IntersectionObserverCallback = records => - records.forEach(({ isIntersecting, target }) => { - const icon = target as BaseIcon; - icon.#intersecting = isIntersecting; - ric(() => { - if (icon.#intersecting) { - icon.load(); - } - }); - }); - - private static io = new IntersectionObserver(this.onIntersect); - - private static getters = new Map(); - - private static instances = new Set(); - - declare public static defaultIconSet: string; - - /** Icon set */ - @property() set = this.#class.defaultIconSet; - - /** Icon name */ - @property({ reflect: true }) icon = ''; - - /** Size of the icon */ - abstract size: string; - - /** - * Controls how eager the element will be to load the icon data - * - `eager`: eagerly load the icon, blocking the main thread - * - `idle`: wait for the browser to attain an idle state before loading - * - `lazy` (default): wait for the element to enter the viewport before loading - */ - @property() loading?: 'idle' | 'lazy' | 'eager' = 'lazy'; - - /** Icon content. Any value that lit can render */ - @state() private content?: unknown; - - #intersecting = false; - - #logger = new Logger(this); - - get #class(): typeof BaseIcon { - return this.constructor as typeof BaseIcon; - } - - #lazyLoad() { - this.#class.io.observe(this); - if (this.#intersecting) { - this.load(); - } - } - - #iconChanged() { - switch (this.loading) { - case 'idle': return void ric(() => this.load()); - case 'lazy': return void this.#lazyLoad(); - case 'eager': return void this.load(); - } - } - - connectedCallback() { - super.connectedCallback(); - this.#class.instances.add(this); - } - - willUpdate(changed: PropertyValues) { - if (changed.has('icon')) { - this.#iconChanged(); - } - } - - disconnectedCallback() { - super.disconnectedCallback(); - this.#class.instances.delete(this); - } - - render() { - const content = this.content ?? ''; - return html` - - `; - } - - protected async load() { - const { set, icon } = this; - const getter = this.#class.getters.get(set) ?? this.#class.getIconUrl; - let spec = 'UNKNOWN ICON'; - if (set && icon) { - try { - const gotten = getter(set, icon); - if (gotten instanceof URL) { - spec = gotten.pathname; - } else { - spec = gotten; - } - const mod = await import(spec); - this.content = mod.default instanceof Node ? mod.default.cloneNode(true) : mod.default; - await this.updateComplete; - this.dispatchEvent(new Event('load', { bubbles: true })); - } catch (error: unknown) { - const event = new IconLoadError(spec, error as Error); - this.#logger.error((error as IconLoadError).message); - this.dispatchEvent(event); - } - } - } -} diff --git a/elements/pf-icon/pf-icon.css b/elements/pf-icon/pf-icon.css index efa78a0e9e..35ee1b57a2 100644 --- a/elements/pf-icon/pf-icon.css +++ b/elements/pf-icon/pf-icon.css @@ -1,3 +1,25 @@ +:host { + position: relative; + display: inline-block; + line-height: 0; + height: fit-content !important; + width: fit-content !important; +} + +#container { + display: grid; + grid-template: 1fr / 1fr; + place-content: center; +} + +#container.content ::slotted(*) { + display: none; +} + +svg { + fill: currentcolor; +} + :host([size=sm]) #container { --_size: var(--pf-global--icon--FontSize--sm, 10px); } :host([size=md]) #container { --_size: var(--pf-global--icon--FontSize--md, 18px); } :host([size=lg]) #container { --_size: var(--pf-global--icon--FontSize--lg, 24px); } diff --git a/elements/pf-icon/pf-icon.ts b/elements/pf-icon/pf-icon.ts index fe34abdad7..795865912c 100644 --- a/elements/pf-icon/pf-icon.ts +++ b/elements/pf-icon/pf-icon.ts @@ -1,26 +1,197 @@ -import { BaseIcon } from './BaseIcon.js'; +import { LitElement, html, type PropertyValues } from 'lit'; import { customElement } from 'lit/decorators/custom-element.js'; import { property } from 'lit/decorators/property.js'; +import { state } from 'lit/decorators/state.js'; + +import { Logger } from '@patternfly/pfe-core/controllers/logger.js'; import style from './pf-icon.css'; +export type URLGetter = (set: string, icon: string) => URL | string; + +/** requestIdleCallback when available, requestAnimationFrame when not */ +const ric = window.requestIdleCallback ?? window.requestAnimationFrame; + +/** Fired when an icon fails to load */ +class IconLoadError extends ErrorEvent { + constructor( + pathname: string, + /** The original error when importing the icon module */ + public originalError: Error + ) { + super('error', { message: `Could not load icon at ${pathname}` }); + } +} + /** * An **icon** component is a container that allows for icons of varying dimensions to * seamlessly replace each other without shifting surrounding content. - * * @slot - Slotted content is used as a fallback in case the icon doesn't load * @fires load - Fired when an icon is loaded and rendered * @fires error - Fired when an icon fails to load * @csspart fallback - Container for the fallback (i.e. slotted) content + * @cssprop {} --pf-icon--size - size of the icon */ @customElement('pf-icon') -export class PfIcon extends BaseIcon { - public static readonly styles = [...BaseIcon.styles, style]; +export class PfIcon extends LitElement { + public static readonly styles = [style]; public static defaultIconSet = 'fas'; + /** + * Register a new icon set + * @param setName - The name of the icon set + * @param getter - A function that returns the URL of an icon + * @example returning a URL object + * ```js + * PfIcon.addIconSet('rh', (set, icon) => + * new URL(`./icons/${set}/${icon}.js`, import.meta.url)); + * ``` + * @example returning a string + * ```js + * PfIcon.addIconSet('rh', (set, icon) => + * `/assets/icons/${set}/${icon}.js`); + * ``` + */ + public static addIconSet(setName: string, getter: URLGetter) { + if (typeof getter !== 'function') { + Logger.warn(`[${this.name}.addIconSet(setName, getter)]: getter must be a function`); + } else { + this.getters.set(setName, getter); + for (const instance of this.instances) { + instance.load(); + } + } + } + + /** + * Gets the URL of an icon. Override this to customize how icon URLs are resolved. + * @param set - The name of the icon set + * @param icon - The name of the icon + * @returns The URL of the icon + * @example returning a URL object + * ```js + * PfIcon.getIconUrl = (set, icon) => + * new URL(`./icons/${set}/${icon}.js`, import.meta.url); + * ``` + * @example returning a string + * ```js + * PfIcon.getIconUrl = (set, icon) => + * `/assets/icons/${set}/${icon}.js`; + * ``` + */ + public static getIconUrl: URLGetter = (set: string, icon: string) => + new URL(`./icons/${set}/${icon}.js`, import.meta.url); + + private static onIntersect: IntersectionObserverCallback = records => + records.forEach(({ isIntersecting, target }) => { + const icon = target as PfIcon; + icon.#intersecting = isIntersecting; + ric(() => { + if (icon.#intersecting) { + icon.load(); + } + }); + }); + + private static io = new IntersectionObserver(PfIcon.onIntersect); + + private static getters = new Map(); + + private static instances = new Set(); + + /** Icon set */ + @property() set = this.#class.defaultIconSet; + + /** Icon name */ + @property({ reflect: true }) icon = ''; + /** Size of the icon */ @property({ reflect: true }) size: 'sm' | 'md' | 'lg' | 'xl' = 'sm'; + + /** + * Controls how eager the element will be to load the icon data + * - `eager`: eagerly load the icon, blocking the main thread + * - `idle`: wait for the browser to attain an idle state before loading + * - `lazy` (default): wait for the element to enter the viewport before loading + */ + @property() loading?: 'idle' | 'lazy' | 'eager' = 'lazy'; + + /** Icon content. Any value that lit can render */ + @state() private content?: unknown; + + #intersecting = false; + + #logger = new Logger(this); + + get #class(): typeof PfIcon { + return this.constructor as typeof PfIcon; + } + + #lazyLoad() { + this.#class.io.observe(this); + if (this.#intersecting) { + this.load(); + } + } + + #iconChanged() { + switch (this.loading) { + case 'idle': return void ric(() => this.load()); + case 'lazy': return void this.#lazyLoad(); + case 'eager': return void this.load(); + } + } + + connectedCallback() { + super.connectedCallback(); + this.#class.instances.add(this); + } + + willUpdate(changed: PropertyValues) { + if (changed.has('icon')) { + this.#iconChanged(); + } + } + + disconnectedCallback() { + super.disconnectedCallback(); + this.#class.instances.delete(this); + } + + render() { + const content = this.content ?? ''; + return html` + + `; + } + + protected async load() { + const { set, icon } = this; + const getter = this.#class.getters.get(set) ?? this.#class.getIconUrl; + let spec = 'UNKNOWN ICON'; + if (set && icon) { + try { + const gotten = getter(set, icon); + if (gotten instanceof URL) { + spec = gotten.pathname; + } else { + spec = gotten; + } + const mod = await import(spec); + this.content = mod.default instanceof Node ? mod.default.cloneNode(true) : mod.default; + await this.updateComplete; + this.dispatchEvent(new Event('load', { bubbles: true })); + } catch (error: unknown) { + const event = new IconLoadError(spec, error as Error); + this.#logger.error((error as IconLoadError).message); + this.dispatchEvent(event); + } + } + } } declare global {