Skip to content

Commit

Permalink
Improved accuracy of IconProps interface (#3848)
Browse files Browse the repository at this point in the history
- Use the `IconName` and `IconPrefix` types provided by FontAwesome.
- Define a `ResolvedIconProps` interface, for specific usages where we know `iconName` and `prefix` have been applied and will be present.

Co-authored-by: lbwexler <[email protected]>
  • Loading branch information
amcclain and lbwexler authored Dec 27, 2024
1 parent 55f6f9b commit c46329f
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 59 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@
* Added workaround for problematic use of SASS-syntax-in-CSS shipped by `react-dates`. This began
throwing "This function isn't allowed in plain CSS" with latest version of sass/sass-loader.

### ⚙️ Typescript API Adjustments

* Improved accuracy of `IconProps` interface, with use of the `IconName` and `IconPrefix` types
provided by FontAwesome.

## v70.0.0 - 2024-11-15

### 💥 Breaking Changes (upgrade difficulty: 🟢 LOW - changes to advanced persistence APIs)
Expand Down
4 changes: 2 additions & 2 deletions admin/tabs/general/alertBanner/AlertBannerModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {fragment, p} from '@xh/hoist/cmp/layout';
import {HoistModel, Intent, LoadSpec, managed, PlainObject, XH} from '@xh/hoist/core';
import {dateIs, required} from '@xh/hoist/data';
import {action, bindable, computed, makeObservable, observable} from '@xh/hoist/mobx';
import {AlertBannerSpec} from '@xh/hoist/svc';
import {AlertBannerIconName, AlertBannerSpec} from '@xh/hoist/svc';
import {isEqual, isMatch, sortBy, without} from 'lodash';

export class AlertBannerModel extends HoistModel {
Expand Down Expand Up @@ -61,7 +61,7 @@ export class AlertBannerModel extends HoistModel {
return ['primary', 'success', 'warning', 'danger'];
}

get iconOptions() {
get iconOptions(): AlertBannerIconName[] {
return [
'bullhorn',
'check-circle',
Expand Down
4 changes: 2 additions & 2 deletions desktop/cmp/dash/container/DashContainerModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
TaskObserver,
XH
} from '@xh/hoist/core';
import {convertIconToHtml, deserializeIcon} from '@xh/hoist/icon';
import {convertIconToHtml, deserializeIcon, ResolvedIconProps} from '@xh/hoist/icon';
import {showContextMenu} from '@xh/hoist/kit/blueprint';
import {GoldenLayout} from '@xh/hoist/kit/golden-layout';
import {action, bindable, makeObservable, observable, runInAction} from '@xh/hoist/mobx';
Expand Down Expand Up @@ -540,7 +540,7 @@ export class DashContainerModel
if (icon) {
const $currentIcon = $el.find(iconSelector).first(),
currentIconType = $currentIcon ? $currentIcon?.data('icon') : null,
newIconType = icon.props.iconName;
newIconType = (icon.props as ResolvedIconProps).iconName;

if (currentIconType !== newIconType) {
const iconSvg = convertIconToHtml(icon);
Expand Down
95 changes: 55 additions & 40 deletions icon/Icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,23 @@
*
* Copyright © 2024 Extremely Heavy Industries Inc.
*/
import {IconName} from '@fortawesome/fontawesome-svg-core';
import {FontAwesomeIconProps} from '@fortawesome/react-fontawesome';
import {div} from '@xh/hoist/cmp/layout';
import {HoistProps, Intent, Thunkable} from '@xh/hoist/core';
import {throwIf} from '@xh/hoist/utils/js';
import classNames from 'classnames';
import {last, pickBy, split, toLower} from 'lodash';
import {ReactElement} from 'react';
import {iconCmp} from './impl/IconCmp';
import {enhanceFaClasses, iconHtml} from './impl/IconHtml';
import {ReactElement} from 'react';
import {HoistProps, Intent, Thunkable} from '@xh/hoist/core';

export interface IconProps extends HoistProps, Partial<Omit<FontAwesomeIconProps, 'ref'>> {
/** Name of the icon in FontAwesome. */
iconName?: string;
iconName?: IconName;

/**
* Prefix / weight of the icon (or "fab" if your app has imported the free-brand-icons pkg).
* - far - Regular
* - fas - Solid
* - fal - Light
* - fat - Thin (yes, unfortunate)
* - fab - Brand (requires optional import, see Toolbox)
*/
prefix?: 'far' | 'fas' | 'fal' | 'fat' | 'fab';
/** Weight / family style of the icon. */
prefix?: HoistIconPrefix;

intent?: Intent;

Expand Down Expand Up @@ -59,27 +53,50 @@ export interface IconProps extends HoistProps, Partial<Omit<FontAwesomeIconProps
omit?: Thunkable<boolean>;
}

/**
* Icon props after defaults applied by Hoist factory methods.
* @internal
*/
export interface ResolvedIconProps extends IconProps {
iconName: IconName;
prefix: HoistIconPrefix;
}

/** Supported FA prefixes, used to request a family-specific variant of an icon. */
export type HoistIconPrefix =
| 'far' // regular
| 'fas' // solid
| 'fal' // light
| 'fat' // thin
| 'fab'; // brands (requires optional import, see Toolbox)

/**
* Singleton class to provide factories for creating standard FontAwesome-based icons.
*
* Currently we are importing the licensed "pro" library with additional icons - note this requires
* fetching the FA npm package via a registry URL w/license token.
* Hoist imports the licensed "pro" library with additional icons - note this requires fetching the
* FA npm package via a registry URL w/license token.
*
* See https://fontawesome.com/pro#license.
*/
export const Icon = {
/**
* Return a standard Hoist FontAwesome-based icon.
* Return a Hoist element wrapper around a FontAwesome-based icon.
*
* Note that in order to use an icon with this factory, its definition must have been already
* imported and registered with FontAwesome via a call to library.add().
* Note that for an app to use an icon with this factory, its definition must have been already
* imported and registered with FontAwesome. Apps will find many/most of the icons they need
* pre-registered and enumerated by the Hoist factories below. Favor those ready-made factories
* wherever possible for consistency across and within apps.
*
* Applications will often not need to use this factory directly when creating specific
* icons enumerated by Hoist. In that case use the supplied factories on the Icon class
* directly (e.g. Icon.add(), Icon.book(), etc.) These factories will delegate to this method,
* with the name of a pre-imported icon preset.
* If the FA icon of your dreams is not available, however, you can do a one-time import within
* your app bootstrap code, eg:
* ```
* import {library} from '@fortawesome/fontawesome-svg-core';
* import {faDreamIcon} from '@fortawesome/pro-regular-svg-icons';
* library.add(faDreamIcon);
* ```
* and then pass its string name to this factory: `icon({iconName: 'dream-icon'})`
*/
icon(opts?: IconProps): any {
icon(opts: IconProps & {iconName: IconName}): any {
let {
iconName,
prefix = 'far',
Expand Down Expand Up @@ -908,47 +925,45 @@ export const Icon = {
};

/**
* Translate an icon into an html <svg/> tag.
* Translate an icon into an HTML `<svg>` tag.
*
* Not typically used by applications. Applications that need html for an icon, e.g.
* for a grid column renderer should use the 'asHtml' flag on the Icon factory functions
* instead.
* Not typically used by applications. Applications that need HTML for an icon should use the
* {@link IconProps.asHtml} flag on the Icon factory functions instead.
*
* @param iconElem - react element representing a Hoist Icon component.
* This must be element created by Hoist's built-in Icon factories.
* @returns html of the <svg> tag representing the icon.
* @param iconElem - React element representing a Hoist Icon component.
* Must be created by Hoist's built-in Icon factories.
* @returns HTML string for the icon's `<svg>` tag.
* @internal
*/
export function convertIconToHtml(iconElem: ReactElement): string {
throwIf(
!(iconElem?.type as any)?.isHoistComponent,
'Icon not provided, or not created by a Hoist Icon factory - cannot convert to HTML/SVG.'
);
return iconHtml(iconElem.props);
return iconHtml(iconElem.props as ResolvedIconProps);
}

/**
* Serialize an icon into a form that can be persisted.
* Serialize an icon into a JSON format with relevant props for persistence.
*
* @param iconElem - react element representing a icon component.
* This must be an element created by Hoist's built-in Icon factories.
* @returns json representation of icon.
* @param iconElem - React element representing a Hoist Icon component.
* Must be created by Hoist's built-in Icon factories.
* @returns JSON representation of icon.
*/
export function serializeIcon(iconElem: ReactElement): any {
throwIf(
!(iconElem?.type as any)?.isHoistComponent,
'Icon not provided, or not created by a Hoist Icon factory - cannot serialize.'
);

return pickBy(iconElem.props);
return pickBy(iconElem.props as ResolvedIconProps);
}

/**
* Deserialize an icon.
*
* This is the inverse operation of serializeIcon().
* Deserialize an icon - the inverse operation of {@link serializeIcon}.
*
* @param iconDef - json representation of icon, produced by serializeIcon.
* @returns react element representing a FontAwesome icon component.
* @param iconDef - JSON representation of icon, produced by serializeIcon.
* @returns React element representing a Hoist Icon component.
* This is the form of element created by Hoist's built-in Icon class factories.
*/
export function deserializeIcon(iconDef: any): ReactElement | string {
Expand Down
27 changes: 17 additions & 10 deletions icon/impl/IconHtml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,33 @@
*
* Copyright © 2024 Extremely Heavy Industries Inc.
*/
import {findIconDefinition, icon} from '@fortawesome/fontawesome-svg-core';
import {findIconDefinition, icon, IconName, IconPrefix} from '@fortawesome/fontawesome-svg-core';
import classNames from 'classnames';
import {isString} from 'lodash';

/**
* Get the raw text of an SVG tag for an icon
* Applications should use the factory methods on Icon instead.
* @internal
* Get the raw HTML string for an icon's SVG tag.
* @internal - apps should use the Hoist Icon factories instead with {@link IconProps.asHtml}.
*/
export function iconHtml({iconName, prefix, title, className, size}) {
export function iconHtml({
iconName,
prefix = 'far',
title,
className,
size
}: {
iconName: IconName;
prefix: IconPrefix;
title?: string;
className?: string;
size?: string;
}) {
const iconDef = findIconDefinition({prefix, iconName}),
classes = enhanceFaClasses(className, size);

return icon(iconDef, {classes, title}).html[0];
}

export function enhanceFaClasses(className: string, size: string) {
let ret = classNames(className, 'fa-fw', 'xh-icon');
if (isString(size)) {
ret = classNames(ret, `fa-${size}`);
}
return ret;
return classNames(className, 'fa-fw', 'xh-icon', isString(size) ? `fa-${size}` : null);
}
18 changes: 13 additions & 5 deletions svc/AlertBannerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*
* Copyright © 2024 Extremely Heavy Industries Inc.
*/
import {IconName} from '@fortawesome/fontawesome-svg-core';
import {BannerModel} from '@xh/hoist/appcontainer/BannerModel';
import {markdown} from '@xh/hoist/cmp/markdown';
import {BannerSpec, HoistService, Intent, XH} from '@xh/hoist/core';
Expand Down Expand Up @@ -47,7 +48,7 @@ export class AlertBannerService extends HoistService {
genBannerSpec(
message: string,
intent: Intent,
iconName: string,
iconName: IconName,
enableClose: boolean
): BannerSpec {
const icon = iconName ? Icon.icon({iconName, size: 'lg'}) : null,
Expand Down Expand Up @@ -90,19 +91,26 @@ export class AlertBannerService extends HoistService {
}
}

/**
* @internal
*/
/** @internal */
export interface AlertBannerSpec {
active: boolean;
expires: number;
publishDate: number;
message: string;
intent: Intent;
iconName: string;
iconName: AlertBannerIconName;
enableClose: boolean;
clientApps: string[];
created: number;
updated: number;
updatedBy: string;
}

/** @internal */
export type AlertBannerIconName =
| 'bullhorn'
| 'check-circle'
| 'exclamation-triangle'
| 'times-circle'
| 'info-circle'
| 'question-circle';

0 comments on commit c46329f

Please sign in to comment.