diff --git a/common/api/appui-abstract.api.md b/common/api/appui-abstract.api.md index cd6030c73e97..b9d388380f2d 100644 --- a/common/api/appui-abstract.api.md +++ b/common/api/appui-abstract.api.md @@ -1200,9 +1200,15 @@ export interface IconListEditorParams extends BasePropertyEditorParams { // @public export class IconSpecUtilities { + // @deprecated static createSvgIconSpec(svgSrc: string): string; + static createWebComponentIconSpec(srcString: string): string; + // @deprecated static getSvgSource(iconSpec: string): string | undefined; + static getWebComponentSource(iconSpec: string): string | undefined; static readonly SVG_PREFIX = "svg:"; + // (undocumented) + static readonly WEB_COMPONENT_PREFIX = "webSvg:"; } // @internal diff --git a/common/api/core-react.api.md b/common/api/core-react.api.md index ded54bac3f1f..8b7ca23af69d 100644 --- a/common/api/core-react.api.md +++ b/common/api/core-react.api.md @@ -2017,13 +2017,13 @@ export interface SvgPathProps extends CommonProps { viewBoxWidth: number; } -// @public +// @public @deprecated export class SvgSprite extends React.PureComponent { // (undocumented) render(): JSX.Element; } -// @public +// @public @deprecated export interface SvgSpriteProps extends CommonProps { src: string; } diff --git a/common/api/summary/core-react.exports.csv b/common/api/summary/core-react.exports.csv index 73c366d5a8e8..f77512ce4d0b 100644 --- a/common/api/summary/core-react.exports.csv +++ b/common/api/summary/core-react.exports.csv @@ -306,7 +306,9 @@ deprecated;Subheading2(props: TextProps): JSX.Element public;SvgPath public;SvgPathProps public;SvgSprite +deprecated;SvgSprite public;SvgSpriteProps +deprecated;SvgSpriteProps public;TabLabel public;Tabs public;TabsProps diff --git a/common/changes/@itwin/appui-abstract/ui-icon-loading_2022-03-15-22-30.json b/common/changes/@itwin/appui-abstract/ui-icon-loading_2022-03-15-22-30.json new file mode 100644 index 000000000000..772385955720 --- /dev/null +++ b/common/changes/@itwin/appui-abstract/ui-icon-loading_2022-03-15-22-30.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/appui-abstract", + "comment": "Implement svg icons loading as a web component.", + "type": "none" + } + ], + "packageName": "@itwin/appui-abstract" +} \ No newline at end of file diff --git a/common/changes/@itwin/appui-layout-react/ui-icon-loading_2022-03-15-22-30.json b/common/changes/@itwin/appui-layout-react/ui-icon-loading_2022-03-15-22-30.json new file mode 100644 index 000000000000..797686329f0d --- /dev/null +++ b/common/changes/@itwin/appui-layout-react/ui-icon-loading_2022-03-15-22-30.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/appui-layout-react", + "comment": "Implement svg icons loading as a web component.", + "type": "none" + } + ], + "packageName": "@itwin/appui-layout-react" +} \ No newline at end of file diff --git a/common/changes/@itwin/appui-react/ui-icon-loading_2022-03-15-22-30.json b/common/changes/@itwin/appui-react/ui-icon-loading_2022-03-15-22-30.json new file mode 100644 index 000000000000..025f39159886 --- /dev/null +++ b/common/changes/@itwin/appui-react/ui-icon-loading_2022-03-15-22-30.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/appui-react", + "comment": "Implement svg icons loading as a web component.", + "type": "none" + } + ], + "packageName": "@itwin/appui-react" +} \ No newline at end of file diff --git a/common/changes/@itwin/components-react/ui-icon-loading_2022-03-15-22-30.json b/common/changes/@itwin/components-react/ui-icon-loading_2022-03-15-22-30.json new file mode 100644 index 000000000000..2c22d33f38af --- /dev/null +++ b/common/changes/@itwin/components-react/ui-icon-loading_2022-03-15-22-30.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/components-react", + "comment": "Implement svg icons loading as a web component.", + "type": "none" + } + ], + "packageName": "@itwin/components-react" +} \ No newline at end of file diff --git a/common/changes/@itwin/core-react/ui-icon-loading_2022-03-15-22-30.json b/common/changes/@itwin/core-react/ui-icon-loading_2022-03-15-22-30.json new file mode 100644 index 000000000000..f82de9704848 --- /dev/null +++ b/common/changes/@itwin/core-react/ui-icon-loading_2022-03-15-22-30.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/core-react", + "comment": "Implement svg icons loading as a web component.", + "type": "none" + } + ], + "packageName": "@itwin/core-react" +} \ No newline at end of file diff --git a/docs/changehistory/NextVersion.md b/docs/changehistory/NextVersion.md index cc010f7bbfd0..bbd5a87fa7ae 100644 --- a/docs/changehistory/NextVersion.md +++ b/docs/changehistory/NextVersion.md @@ -22,3 +22,7 @@ iTwin.js applications can now check [WebGLRenderCompatibilityInfo.usingIntegrate ## ColorByName is an object, not an enum Enums in TypeScript have some shortcomings, one of which resulted in a bug that caused [ColorDef.fromString]($common) to return [ColorDef.black]($common) for some valid color strings like "aqua". This is due to several standard color names ("aqua" and "cyan", "magenta" and "fuschia", and several "grey" vs "gray" variations) having the same numeric values. To address this, [ColorByName]($common) has been converted from an `enum` to a `namespace`. Code that accesses `ColorByName` members by name will continue to compile with no change. + +## Deprecations in @itwin/core-react package + +Using the sprite loader for SVG icons is deprecated. This includes [SvgSprite]($core-react) and the methods getSvgIconSpec() and getSvgIconSource() methods on [IconSpecUtilities]($appui-abstract). The sprite loader has been replaced with a web component [IconWebComponent]($core-react) used by [Icon]($core-react) to load SVGs onto icons. diff --git a/docs/learning/ui/appui-react/Backstage.md b/docs/learning/ui/appui-react/Backstage.md index c83065661481..cc50368cc3f3 100644 --- a/docs/learning/ui/appui-react/Backstage.md +++ b/docs/learning/ui/appui-react/Backstage.md @@ -9,12 +9,12 @@ These overlays are an implementation of a modal frontstage. The backstage is ope To ensure that an extension can supply items for the Backstage menu, it should be created using the [BackstageComposer]($appui-react) component. The example below shows how to provide [BackstageActionItem]($appui-abstract) and [BackstageStageLauncher]($appui-abstract) item to the BackstageComposer. ```tsx -import stageIconSvg from "@bentley/icons-generic/icons/imodeljs.svg?sprite"; -import settingsIconSvg from "@bentley/icons-generic/icons/settings.svg?sprite"; +import stageIconSvg from "@bentley/icons-generic/icons/imodeljs.svg"; +import settingsIconSvg from "@bentley/icons-generic/icons/settings.svg"; export function AppBackstageComposer() { const [backstageItems] = React.useState(() => [ - BackstageItemUtilities.createStageLauncher("app.SampleFrontstage", 100, 10, IModelApp.i18n.translate("app:backstage.sampleFrontstage"), undefined, IconSpecUtilities.createSvgIconSpec(stageIconSvg)), + BackstageItemUtilities.createStageLauncher("app.SampleFrontstage", 100, 10, IModelApp.i18n.translate("app:backstage.sampleFrontstage"), undefined, IconSpecUtilities.createWebComponentIconSpec(stageIconSvg)), SettingsModalFrontstage.getBackstageActionItem (300, 10), ]); diff --git a/docs/learning/ui/core/Icon.md b/docs/learning/ui/core/Icon.md index 3f3bf44eddb3..70d88d47f610 100644 --- a/docs/learning/ui/core/Icon.md +++ b/docs/learning/ui/core/Icon.md @@ -2,17 +2,24 @@ The [Icon]($core-react:Icon) category in the `@itwin/core-react` package includes components that render icons when given an icon name or SVG source or path. -The [Icon]($core-react) React component displays an icon based on an IconSpec. -An [IconSpec]($core-react:Icon) can be a string, ReactNode or -[ConditionalStringValue]($appui-abstract). -When the IconSpec is a string, the value is either a Webfont name or a formatted string that includes an imported SVG. -When using an SVG, it must be imported using the webpack loader `svg-sprite-loader`. -The formatted string begins with "svg:". -The `IconSpecUtilities.createSvgIconSpec` can be used to format the SVG string. See example usage below. +The [Icon]($core-react) React component displays an icon based on an IconSpec. An [IconSpec]($core-react:Icon) can be a string, ReactNode or [ConditionalStringValue]($appui-abstract). +When the IconSpec is a string, the value is either a Webfont symbol name or a formatted string that specifies an imported SVG. +When using an SVG, we use the web component svg-loader to load the SVG from its path. This web component is defined in UiCore.initialize() as follows: -The [SvgSprite]($core-react) React component displays an icon using `` and `` elements to reference an imported SVG file. The SVG file must be imported using the webpack loader `svg-sprite-loader`. +```tsx + if (window.customElements.get("svg-loader") === undefined) + window.customElements.define("svg-loader", IconWebComponent); +``` + +If your app does not initialize UiFramework or UiCore, you'll need to define this custom element to use the svg-loader. -The [SvgSprite]($core-react) React component displays an icon using an `` element and an array of SVG paths. +The [Icon]($core-react) will automatically use the svg-loader in IconWebComponent, assuming the customElement has been defined. + +The formatted svg icon string begins with "webSvg:". +The `IconSpecUtilities.createWebComponentIconSpec` can be used to format the SVG string. See example usage below. + +The sprite SVG loader has been deprecated. +The [SvgSprite]($core-react) React component has been deprecated. ## Examples @@ -32,9 +39,9 @@ This example shows how to use an SVG from `@bentley/icons-generic`. import { IconSpecUtilities } from "@itwin/appui-abstract"; import { Icon } from "@itwin/core-react"; -import placeholderSvg from "@bentley/icons-generic/icons/placeholder.svg?sprite"; +import placeholderSvg from "@bentley/icons-generic/icons/placeholder.svg"; . . . -const iconSpec = IconSpecUtilities.createSvgIconSpec(placeholderSvg); +const iconSpec = IconSpecUtilities.createWebComponentIconSpec(placeholderSvg); . . . ``` @@ -44,24 +51,13 @@ const iconSpec = IconSpecUtilities.createSvgIconSpec(placeholderSvg); This example shows how to use an SVG from within the application. ```tsx -import rotateSvg from "../icons/rotate.svg?sprite"; +import rotateSvg from "../icons/rotate.svg"; . . . -const iconSpec = IconSpecUtilities.createSvgIconSpec(rotateSvg); +const iconSpec = IconSpecUtilities.createWebComponentIconSpec(rotateSvg); . . . ``` -### SvgSprite and SVG File - -Rather then using the Icon component with SVG files, the SvgSprite component can be used. - -```tsx -import { Icon } from "@itwin/core-react"; -import rotateSvg from "../icons/rotate.svg?sprite"; -. . . - -``` - ### SvgPath When you have SVG path statements instead of SVG files, the SvgPath component can be used. diff --git a/test-apps/ui-items-providers-test/package.json b/test-apps/ui-items-providers-test/package.json index b15c3cbb077c..2579a37e02ab 100644 --- a/test-apps/ui-items-providers-test/package.json +++ b/test-apps/ui-items-providers-test/package.json @@ -5,12 +5,11 @@ "main": "lib/ui-items-providers-test.js", "typings": "lib/ui-items-providers-test", "scripts": { - "prebuild": "npm run -s pseudolocalize && npm run -s copy:assets && npm run -s copy:locale", + "prebuild": "npm run -s pseudolocalize && npm run -s copy:assets", "build": "npm run -s prebuild && tsc 1>&2", "build:ci": "npm run -s build", "clean": "rimraf lib .rush/temp/package-deps*.json", - "copy:assets": "cpx \"./src/**/*.{*css,json,svg}\" \"./lib\" && cpx \"./src/public/**/*\" ./lib/public/", - "copy:locale": "cpx \"./src/public/locales/**/*\" ./lib/public", + "copy:assets": "cpx \"./src/**/*.{*css,json,svg}\" \"./lib\"", "cover": "", "docs": "", "lint": "eslint -f visualstudio \"./src/**/*.{ts,tsx}\" 1>&2", @@ -68,4 +67,4 @@ ], "extends": "plugin:@itwin/itwinjs-recommended" } -} +} \ No newline at end of file diff --git a/test-apps/ui-items-providers-test/src/tools/GenericLocateTool.ts b/test-apps/ui-items-providers-test/src/tools/GenericLocateTool.ts index f7ce689b17e2..5febee6ed040 100644 --- a/test-apps/ui-items-providers-test/src/tools/GenericLocateTool.ts +++ b/test-apps/ui-items-providers-test/src/tools/GenericLocateTool.ts @@ -11,9 +11,9 @@ import { } from "@itwin/core-frontend"; import { Point3d } from "@itwin/core-geometry"; import { UiFramework } from "@itwin/appui-react"; -import { ToolbarItemUtilities } from "@itwin/appui-abstract"; -import genericToolSvg from "./generic-tool.svg?sprite"; +import { IconSpecUtilities, ToolbarItemUtilities } from "@itwin/appui-abstract"; import { UiItemsProvidersTest } from "../ui-items-providers-test"; +import genericToolSvg from "./generic-tool.svg"; /** Sample Primitive tool where user selects an element for processing */ export class GenericLocateTool extends PrimitiveTool { @@ -21,7 +21,7 @@ export class GenericLocateTool extends PrimitiveTool { public elementId: string | undefined; public static override get toolId() { return "uiItemsProvidersTest-GenericLocateTool"; } public static get toolStringKey() { return `tools.${GenericLocateTool.toolId}.`; } - public static override iconSpec = `svg:${genericToolSvg}`; + public static override iconSpec = genericToolSvg; public static useDefaultPosition = false; public override autoLockTarget(): void { } // NOTE: For selecting elements we only care about iModel, so don't lock target model automatically. protected wantSelectionClearOnMiss(_ev: BeButtonEvent): boolean { return SelectionMode.Replace === this.getSelectionMode(); } @@ -120,7 +120,8 @@ export class GenericLocateTool extends PrimitiveTool { public static getActionButtonDef(itemPriority: number, groupPriority?: number) { const overrides = undefined !== groupPriority ? { groupPriority } : {}; - return ToolbarItemUtilities.createActionButton(GenericLocateTool.toolId, itemPriority, GenericLocateTool.iconSpec, GenericLocateTool.flyover, + const iconSpec = IconSpecUtilities.createWebComponentIconSpec(this.iconSpec); + return ToolbarItemUtilities.createActionButton(GenericLocateTool.toolId, itemPriority, iconSpec, GenericLocateTool.flyover, async () => { await IModelApp.tools.run(GenericLocateTool.toolId, IModelApp.viewManager.selectedView, true); }, overrides); } diff --git a/test-apps/ui-items-providers-test/src/tools/OpenTraceDialogTool.tsx b/test-apps/ui-items-providers-test/src/tools/OpenTraceDialogTool.tsx index b3b4fc09086d..0907967df426 100644 --- a/test-apps/ui-items-providers-test/src/tools/OpenTraceDialogTool.tsx +++ b/test-apps/ui-items-providers-test/src/tools/OpenTraceDialogTool.tsx @@ -12,10 +12,7 @@ import { ModalDialogManager } from "@itwin/appui-react"; import { SampleModalDialog } from "../ui/dialogs/SampleModalDialog"; import { IconSpecUtilities, ToolbarItemUtilities } from "@itwin/appui-abstract"; import { UiItemsProvidersTest } from "../ui-items-providers-test"; - -/** the following will import svgs into DOM and generate SymbolId that is used to locate the svg image. This - * processing is done via the 'magic' webpack plugin and requires the use or the Bentley build scripts. */ -import connectedIcon from "../ui/icons/connected-query.svg?sprite"; +import connectedQuerySvg from "../ui/icons/connected-query.svg"; /** * Immediate tool that will open an example modal dialog.The tool is created and register to allow the user @@ -24,7 +21,7 @@ import connectedIcon from "../ui/icons/connected-query.svg?sprite"; */ export class OpenTraceDialogTool extends Tool { public static override toolId = "uiItemsProvidersTest-OpenTraceDialogTool"; - public static override iconSpec = IconSpecUtilities.createSvgIconSpec(connectedIcon); + public static override iconSpec = connectedQuerySvg; // istanbul ignore next public static override get minArgs() { return 0; } @@ -53,7 +50,8 @@ export class OpenTraceDialogTool extends Tool { const overrides = { groupPriority, }; - return ToolbarItemUtilities.createActionButton(OpenTraceDialogTool.toolId, itemPriority, OpenTraceDialogTool.iconSpec, OpenTraceDialogTool.flyover, + const iconSpec = IconSpecUtilities.createWebComponentIconSpec(`${this.iconSpec}`); + return ToolbarItemUtilities.createActionButton(OpenTraceDialogTool.toolId, itemPriority, iconSpec, OpenTraceDialogTool.flyover, async () => { await IModelApp.tools.run(OpenTraceDialogTool.toolId); }, overrides); } diff --git a/test-apps/ui-items-providers-test/src/tools/SampleTool.ts b/test-apps/ui-items-providers-test/src/tools/SampleTool.ts index 4859f57207a6..7523f0e4026f 100644 --- a/test-apps/ui-items-providers-test/src/tools/SampleTool.ts +++ b/test-apps/ui-items-providers-test/src/tools/SampleTool.ts @@ -13,6 +13,7 @@ import { } from "@itwin/core-frontend"; import { ColorEditorParams, DialogItem, DialogItemValue, DialogPropertySyncItem, + IconSpecUtilities, InputEditorSizeParams, PropertyDescription, PropertyEditorParamTypes, StandardEditorNames, SuppressLabelEditorParams, ToolbarItemUtilities, } from "@itwin/appui-abstract"; @@ -22,8 +23,8 @@ import { Point3d } from "@itwin/core-geometry"; import { ColorByName, ColorDef } from "@itwin/core-common"; import { FormatterSpec } from "@itwin/core-quantity"; import { CursorInformation, MenuItemProps, UiFramework } from "@itwin/appui-react"; -import sampleToolSvg from "./SampleTool.svg?sprite"; import { UiItemsProvidersTest } from "../ui-items-providers-test"; +import sampleToolSvg from "./SampleTool.svg"; enum ToolOptions { Red, @@ -37,7 +38,7 @@ enum ToolOptions { export class SampleTool extends PrimitiveTool { // ensure toolId is unique by adding "uiItemsProvidersTest-" prefix public static override toolId = "uiItemsProvidersTest-SampleTool"; - public static override iconSpec = `svg:${sampleToolSvg}`; + public static override iconSpec = sampleToolSvg; public readonly points: Point3d[] = []; private _showCoordinatesOnPointerMove = false; private _stationFormatterSpec?: FormatterSpec; @@ -529,7 +530,9 @@ export class SampleTool extends PrimitiveTool { public static getActionButtonDef(itemPriority: number, groupPriority?: number) { const overrides = undefined !== groupPriority ? { groupPriority } : {}; - return ToolbarItemUtilities.createActionButton(SampleTool.toolId, itemPriority, SampleTool.iconSpec, SampleTool.flyover, + const iconSpec = IconSpecUtilities.createWebComponentIconSpec(this.iconSpec); + + return ToolbarItemUtilities.createActionButton(SampleTool.toolId, itemPriority, iconSpec, SampleTool.flyover, async () => { await IModelApp.tools.run(SampleTool.toolId); }, overrides); } diff --git a/test-apps/ui-items-providers-test/src/ui/providers/CustomContentUiProvider.tsx b/test-apps/ui-items-providers-test/src/ui/providers/CustomContentUiProvider.tsx index 34403c3c574a..85a0e49c1edf 100644 --- a/test-apps/ui-items-providers-test/src/ui/providers/CustomContentUiProvider.tsx +++ b/test-apps/ui-items-providers-test/src/ui/providers/CustomContentUiProvider.tsx @@ -10,7 +10,7 @@ import { import { IModelApp, NotifyMessageDetails, OutputMessagePriority, OutputMessageType } from "@itwin/core-frontend"; import { UiItemsProvidersTest } from "../../ui-items-providers-test"; import { CustomFrontstage } from "../frontstages/CustomContent"; -import visibilityIcon from "../icons/visibility-semi-transparent.svg?sprite"; +import visibilitySemiTransparentSvg from "../icons/visibility-semi-transparent.svg"; /** * Test UiItemsProvider that provide buttons, widgets, and backstage item to NetworkTracing stage. @@ -38,11 +38,12 @@ export class CustomContentUiProvider implements UiItemsProvider { toolbarUsage === ToolbarUsage.ContentManipulation && toolbarOrientation === ToolbarOrientation.Horizontal ) { + const iconData = IconSpecUtilities.createWebComponentIconSpec(visibilitySemiTransparentSvg); const getSvgTestButton = ToolbarItemUtilities.createActionButton( "custom-visibility-tool", -1, - IconSpecUtilities.createSvgIconSpec(visibilityIcon), + iconData, "Custom Visibility Tool", (): void => { IModelApp.notifications.outputMessage(new NotifyMessageDetails(OutputMessagePriority.Info, "custom-visibility-tool activated", undefined, OutputMessageType.Toast)); diff --git a/test-apps/ui-items-providers-test/src/ui/providers/GeneralUiItemsProvider.ts b/test-apps/ui-items-providers-test/src/ui/providers/GeneralUiItemsProvider.ts index 24596e767292..fc1ebbd6646d 100644 --- a/test-apps/ui-items-providers-test/src/ui/providers/GeneralUiItemsProvider.ts +++ b/test-apps/ui-items-providers-test/src/ui/providers/GeneralUiItemsProvider.ts @@ -5,14 +5,14 @@ import { AbstractStatusBarItemUtilities, - CommonStatusBarItem, CommonToolbarItem, StageUsage, StatusBarSection, ToolbarOrientation, ToolbarUsage, UiItemsProvider, + CommonStatusBarItem, CommonToolbarItem, IconSpecUtilities, StageUsage, StatusBarSection, ToolbarOrientation, ToolbarUsage, UiItemsProvider, } from "@itwin/appui-abstract"; import { SampleTool } from "../../tools/SampleTool"; -import statusBarButtonSvg from "../icons/StatusField.svg?sprite"; import { UnitsPopupUiDataProvider } from "../dialogs/UnitsPopup"; import { IModelApp } from "@itwin/core-frontend"; import { UiItemsProvidersTest } from "../../ui-items-providers-test"; import { OpenAbstractDialogTool } from "../../tools/OpenAbstractModalDialogTool"; +import statusFieldSvg from "../icons/StatusField.svg"; /** * The GeneralUiItemsProvider provides additional items to any frontstage that has a usage value of StageUsage.General. @@ -35,7 +35,7 @@ export class GeneralUiItemsProvider implements UiItemsProvider { } public provideStatusBarItems(_stageId: string, stageUsage: string): CommonStatusBarItem[] { - const unitsIcon = `svg:${statusBarButtonSvg}`; + const unitsIcon = IconSpecUtilities.createWebComponentIconSpec(statusFieldSvg); const statusBarItems: CommonStatusBarItem[] = []; if (stageUsage === StageUsage.General) { statusBarItems.push( diff --git a/test-apps/ui-items-providers-test/src/ui/providers/NetworkTracingUiProvider.tsx b/test-apps/ui-items-providers-test/src/ui/providers/NetworkTracingUiProvider.tsx index 50806b504028..68c7fefd1e72 100644 --- a/test-apps/ui-items-providers-test/src/ui/providers/NetworkTracingUiProvider.tsx +++ b/test-apps/ui-items-providers-test/src/ui/providers/NetworkTracingUiProvider.tsx @@ -24,12 +24,9 @@ import { getTestProviderState, setIsTraceAvailable } from "../../store"; import { UiItemsProvidersTest } from "../../ui-items-providers-test"; import { SelectedElementDataWidgetComponent } from "../widgets/SelectedElementDataWidget"; import { VisibilityTreeComponent } from "../widgets/VisibilityWidget"; - -/** the following will import svgs into DOM and generate SymbolId that is used to locate the svg image. This - * processing is done via the 'magic' webpack plugin and requires the use or the Bentley build scripts. */ -import upstreamIcon from "../icons/upstream-query.svg?sprite"; -import downstreamIcon from "../icons/downstream-query.svg?sprite"; -import traceIcon from "../icons/query-multi.svg?sprite"; +import downstreamQuerySvg from "../icons/downstream-query.svg"; +import queryMultiSvg from "../icons/query-multi.svg"; +import upstreamQuerySvg from "../icons/upstream-query.svg"; /** * Test UiItemsProvider that provide buttons, widgets, and backstage item to NetworkTracing stage. @@ -88,7 +85,7 @@ export class NetworkTracingUiProvider implements UiItemsProvider { const getDownstreamButton = ToolbarItemUtilities.createActionButton( "trace-tool-downstream", 15, /* order within group button */ - IconSpecUtilities.createSvgIconSpec(downstreamIcon), + IconSpecUtilities.createWebComponentIconSpec(downstreamQuerySvg), UiItemsProvidersTest.translate("trace-tool-downstream"), (): void => { IModelApp.notifications.outputMessage(new NotifyMessageDetails(OutputMessagePriority.Info, "trace-tool-downstream activated", undefined, OutputMessageType.Toast)); @@ -103,7 +100,7 @@ export class NetworkTracingUiProvider implements UiItemsProvider { const getUpstreamButton = ToolbarItemUtilities.createActionButton( "trace-tool-upstream", 20, /* order within group button */ - IconSpecUtilities.createSvgIconSpec(upstreamIcon), + IconSpecUtilities.createWebComponentIconSpec(upstreamQuerySvg), UiItemsProvidersTest.translate("trace-tool-upstream"), (): void => { IModelApp.notifications.outputMessage(new NotifyMessageDetails(OutputMessagePriority.Info, "trace-tool-upstream activated", undefined, OutputMessageType.Toast)); @@ -120,7 +117,8 @@ export class NetworkTracingUiProvider implements UiItemsProvider { */ const groupSpec = ToolbarItemUtilities.createGroupButton( "trace-tool-group", 230, - IconSpecUtilities.createSvgIconSpec(traceIcon), UiItemsProvidersTest.translate("trace-tool-group"), + IconSpecUtilities.createWebComponentIconSpec(queryMultiSvg), + UiItemsProvidersTest.translate("trace-tool-group"), [getConnectedButton, getDownstreamButton, getUpstreamButton], { badgeType: BadgeType.TechnicalPreview, diff --git a/test-apps/ui-test-app/src/frontend/appui/frontstages/ViewsFrontstage.tsx b/test-apps/ui-test-app/src/frontend/appui/frontstages/ViewsFrontstage.tsx index 7e1b722bc043..978ac4bc5b22 100644 --- a/test-apps/ui-test-app/src/frontend/appui/frontstages/ViewsFrontstage.tsx +++ b/test-apps/ui-test-app/src/frontend/appui/frontstages/ViewsFrontstage.tsx @@ -26,8 +26,6 @@ import { } from "@itwin/appui-react"; import { Button, Slider } from "@itwin/itwinui-react"; import { SampleAppIModelApp, SampleAppUiActionId } from "../../../frontend/index"; -// SVG Support - SvgPath or SvgSprite -// import { SvgPath } from "@itwin/core-react"; import { AccuDrawPopupTools } from "../../tools/AccuDrawPopupTools"; import { AppTools } from "../../tools/ToolSpecifications"; import { ToolWithDynamicSettings } from "../../tools/ToolWithDynamicSettings"; diff --git a/test-apps/ui-test-app/src/frontend/appui/frontstages/component-examples/ComponentExamplesProvider.tsx b/test-apps/ui-test-app/src/frontend/appui/frontstages/component-examples/ComponentExamplesProvider.tsx index 9def1c51d3b7..d8a9d2469aa5 100644 --- a/test-apps/ui-test-app/src/frontend/appui/frontstages/component-examples/ComponentExamplesProvider.tsx +++ b/test-apps/ui-test-app/src/frontend/appui/frontstages/component-examples/ComponentExamplesProvider.tsx @@ -8,6 +8,8 @@ import * as React from "react"; import { BeDuration, Logger } from "@itwin/core-bentley"; import moreSvg from "@bentley/icons-generic/icons/more-circular.svg?sprite"; import moreVerticalSvg from "@bentley/icons-generic/icons/more-vertical-circular.svg?sprite"; +import moreWebSvg from "@bentley/icons-generic/icons/more-circular.svg"; +import moreVerticalWebSvg from "@bentley/icons-generic/icons/more-vertical-circular.svg"; import { ColorByName, ColorDef } from "@itwin/core-common"; import { ActivityMessageDetails, ActivityMessageEndReason, IModelApp, NotifyMessageDetails, OutputMessagePriority, OutputMessageType, QuantityType, @@ -812,7 +814,8 @@ export class ComponentExamplesProvider { createComponentExample("Labeled Textarea", "Labeled Textarea component", ), createComponentExample("Image Checkbox", "ImageCheckbox with WebFonts", ), - createComponentExample("Image Checkbox", "ImageCheckbox with SVG fonts", ), + createComponentExample("Image Checkbox", "ImageCheckbox with SVG (deprecate sprite)", ), + createComponentExample("Image Checkbox", "ImageCheckbox with SVG using web component", ), createComponentExample("Input Described By", "Input with aria-describedby",
diff --git a/test-apps/ui-test-app/src/frontend/appui/frontstages/component-examples/SampleTimelineComponent.tsx b/test-apps/ui-test-app/src/frontend/appui/frontstages/component-examples/SampleTimelineComponent.tsx index 55ddddb2454b..70d40fef8af7 100644 --- a/test-apps/ui-test-app/src/frontend/appui/frontstages/component-examples/SampleTimelineComponent.tsx +++ b/test-apps/ui-test-app/src/frontend/appui/frontstages/component-examples/SampleTimelineComponent.tsx @@ -2,6 +2,7 @@ * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ +/* eslint-disable deprecation/deprecation */ import "./SampleTimelineComponent.scss"; import * as React from "react"; import starSvg from "@bentley/icons-generic/icons/star.svg?sprite"; diff --git a/test-apps/ui-test-app/src/frontend/tools/ImmediateTools.tsx b/test-apps/ui-test-app/src/frontend/tools/ImmediateTools.tsx index 12ebdcbe84a9..c9258f86f09f 100644 --- a/test-apps/ui-test-app/src/frontend/tools/ImmediateTools.tsx +++ b/test-apps/ui-test-app/src/frontend/tools/ImmediateTools.tsx @@ -2,6 +2,7 @@ * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ +/* eslint-disable deprecation/deprecation */ /** @packageDocumentation * @module Tools */ @@ -20,12 +21,12 @@ import { ChildWindowLocationProps, ContentDialog, ContentDialogManager, ContentGroup, ContentLayoutManager, ContentProps, FrontstageManager, StageContentLayout, StageContentLayoutProps, UiFramework, } from "@itwin/appui-react"; -import toolIconSvg from "@bentley/icons-generic/icons/window-add.svg?sprite"; -import tool2IconSvg from "@bentley/icons-generic/icons/window-maximize.svg?sprite"; -import tool3IconSvg from "@bentley/icons-generic/icons/3d-render.svg?sprite"; -import tool4IconSvg from "@bentley/icons-generic/icons/3d.svg?sprite"; -import layoutRestoreIconSvg from "@bentley/icons-generic/icons/download.svg?sprite"; -import removeLayoutIconSvg from "@bentley/icons-generic/icons/remove.svg?sprite"; +import toolIconSvg from "@bentley/icons-generic/icons/window-add.svg"; +import tool2IconSvg from "@bentley/icons-generic/icons/window-maximize.svg"; +import tool3IconSvg from "@bentley/icons-generic/icons/3d-render.svg"; +import tool4IconSvg from "@bentley/icons-generic/icons/3d.svg"; +import layoutRestoreIconSvg from "@bentley/icons-generic/icons/download.svg"; +import removeLayoutIconSvg from "@bentley/icons-generic/icons/remove.svg"; import layoutSaveIconSvg from "@bentley/icons-generic/icons/upload.svg?sprite"; import { PopupTestPanel } from "./PopupTestPanel"; import { PopupTestView } from "./PopupTestView"; @@ -129,7 +130,7 @@ export class SaveContentLayoutTool extends Tool { export class RestoreSavedContentLayoutTool extends Tool { public static override toolId = "RestoreSavedContentLayoutTool"; - public static override iconSpec = IconSpecUtilities.createSvgIconSpec(layoutRestoreIconSvg); + public static override iconSpec = IconSpecUtilities.createWebComponentIconSpec(layoutRestoreIconSvg); public static override get minArgs() { return 0; } public static override get maxArgs() { return 0; } public static override get keyin(): string { @@ -161,7 +162,7 @@ export class RestoreSavedContentLayoutTool extends Tool { export class RemoveSavedContentLayoutTool extends Tool { public static override toolId = "RemoveSavedContentLayoutTool"; - public static override iconSpec = IconSpecUtilities.createSvgIconSpec(removeLayoutIconSvg); + public static override iconSpec = IconSpecUtilities.createWebComponentIconSpec(removeLayoutIconSvg); public static override get minArgs() { return 0; } public static override get maxArgs() { return 0; } public static override get keyin(): string { @@ -184,7 +185,7 @@ export class RemoveSavedContentLayoutTool extends Tool { export class OpenComponentExamplesPopoutTool extends Tool { public static override toolId = "openComponentExamplesChildWindow"; - public static override iconSpec = IconSpecUtilities.createSvgIconSpec(toolIconSvg); + public static override iconSpec = "@bentley/icons-generic/icons/window-add.svg"; public static override get minArgs() { return 0; } public static override get maxArgs() { return 0; } @@ -225,13 +226,14 @@ export class OpenComponentExamplesPopoutTool extends Tool { const overrides = { groupPriority, }; - return ToolbarItemUtilities.createActionButton(OpenComponentExamplesPopoutTool.toolId, itemPriority, OpenComponentExamplesPopoutTool.iconSpec, OpenComponentExamplesPopoutTool.flyover, + const iconSpec = IconSpecUtilities.createWebComponentIconSpec(toolIconSvg); + return ToolbarItemUtilities.createActionButton(OpenComponentExamplesPopoutTool.toolId, itemPriority, iconSpec, OpenComponentExamplesPopoutTool.flyover, async () => { await IModelApp.tools.run(OpenComponentExamplesPopoutTool.toolId); }, overrides); } } export class OpenCustomPopoutTool extends Tool { public static override toolId = "OpenCustomPopout"; - public static override iconSpec = IconSpecUtilities.createSvgIconSpec(tool2IconSvg); + public static override iconSpec = IconSpecUtilities.createWebComponentIconSpec(tool2IconSvg); public static override get minArgs() { return 0; } public static override get maxArgs() { return 0; } @@ -275,7 +277,7 @@ export class OpenCustomPopoutTool extends Tool { export class OpenViewPopoutTool extends Tool { public static override toolId = "OpenViewPopout"; - public static override iconSpec = IconSpecUtilities.createSvgIconSpec(tool3IconSvg); + public static override iconSpec = IconSpecUtilities.createWebComponentIconSpec(tool3IconSvg); public static override get minArgs() { return 0; } public static override get maxArgs() { return 0; } @@ -345,7 +347,7 @@ export function IModelViewDialog({ x, y, id, title }: { x?: number, y?: number, export class OpenViewDialogTool extends Tool { private static _counter = 0; public static override toolId = "OpenViewDialog"; - public static override iconSpec = IconSpecUtilities.createSvgIconSpec(tool4IconSvg); + public static override iconSpec = IconSpecUtilities.createWebComponentIconSpec(tool4IconSvg); public static get dialogId(): string { return `ui-test-app:popup-view-dialog-${OpenViewDialogTool._counter}`; } diff --git a/test-apps/ui-test-app/src/frontend/tools/InspectTool.ts b/test-apps/ui-test-app/src/frontend/tools/InspectTool.ts index 2d83be5086d6..7c346e222569 100644 --- a/test-apps/ui-test-app/src/frontend/tools/InspectTool.ts +++ b/test-apps/ui-test-app/src/frontend/tools/InspectTool.ts @@ -13,7 +13,7 @@ import { BeButtonEvent, EventHandled, IModelApp, PrimitiveTool } from "@itwin/co import { IconSpecUtilities, ToolbarItemUtilities, } from "@itwin/appui-abstract"; -import inspectIconSvg from "@bentley/icons-generic/icons/search.svg?sprite"; +import inspectIconSvg from "@bentley/icons-generic/icons/search.svg"; export class InspectUiItemInfoTool extends PrimitiveTool { private _timerId: number | undefined; @@ -23,7 +23,7 @@ export class InspectUiItemInfoTool extends PrimitiveTool { private static _counter = 0; public static override toolId = "InspectUiItemInfoTool"; - public static override iconSpec = IconSpecUtilities.createSvgIconSpec(inspectIconSvg); + public static override iconSpec = IconSpecUtilities.createWebComponentIconSpec(inspectIconSvg); public static override get minArgs() { return 0; } public static override get maxArgs() { return 0; } diff --git a/test-apps/ui-test-app/src/frontend/tools/Tool2.ts b/test-apps/ui-test-app/src/frontend/tools/Tool2.ts index 838e08858b0f..f63e9afb71de 100644 --- a/test-apps/ui-test-app/src/frontend/tools/Tool2.ts +++ b/test-apps/ui-test-app/src/frontend/tools/Tool2.ts @@ -2,9 +2,10 @@ * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ +/* eslint-disable deprecation/deprecation */ import { Point3d } from "@itwin/core-geometry"; -import placeholderSvg from "@bentley/icons-generic/icons/placeholder.svg?sprite"; +import placeholderSvg from "@bentley/icons-generic/icons/placeholder.svg"; import { BeButtonEvent, EventHandled, IModelApp, PrimitiveTool, ToolAssistance, ToolAssistanceImage, ToolAssistanceInputMethod, } from "@itwin/core-frontend"; @@ -12,7 +13,7 @@ import { IconSpecUtilities } from "@itwin/appui-abstract"; export class Tool2 extends PrimitiveTool { public static override toolId = "Tool2"; - public static override iconSpec = IconSpecUtilities.createSvgIconSpec(placeholderSvg); + public static override iconSpec = IconSpecUtilities.createWebComponentIconSpec(placeholderSvg); public readonly points: Point3d[] = []; public override requireWriteableTarget(): boolean { return false; } diff --git a/test-apps/ui-test-app/src/frontend/tools/ToolSpecifications.tsx b/test-apps/ui-test-app/src/frontend/tools/ToolSpecifications.tsx index 15828678cce5..b684d5ecea7a 100644 --- a/test-apps/ui-test-app/src/frontend/tools/ToolSpecifications.tsx +++ b/test-apps/ui-test-app/src/frontend/tools/ToolSpecifications.tsx @@ -2,6 +2,7 @@ * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ +/* eslint-disable deprecation/deprecation */ import * as React from "react"; import imperialIconSvg from "@bentley/icons-generic/icons/app-2.svg?sprite"; import automationIconSvg from "@bentley/icons-generic/icons/automation.svg?sprite"; diff --git a/ui/appui-abstract/src/appui-abstract/backstage/BackstageItem.ts b/ui/appui-abstract/src/appui-abstract/backstage/BackstageItem.ts index d2389ed5b25c..c58e4ba2e5f6 100644 --- a/ui/appui-abstract/src/appui-abstract/backstage/BackstageItem.ts +++ b/ui/appui-abstract/src/appui-abstract/backstage/BackstageItem.ts @@ -34,7 +34,7 @@ export interface CommonBackstageItem extends ProvidedItem { * allows extensions enough gaps to insert their own groups. */ readonly groupPriority: number; - /** Name of icon WebFont entry or if specifying an SVG symbol added by plug on use "svg:" prefix to imported symbol Id. */ + /** Name of icon WebFont entry or if specifying an imported SVG symbol use "webSvg:" prefix to imported symbol Id. */ readonly icon?: string | ConditionalStringValue; /** Required unique id of the item. To ensure uniqueness it is suggested that a namespace prefix of the extension name be used. */ readonly id: string; diff --git a/ui/appui-abstract/src/appui-abstract/items/AbstractItemProps.ts b/ui/appui-abstract/src/appui-abstract/items/AbstractItemProps.ts index 4a1f815395e9..f0378cb595a4 100644 --- a/ui/appui-abstract/src/appui-abstract/items/AbstractItemProps.ts +++ b/ui/appui-abstract/src/appui-abstract/items/AbstractItemProps.ts @@ -25,7 +25,7 @@ export interface CommonItemProps { badgeType?: BadgeType; /** if set, it is used to explicitly set the description shown by components that support a description. */ description?: string | ConditionalStringValue; - /** Name of icon WebFont entry or if specifying an SVG symbol added by plug on use "svg:" prefix to imported symbol Id. */ + /** Name of icon WebFont entry or if specifying an imported SVG symbol use "webSvg:" prefix to imported symbol Id. */ icon?: string | ConditionalStringValue; /** optional data to be used by item implementor. */ readonly internalData?: Map; diff --git a/ui/appui-abstract/src/appui-abstract/items/AbstractMenuItemProps.ts b/ui/appui-abstract/src/appui-abstract/items/AbstractMenuItemProps.ts index de7e1fb2f568..bf74ec56a3aa 100644 --- a/ui/appui-abstract/src/appui-abstract/items/AbstractMenuItemProps.ts +++ b/ui/appui-abstract/src/appui-abstract/items/AbstractMenuItemProps.ts @@ -20,7 +20,7 @@ export interface AbstractMenuItemProps extends CommonItemProps { /** Nested array of item props. Either 'item' or 'submenu' must be specified. */ submenu?: AbstractMenuItemProps[]; /** Icon to display on right side of the menu item. - * Name of icon WebFont entry or if specifying an SVG symbol added by plug on use "svg:" prefix to imported symbol Id. + * Name of icon WebFont entry or if specifying an imported SVG symbol use "webSvg:" prefix to imported symbol Id. */ iconRight?: string | ConditionalStringValue; } diff --git a/ui/appui-abstract/src/appui-abstract/properties/EditorParams.ts b/ui/appui-abstract/src/appui-abstract/properties/EditorParams.ts index 63f68ae77443..05f867e4eb43 100644 --- a/ui/appui-abstract/src/appui-abstract/properties/EditorParams.ts +++ b/ui/appui-abstract/src/appui-abstract/properties/EditorParams.ts @@ -148,7 +148,7 @@ export const isIconListEditorParams = (item: BasePropertyEditorParams): item is * @public */ export interface IconDefinition { - /** Icon specification. The value is the name of an icon WebFont entry, or if specifying an SVG symbol, use `svg:` prefix. */ + /** Icon specification. The value is the name of an icon WebFont entry, or if specifying an imported SVG symbol use "webSvg:" prefix . */ iconSpec: string; /** Function to determine if the item is enabled. */ isEnabledFunction?: () => boolean; diff --git a/ui/appui-abstract/src/appui-abstract/statusbar/StatusBarItem.ts b/ui/appui-abstract/src/appui-abstract/statusbar/StatusBarItem.ts index d7bf6f2b3c66..2104c4604598 100644 --- a/ui/appui-abstract/src/appui-abstract/statusbar/StatusBarItem.ts +++ b/ui/appui-abstract/src/appui-abstract/statusbar/StatusBarItem.ts @@ -74,7 +74,7 @@ export interface AbstractStatusBarItem extends ProvidedItem { export interface AbstractStatusBarActionItem extends AbstractStatusBarItem { /** method to execute when icon is pressed */ readonly execute: () => void; - /** Name of icon WebFont entry or if specifying an SVG symbol added by plug on use "svg:" prefix to imported symbol Id. */ + /** Name of icon WebFont entry or if specifying an imported SVG symbol use "webSvg:" prefix to imported symbol Id. */ readonly icon?: string | ConditionalStringValue; /** Label. */ readonly label?: string | ConditionalStringValue; @@ -86,7 +86,7 @@ export interface AbstractStatusBarActionItem extends AbstractStatusBarItem { * @public */ export interface AbstractStatusBarLabelItem extends AbstractStatusBarItem { - /** Name of icon WebFont entry or if specifying an SVG symbol added by plug on use "svg:" prefix to imported symbol Id. */ + /** Name of icon WebFont entry or if specifying an imported SVG symbol use "webSvg:" prefix to imported symbol Id. */ readonly icon?: string | ConditionalStringValue; /** Label. */ readonly label: string | ConditionalStringValue; diff --git a/ui/appui-abstract/src/appui-abstract/toolbars/ToolbarItem.ts b/ui/appui-abstract/src/appui-abstract/toolbars/ToolbarItem.ts index 455f9cd9a235..7aff74bdfb5a 100644 --- a/ui/appui-abstract/src/appui-abstract/toolbars/ToolbarItem.ts +++ b/ui/appui-abstract/src/appui-abstract/toolbars/ToolbarItem.ts @@ -71,7 +71,7 @@ export interface ToolbarItem extends ProvidedItem { * @public */ export interface ActionButton extends ToolbarItem { - /** Name of icon WebFont entry or if specifying an SVG symbol added by plug on use "svg:" prefix to imported symbol Id. */ + /** Name of icon WebFont entry or if specifying an imported SVG symbol use "webSvg:" prefix to imported symbol Id. */ readonly icon: string | ConditionalStringValue; /** label, shown as tool tip on a button or an item label in a group. */ readonly label: string | ConditionalStringValue; @@ -83,7 +83,7 @@ export interface ActionButton extends ToolbarItem { * @public */ export interface GroupButton extends ToolbarItem { - /** Name of icon WebFont entry or if specifying an SVG symbol added by plug on use "svg:" prefix to imported symbol Id. */ + /** Name of icon WebFont entry or if specifying an imported SVG symbol use "webSvg:" prefix to imported symbol Id. */ readonly icon: string | ConditionalStringValue; /** label, shown as tool tip on group button or a group button label in a group panel. */ readonly label: string | ConditionalStringValue; @@ -97,7 +97,7 @@ export interface GroupButton extends ToolbarItem { * @public */ export interface CustomButtonDefinition extends ToolbarItem { - /** Name of icon WebFont entry or if specifying an SVG symbol added by plug on use "svg:" prefix to imported symbol Id. */ + /** Name of icon WebFont entry or if specifying an imported SVG symbol use "webSvg:" prefix to imported symbol Id. */ readonly icon?: string | ConditionalStringValue; /** label, shown as tool tip on group button or a group button label in a group panel. */ readonly label?: string | ConditionalStringValue; diff --git a/ui/appui-abstract/src/appui-abstract/utils/IconSpecUtilities.ts b/ui/appui-abstract/src/appui-abstract/utils/IconSpecUtilities.ts index f644eb0c7f5f..9dd142b3c882 100644 --- a/ui/appui-abstract/src/appui-abstract/utils/IconSpecUtilities.ts +++ b/ui/appui-abstract/src/appui-abstract/utils/IconSpecUtilities.ts @@ -10,15 +10,28 @@ * @public */ export class IconSpecUtilities { - /** Prefix for an SVG IconSpec */ + /** Prefix for an SVG IconSpec loaded with the Sprite loader */ public static readonly SVG_PREFIX = "svg:"; + public static readonly WEB_COMPONENT_PREFIX = "webSvg:"; - /** Create an IconSpec for an SVG */ + /** Create an IconSpec for an SVG loaded into web component with sprite loader + * This method is deprecated -- use createWebComponentIconSpec() + * @public @deprecated + */ public static createSvgIconSpec(svgSrc: string): string { return `${IconSpecUtilities.SVG_PREFIX}${svgSrc}`; } + /** Create an IconSpec for an SVG loaded into web component with svg-loader + * @public + */ + public static createWebComponentIconSpec(srcString: string): string { + return `${IconSpecUtilities.WEB_COMPONENT_PREFIX}${srcString}`; + } - /** Get the SVG Source from an IconSpec */ + /** Get the SVG Source from an sprite IconSpec + * This method is deprecated -- use getWebComponentSource() + * @public @deprecated + */ public static getSvgSource(iconSpec: string): string | undefined { if (iconSpec.startsWith(IconSpecUtilities.SVG_PREFIX) && iconSpec.length > 4) { return iconSpec.slice(4); @@ -26,4 +39,16 @@ export class IconSpecUtilities { return undefined; } + /** Get the SVG Source from an svg-loader IconSpec + * @public + */ + + public static getWebComponentSource(iconSpec: string): string | undefined { + if (iconSpec.startsWith(IconSpecUtilities.WEB_COMPONENT_PREFIX) && iconSpec.length > 7) { + return iconSpec.slice(7); + } + + return undefined; + + } } diff --git a/ui/appui-abstract/src/appui-abstract/widget/AbstractWidgetProps.ts b/ui/appui-abstract/src/appui-abstract/widget/AbstractWidgetProps.ts index f7132701f401..38f82a529951 100644 --- a/ui/appui-abstract/src/appui-abstract/widget/AbstractWidgetProps.ts +++ b/ui/appui-abstract/src/appui-abstract/widget/AbstractWidgetProps.ts @@ -17,7 +17,7 @@ import { WidgetState } from "./WidgetState"; export interface AbstractWidgetProps extends ProvidedItem { /** Gets the widget content */ readonly getWidgetContent: () => any; - /** Name of icon WebFont entry or if specifying an SVG symbol added by plug on use "svg:" prefix to imported symbol Id. */ + /** Name of icon WebFont entry or if specifying an imported SVG symbol use "webSvg:" prefix to imported symbol Id. */ readonly icon?: string | ConditionalStringValue; /** Id used to uniquely identify the widget. * @note It is recommended to provide unique widget id to correctly save/restore App layout. diff --git a/ui/appui-abstract/src/test/utils/IconSpecUtilities.test.ts b/ui/appui-abstract/src/test/utils/IconSpecUtilities.test.ts index 9fd69dd3bb9a..2798095fe338 100644 --- a/ui/appui-abstract/src/test/utils/IconSpecUtilities.test.ts +++ b/ui/appui-abstract/src/test/utils/IconSpecUtilities.test.ts @@ -2,6 +2,7 @@ * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ +/* eslint-disable deprecation/deprecation */ import { expect } from "chai"; import { IconSpecUtilities } from "../../appui-abstract/utils/IconSpecUtilities"; @@ -22,4 +23,18 @@ describe("IconSpecUtilities", () => { expect(svgSource).to.be.undefined; }); + it("should correctly create iconSpec for WebSvg", () => { + const iconSpec = IconSpecUtilities.createWebComponentIconSpec("test"); + expect(iconSpec).to.eq(`${IconSpecUtilities.WEB_COMPONENT_PREFIX}test`); + }); + + it("should correctly return WebSvg source from iconSpec", () => { + const webSvgSource = IconSpecUtilities.getWebComponentSource(`${IconSpecUtilities.WEB_COMPONENT_PREFIX}test`); + expect(webSvgSource).to.eq("test"); + }); + + it("should return undefined if given invalid iconSpec", () => { + const webSvgSource = IconSpecUtilities.getWebComponentSource(""); + expect(webSvgSource).to.be.undefined; + }); }); diff --git a/ui/appui-layout-react/src/appui-layout-react/widget/PopoutToggle.tsx b/ui/appui-layout-react/src/appui-layout-react/widget/PopoutToggle.tsx index 27ff21849e20..6e5f2a74851e 100644 --- a/ui/appui-layout-react/src/appui-layout-react/widget/PopoutToggle.tsx +++ b/ui/appui-layout-react/src/appui-layout-react/widget/PopoutToggle.tsx @@ -11,7 +11,7 @@ import "./PopoutToggle.scss"; import * as React from "react"; import { NineZoneDispatchContext, useLabel } from "../base/NineZone"; -import popoutToggleSvg from "./window-popout.svg?sprite"; +import popoutToggleSvg from "./window-popout.svg"; import { Icon } from "@itwin/core-react"; import { IconSpecUtilities } from "@itwin/appui-abstract"; import { ActiveTabIdContext } from "./Widget"; @@ -20,7 +20,7 @@ import { ActiveTabIdContext } from "./Widget"; export const PopoutToggle = React.memo(function PopoutToggle() { // eslint-disable-line @typescript-eslint/naming-convention, no-shadow const dispatch = React.useContext(NineZoneDispatchContext); const activeTabId = React.useContext(ActiveTabIdContext); - const iconSpec = IconSpecUtilities.createSvgIconSpec(popoutToggleSvg); + const iconSpec = IconSpecUtilities.createWebComponentIconSpec(popoutToggleSvg); const popoutTitle = useLabel("popoutActiveTab"); return (
`; diff --git a/ui/appui-react/src/appui-react/accudraw/AccuDrawFieldContainer.tsx b/ui/appui-react/src/appui-react/accudraw/AccuDrawFieldContainer.tsx index 1b06b4454a92..864ab037930b 100644 --- a/ui/appui-react/src/appui-react/accudraw/AccuDrawFieldContainer.tsx +++ b/ui/appui-react/src/appui-react/accudraw/AccuDrawFieldContainer.tsx @@ -20,8 +20,8 @@ import { KeyboardShortcutManager } from "../keyboardshortcut/KeyboardShortcut"; import { AccuDrawSetCompassModeEventArgs, AccuDrawSetFieldFocusEventArgs, AccuDrawSetFieldLockEventArgs, FrameworkAccuDraw } from "./FrameworkAccuDraw"; import { AccuDrawUiSettings } from "./AccuDrawUiSettings"; -import angleIconSvg from "./angle.svg?sprite"; -import distanceIconSvg from "./distance.svg?sprite"; +import angleIconSvg from "./angle.svg"; +import distanceIconSvg from "./distance.svg"; /** Properties for [[AccuDrawFieldContainer]] component * @beta */ @@ -44,8 +44,8 @@ function determineShowZ(vp?: ScreenViewport): boolean { const defaultXLabel = "X"; const defaultYLabel = "Y"; const defaultZLabel = "Z"; -const defaultAngleIcon = IconSpecUtilities.createSvgIconSpec(angleIconSvg); -const defaultDistanceIcon = IconSpecUtilities.createSvgIconSpec(distanceIconSvg); +const defaultAngleIcon = IconSpecUtilities.createWebComponentIconSpec(angleIconSvg); +const defaultDistanceIcon = IconSpecUtilities.createWebComponentIconSpec(distanceIconSvg); /** AccuDraw Ui Field Container displays [[AccuDrawInputField]] for each field * @beta */ diff --git a/ui/appui-react/src/appui-react/accudraw/AccuDrawPopupManager.tsx b/ui/appui-react/src/appui-react/accudraw/AccuDrawPopupManager.tsx index e8807c5248f2..5fa07388e961 100644 --- a/ui/appui-react/src/appui-react/accudraw/AccuDrawPopupManager.tsx +++ b/ui/appui-react/src/appui-react/accudraw/AccuDrawPopupManager.tsx @@ -16,9 +16,9 @@ import { MenuItemHelpers } from "../shared/MenuItem"; import { CalculatorPopup } from "./CalculatorPopup"; import { MenuButtonPopup } from "./MenuButtonPopup"; -import angleIcon from "./angle.svg?sprite"; -import lengthIcon from "./distance.svg?sprite"; -import heightIcon from "./height-2.svg?sprite"; +import angleIcon from "./angle.svg"; +import lengthIcon from "./distance.svg"; +import heightIcon from "./height-2.svg"; /** AccuDraw Popup Manager class * @alpha @@ -67,17 +67,17 @@ export class AccuDrawPopupManager { } public static showAngleEditor(el: HTMLElement, pt: XAndY, value: number, onCommit: OnNumberCommitFunc, onCancel: OnCancelFunc): boolean { - const propertyDescription = new AngleDescription(undefined, undefined, IconSpecUtilities.createSvgIconSpec(angleIcon)); + const propertyDescription = new AngleDescription(undefined, undefined, IconSpecUtilities.createWebComponentIconSpec(angleIcon)); return PopupManager.showInputEditor(el, pt, value, propertyDescription, onCommit as OnValueCommitFunc, onCancel); } public static showLengthEditor(el: HTMLElement, pt: XAndY, value: number, onCommit: OnNumberCommitFunc, onCancel: OnCancelFunc): boolean { - const propertyDescription = new LengthDescription(undefined, undefined, IconSpecUtilities.createSvgIconSpec(lengthIcon)); + const propertyDescription = new LengthDescription(undefined, undefined, IconSpecUtilities.createWebComponentIconSpec(lengthIcon)); return PopupManager.showInputEditor(el, pt, value, propertyDescription, onCommit as OnValueCommitFunc, onCancel); } public static showHeightEditor(el: HTMLElement, pt: XAndY, value: number, onCommit: OnNumberCommitFunc, onCancel: OnCancelFunc): boolean { - const propertyDescription = new LengthDescription(undefined, undefined, IconSpecUtilities.createSvgIconSpec(heightIcon)); + const propertyDescription = new LengthDescription(undefined, undefined, IconSpecUtilities.createWebComponentIconSpec(heightIcon)); return PopupManager.showInputEditor(el, pt, value, propertyDescription, onCommit as OnValueCommitFunc, onCancel); } } diff --git a/ui/appui-react/src/appui-react/accudraw/Calculator.tsx b/ui/appui-react/src/appui-react/accudraw/Calculator.tsx index 6b278590bd19..4015aca39851 100644 --- a/ui/appui-react/src/appui-react/accudraw/Calculator.tsx +++ b/ui/appui-react/src/appui-react/accudraw/Calculator.tsx @@ -9,13 +9,13 @@ import "./Calculator.scss"; import classnames from "classnames"; import * as React from "react"; -import { OnCancelFunc, OnNumberCommitFunc, SpecialKey } from "@itwin/appui-abstract"; -import { CommonProps, Icon, IconInput, Omit, SvgSprite } from "@itwin/core-react"; +import { IconSpecUtilities, OnCancelFunc, OnNumberCommitFunc, SpecialKey } from "@itwin/appui-abstract"; +import { CommonProps, Icon, IconInput, Omit } from "@itwin/core-react"; import { Button, Input } from "@itwin/itwinui-react"; import { CalculatorEngine, CalculatorOperator } from "./CalculatorEngine"; import { SquareButton, SquareButtonProps } from "./SquareButton"; -import backspaceIcon from "./backspace.svg?sprite"; +import backspaceIcon from "./backspace.svg"; // cSpell:ignore plusmn @@ -232,6 +232,7 @@ interface CalculatorKeyPadProps extends CommonProps { class CalculatorKeyPad extends React.PureComponent { public override render() { + const iconSpec = IconSpecUtilities.createWebComponentIconSpec(backspaceIcon); return (
@@ -239,7 +240,7 @@ class CalculatorKeyPad extends React.PureComponent { C
- +
FrontstageManager.openModalFrontstage(new SettingsModalFrontstage()), UiFramework.translate("settings.settingsStageLabel"), - undefined, IconSpecUtilities.createSvgIconSpec(settingsIconSvg), { isHidden: SettingsModalFrontstage.noSettingsAvailable() }); + undefined, IconSpecUtilities.createWebComponentIconSpec(settingsIconSvg), { isHidden: SettingsModalFrontstage.noSettingsAvailable() }); } public static showSettingsStage(initialSettingsTab?: string) { diff --git a/ui/appui-react/src/appui-react/settings/ui/UiSettingsPage.tsx b/ui/appui-react/src/appui-react/settings/ui/UiSettingsPage.tsx index 66560438732a..10d6f7be51ad 100644 --- a/ui/appui-react/src/appui-react/settings/ui/UiSettingsPage.tsx +++ b/ui/appui-react/src/appui-react/settings/ui/UiSettingsPage.tsx @@ -8,7 +8,7 @@ // cSpell:ignore configurableui checkmark -import widowSettingsIconSvg from "@bentley/icons-generic/icons/window-settings.svg?sprite"; +import widowSettingsIconSvg from "@bentley/icons-generic/icons/window-settings.svg"; import "./UiSettingsPage.scss"; import * as React from "react"; import { SettingsTabEntry } from "@itwin/core-react"; @@ -213,7 +213,7 @@ export function getUiSettingsManagerEntry(itemPriority: number, allowSettingUiFr return { itemPriority, tabId: "uifw:UiStateStorage", label: UiFramework.translate("settings.uiSettingsPage.label"), - icon: IconSpecUtilities.createSvgIconSpec(widowSettingsIconSvg), + icon: IconSpecUtilities.createWebComponentIconSpec(widowSettingsIconSvg), page: , isDisabled: false, tooltip: UiFramework.translate("settings.uiSettingsPage.tooltip"), diff --git a/ui/appui-react/src/appui-react/statusfields/toolassistance/ToolAssistanceField.tsx b/ui/appui-react/src/appui-react/statusfields/toolassistance/ToolAssistanceField.tsx index 3fecad42484e..98654370433d 100644 --- a/ui/appui-react/src/appui-react/statusfields/toolassistance/ToolAssistanceField.tsx +++ b/ui/appui-react/src/appui-react/statusfields/toolassistance/ToolAssistanceField.tsx @@ -2,6 +2,7 @@ * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ +/* eslint-disable deprecation/deprecation */ /** @packageDocumentation * @module Notification */ @@ -16,7 +17,7 @@ import { } from "@itwin/core-frontend"; import { IconSpecUtilities } from "@itwin/appui-abstract"; import { - FillCentered, Icon, LocalStateStorage, SvgSprite, UiCore, UiStateEntry, UiStateStorage, UiStateStorageResult, UiStateStorageStatus, + FillCentered, Icon, LocalStateStorage, UiCore, UiStateEntry, UiStateStorage, UiStateStorageResult, UiStateStorageStatus, } from "@itwin/core-react"; import { FooterPopup, ToolAssistanceInstruction as NZ_ToolAssistanceInstruction, TitleBarButton, ToolAssistance, ToolAssistanceDialog, @@ -32,22 +33,22 @@ import { UiFramework } from "../../UiFramework"; import { StatusFieldProps } from "../StatusFieldProps"; import { UiStateStorageContext } from "../../uistate/useUiStateStorage"; -import acceptPointIcon from "./accept-point.svg?sprite"; -import cursorClickIcon from "./cursor-click.svg?sprite"; -import oneTouchDragIcon from "./gesture-one-finger-drag.svg?sprite"; -import oneTouchDoubleTapIcon from "./gesture-one-finger-tap-double.svg?sprite"; -import oneTouchTapIcon from "./gesture-one-finger-tap.svg?sprite"; -import twoTouchPinchIcon from "./gesture-pinch.svg?sprite"; -import twoTouchDragIcon from "./gesture-two-finger-drag.svg?sprite"; -import twoTouchTapIcon from "./gesture-two-finger-tap.svg?sprite"; -import clickLeftDragIcon from "./mouse-click-left-drag.svg?sprite"; -import clickLeftIcon from "./mouse-click-left.svg?sprite"; -import clickRightDragIcon from "./mouse-click-right-drag.svg?sprite"; -import clickRightIcon from "./mouse-click-right.svg?sprite"; -import clickMouseWheelDragIcon from "./mouse-click-wheel-drag.svg?sprite"; -import mouseWheelClickIcon from "./mouse-click-wheel.svg?sprite"; -import touchCursorDragIcon from "./touch-cursor-pan.svg?sprite"; -import touchCursorTapIcon from "./touch-cursor-point.svg?sprite"; +import acceptPointIcon from "./accept-point.svg"; +import cursorClickIcon from "./cursor-click.svg"; +import oneTouchDragIcon from "./gesture-one-finger-drag.svg"; +import oneTouchDoubleTapIcon from "./gesture-one-finger-tap-double.svg"; +import oneTouchTapIcon from "./gesture-one-finger-tap.svg"; +import twoTouchPinchIcon from "./gesture-pinch.svg"; +import twoTouchDragIcon from "./gesture-two-finger-drag.svg"; +import twoTouchTapIcon from "./gesture-two-finger-tap.svg"; +import clickLeftDragIcon from "./mouse-click-left-drag.svg"; +import clickLeftIcon from "./mouse-click-left.svg"; +import clickRightDragIcon from "./mouse-click-right-drag.svg"; +import clickRightIcon from "./mouse-click-right.svg"; +import clickMouseWheelDragIcon from "./mouse-click-wheel-drag.svg"; +import mouseWheelClickIcon from "./mouse-click-wheel.svg"; +import touchCursorDragIcon from "./touch-cursor-pan.svg"; +import touchCursorTapIcon from "./touch-cursor-point.svg"; // cSpell:ignore cursorprompt @@ -589,12 +590,12 @@ export class ToolAssistanceField extends React.Component {svgImage && // istanbul ignore next - + }
); diff --git a/ui/appui-react/src/appui-react/widgets/BackstageAppButton.tsx b/ui/appui-react/src/appui-react/widgets/BackstageAppButton.tsx index d86b3ded25a5..137434c294a6 100644 --- a/ui/appui-react/src/appui-react/widgets/BackstageAppButton.tsx +++ b/ui/appui-react/src/appui-react/widgets/BackstageAppButton.tsx @@ -7,7 +7,7 @@ */ import * as React from "react"; -import widgetIconSvg from "@bentley/icons-generic/icons/home.svg?sprite"; +import widgetIconSvg from "@bentley/icons-generic/icons/home.svg"; import { IconSpecUtilities } from "@itwin/appui-abstract"; import { Icon, useWidgetOpacityContext } from "@itwin/core-react"; import { AppButton } from "@itwin/appui-layout-react"; @@ -36,7 +36,7 @@ export interface BackstageAppButtonProps { export function BackstageAppButton(props: BackstageAppButtonProps) { const backstageToggleCommand = React.useMemo(() => BackstageManager.getBackstageToggleCommand(props.icon), [props.icon]); const backstageLabel = React.useMemo(() => props.label || backstageToggleCommand.tooltip, [backstageToggleCommand.tooltip, props.label]); - const [icon, setIcon] = React.useState(props.icon ? props.icon : IconSpecUtilities.createSvgIconSpec(widgetIconSvg)); + const [icon, setIcon] = React.useState(props.icon ? props.icon : IconSpecUtilities.createWebComponentIconSpec(widgetIconSvg)); const isInitialMount = React.useRef(true); const useSmallAppButton = "1" !== useFrameworkVersion(); const divClassName = useSmallAppButton ? "uifw-app-button-small" : undefined; @@ -56,7 +56,7 @@ export function BackstageAppButton(props: BackstageAppButtonProps) { isInitialMount.current = false; onElementRef(ref); } else { - setIcon(props.icon ? props.icon : IconSpecUtilities.createSvgIconSpec(widgetIconSvg)); + setIcon(props.icon ? props.icon : IconSpecUtilities.createWebComponentIconSpec(widgetIconSvg)); } }, [props.icon, onElementRef]); diff --git a/ui/appui-react/src/appui-react/widgets/BasicToolWidget.tsx b/ui/appui-react/src/appui-react/widgets/BasicToolWidget.tsx index abdb306f28b4..2d6251f1cc4e 100644 --- a/ui/appui-react/src/appui-react/widgets/BasicToolWidget.tsx +++ b/ui/appui-react/src/appui-react/widgets/BasicToolWidget.tsx @@ -23,7 +23,7 @@ import { useUiVisibility } from "../hooks/useUiVisibility"; export interface BasicToolWidgetProps { /** if true include hide/isolate Models and Categories */ showCategoryAndModelsContextTools?: boolean; - /** Name of icon WebFont entry or if specifying an SVG symbol added by plug on use "svg:" prefix to imported symbol Id. */ + /** Name of icon WebFont entry or if specifying an imported SVG symbol use "webSvg:" prefix to imported symbol Id. */ icon?: string; /** optional set of additional items to include in horizontal toolbar */ additionalHorizontalItems?: CommonToolbarItem[]; diff --git a/ui/appui-react/src/declarations.d.ts b/ui/appui-react/src/declarations.d.ts index 2c61a7699561..be2122294d64 100644 --- a/ui/appui-react/src/declarations.d.ts +++ b/ui/appui-react/src/declarations.d.ts @@ -6,3 +6,7 @@ declare module "*.svg?sprite" { const src: string; export default src; } +declare module "*.svg" { + const src: string; + export default src; +} diff --git a/ui/components-react/src/components-react/common/ImageRenderer.tsx b/ui/components-react/src/components-react/common/ImageRenderer.tsx index c00590bca73a..32fd492eab82 100644 --- a/ui/components-react/src/components-react/common/ImageRenderer.tsx +++ b/ui/components-react/src/components-react/common/ImageRenderer.tsx @@ -7,8 +7,8 @@ */ import * as React from "react"; -import { UiError } from "@itwin/appui-abstract"; -import { SvgSprite, WebFontIcon, WebFontIconProps } from "@itwin/core-react"; +import { IconSpecUtilities, UiError } from "@itwin/appui-abstract"; +import { Icon, WebFontIcon, WebFontIconProps } from "@itwin/core-react"; import { UiComponents } from "../UiComponents"; import { Image, ImageFileFormat, LoadedBinaryImage, LoadedImage } from "./IImageLoader"; @@ -36,8 +36,9 @@ export class ImageRenderer { /** Render svg string into JSX */ private renderSvg(svg: string) { + const iconSpec = IconSpecUtilities.createWebComponentIconSpec(svg); return ( -
+
); } diff --git a/ui/components-react/src/test/common/ImageRenderer.test.snap b/ui/components-react/src/test/common/ImageRenderer.test.snap index a8f4d8164bc2..b0bc54401e77 100644 --- a/ui/components-react/src/test/common/ImageRenderer.test.snap +++ b/ui/components-react/src/test/common/ImageRenderer.test.snap @@ -1,12 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ImageRenderer render renders svg 1`] = ` -"
- - - - - - \\">
" +"
+ + id=\\"svg-id\\">
" `; diff --git a/ui/core-react/src/core-react/UiCore.ts b/ui/core-react/src/core-react/UiCore.ts index efeef258e1b3..a2d5d15ea1ad 100644 --- a/ui/core-react/src/core-react/UiCore.ts +++ b/ui/core-react/src/core-react/UiCore.ts @@ -15,7 +15,7 @@ import "./classes.scss"; import { Logger } from "@itwin/core-bentley"; import type { Localization } from "@itwin/core-common"; import { getClassName, UiError } from "@itwin/appui-abstract"; - +import { IconWebComponent } from "./utils/IconWebComponent"; // cSpell:ignore colorthemes colorvariables /** @@ -38,6 +38,9 @@ export class UiCore { UiCore._localization = localization; await UiCore._localization.registerNamespace(UiCore.localizationNamespace); + if (window.customElements.get("svg-loader") === undefined) + window.customElements.define("svg-loader", IconWebComponent); + UiCore._initialized = true; } diff --git a/ui/core-react/src/core-react/badge/Badge.tsx b/ui/core-react/src/core-react/badge/Badge.tsx index 050bb9483b5e..bd603d2045b9 100644 --- a/ui/core-react/src/core-react/badge/Badge.tsx +++ b/ui/core-react/src/core-react/badge/Badge.tsx @@ -9,8 +9,9 @@ import "./Badge.scss"; import classnames from "classnames"; import * as React from "react"; -import { SvgSprite } from "../icons/SvgSprite"; import { CommonProps } from "../utils/Props"; +import { IconSpecUtilities } from "@itwin/appui-abstract"; +import { Icon } from "../icons/IconComponent"; /** Properties for the [[Badge]] React component * @internal @@ -24,9 +25,10 @@ export interface BadgeProps extends CommonProps { */ export class Badge extends React.PureComponent { public override render(): JSX.Element { + const iconSpec = IconSpecUtilities.createWebComponentIconSpec(this.props.svg); return (
- +
); } diff --git a/ui/core-react/src/core-react/badge/BetaBadge.tsx b/ui/core-react/src/core-react/badge/BetaBadge.tsx index 58e8a49dcab1..895aa7f7b65e 100644 --- a/ui/core-react/src/core-react/badge/BetaBadge.tsx +++ b/ui/core-react/src/core-react/badge/BetaBadge.tsx @@ -11,7 +11,7 @@ import classnames from "classnames"; import * as React from "react"; import { CommonProps } from "../utils/Props"; import { Badge } from "./Badge"; -import betaBadgeIcon from "./technical-preview-badge.svg?sprite"; +import betaBadgeIcon from "./technical-preview-badge.svg"; /** Beta Badge React component * @internal diff --git a/ui/core-react/src/core-react/badge/NewBadge.tsx b/ui/core-react/src/core-react/badge/NewBadge.tsx index 5dbb8eaf552c..789f01c8fc70 100644 --- a/ui/core-react/src/core-react/badge/NewBadge.tsx +++ b/ui/core-react/src/core-react/badge/NewBadge.tsx @@ -11,7 +11,7 @@ import classnames from "classnames"; import * as React from "react"; import { CommonProps } from "../utils/Props"; import { Badge } from "./Badge"; -import newBadgeIcon from "./new-feature-badge.svg?sprite"; +import newBadgeIcon from "./new-feature-badge.svg"; /** New Badge React component * @internal diff --git a/ui/core-react/src/core-react/icons/IconComponent.tsx b/ui/core-react/src/core-react/icons/IconComponent.tsx index 4a33ac0a0a7c..504dd905c95f 100644 --- a/ui/core-react/src/core-react/icons/IconComponent.tsx +++ b/ui/core-react/src/core-react/icons/IconComponent.tsx @@ -2,6 +2,7 @@ * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ +/* eslint-disable deprecation/deprecation */ /** @packageDocumentation * @module Icon */ @@ -12,6 +13,7 @@ import classnames from "classnames"; import { ConditionalStringValue, IconSpecUtilities } from "@itwin/appui-abstract"; import { SvgSprite } from "./SvgSprite"; import { CommonProps } from "../utils/Props"; +import { sanitize as sanitizer } from "dompurify"; /** Prototype for an IconSpec which can be a string, ReactNode or ConditionalStringValue. * @public @@ -47,7 +49,20 @@ export function Icon(props: IconProps) { ); - + const webComponentString = IconSpecUtilities.getWebComponentSource(iconString); + if (webComponentString){ + const svgLoader = ``; + const svgDiv = `
${svgLoader}
`; + const sanitizedIconString = sanitizer(svgDiv, {ALLOWED_TAGS: ["svg-loader"]}); + // we can safely disable jam3/no-sanitizer-with-danger as we are sanitizing above + // eslint-disable-next-line @typescript-eslint/naming-convention, jam3/no-sanitizer-with-danger + const webComponentNode =
; + return ( + + {webComponentNode} + + ); + } return (); } diff --git a/ui/core-react/src/core-react/icons/SvgSprite.tsx b/ui/core-react/src/core-react/icons/SvgSprite.tsx index b34e89125ad1..9b116ade8c58 100644 --- a/ui/core-react/src/core-react/icons/SvgSprite.tsx +++ b/ui/core-react/src/core-react/icons/SvgSprite.tsx @@ -2,6 +2,7 @@ * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ +/* eslint-disable deprecation/deprecation */ /** @packageDocumentation * @module Icon */ @@ -12,7 +13,7 @@ import * as React from "react"; import { CommonProps } from "../utils/Props"; /** Properties of [[SvgSprite]] component. - * @public + * @public @deprecated */ export interface SvgSpriteProps extends CommonProps { /** Source for the Svg */ @@ -20,7 +21,8 @@ export interface SvgSpriteProps extends CommonProps { } /** Svg element wrapper. - * @public + * This component is deprecate -- use IconComponent + * @public @deprecated */ export class SvgSprite extends React.PureComponent { public override render() { diff --git a/ui/core-react/src/core-react/utils/IconHelper.tsx b/ui/core-react/src/core-react/utils/IconHelper.tsx index 4ba9d11aa634..094c21982595 100644 --- a/ui/core-react/src/core-react/utils/IconHelper.tsx +++ b/ui/core-react/src/core-react/utils/IconHelper.tsx @@ -17,7 +17,6 @@ export class IconHelper { public static get reactIconKey(): string { return "#-react-iconspec-node-#"; } - /** Returns an ReactNode from the many ways an icon can be specified. * @param icon abstract icon specification. * @param internalData a map that may hold a React.ReactNode stored in an abstract item definition. @@ -46,7 +45,6 @@ export class IconHelper { return ; return null; } - return ; } diff --git a/ui/core-react/src/core-react/utils/IconWebComponent.ts b/ui/core-react/src/core-react/utils/IconWebComponent.ts new file mode 100644 index 000000000000..cc174ff886d3 --- /dev/null +++ b/ui/core-react/src/core-react/utils/IconWebComponent.ts @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Bentley Systems, Incorporated. All rights reserved. +* See LICENSE.md in the project root for license terms and full copyright notice. +*--------------------------------------------------------------------------------------------*/ +// istanbul ignore file +// IconWebComponent requires in-browser testing +/** @packageDocumentation + * @module Utilities + */ + +import { UiError } from "@itwin/appui-abstract"; +import { Logger } from "@itwin/core-bentley"; +import { UiCore } from "../UiCore"; + +/** + * IconWebComponent loads icon from an svg path + */ +export class IconWebComponent extends HTMLElement { + private async connectedCallback() { + await this.loadSvg(); + this.dispatchEvent(new CustomEvent("load")); + } + + private async loadSvg() { + await fetch(this.getAttribute("src") || "") + .catch((_error) => { + Logger.logError(UiCore.loggerCategory(this), "Unable to load icon."); + }) + .then(async (response) => { + if (response && response.ok) { + return response.text(); + } else { + throw new UiError (UiCore.loggerCategory(this), "Unable to load icon."); + } + }) + .then((str) => { + if (str !== undefined) { + return (new window.DOMParser()).parseFromString(str, "text/xml"); + } else { + throw new UiError (UiCore.loggerCategory(this), "Unable to load icon."); + } + }) + .then((data) => this.append(data.documentElement)); + } +} diff --git a/ui/core-react/src/declarations.d.ts b/ui/core-react/src/declarations.d.ts index 2c61a7699561..be2122294d64 100644 --- a/ui/core-react/src/declarations.d.ts +++ b/ui/core-react/src/declarations.d.ts @@ -6,3 +6,7 @@ declare module "*.svg?sprite" { const src: string; export default src; } +declare module "*.svg" { + const src: string; + export default src; +} diff --git a/ui/core-react/src/test/icons/IconComponent.test.snap b/ui/core-react/src/test/icons/IconComponent.test.snap deleted file mode 100644 index 32620d19b338..000000000000 --- a/ui/core-react/src/test/icons/IconComponent.test.snap +++ /dev/null @@ -1,29 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`IconComponent should render correctly with ReactNode 1`] = ` - - - Test - - -`; - -exports[`IconComponent should render correctly with icon class string 1`] = ` - -`; - -exports[`IconComponent should render correctly with icon svg string 1`] = ` - - - -`; - -exports[`IconComponent should render correctly with no iconSpec 1`] = `""`; diff --git a/ui/core-react/src/test/icons/IconComponent.test.tsx b/ui/core-react/src/test/icons/IconComponent.test.tsx index 813daa68a8cf..847fedc70007 100644 --- a/ui/core-react/src/test/icons/IconComponent.test.tsx +++ b/ui/core-react/src/test/icons/IconComponent.test.tsx @@ -2,30 +2,39 @@ * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ -import { mount, shallow } from "enzyme"; +import { expect } from "chai"; import * as React from "react"; import { Icon } from "../../core-react/icons/IconComponent"; +import { render } from "@testing-library/react"; describe("IconComponent", () => { - it("should render with ReactNode", () => { - mount(Test} />); + it("Should return null from undefined iconSpec", () => { + const { container } = render(); + expect(container.firstChild).to.be.null; }); - - it("should render correctly with ReactNode", () => { - shallow(Test} />).should.matchSnapshot(); + it("should render with ReactNode", () => { + const { container } = render(Test} />); + const span = container.querySelector("span"); + expect(span).not.to.be.null; }); it("should render correctly with icon svg string", () => { - shallow().should.matchSnapshot(); + const { container } = render(); + const svgIconClassName = container.querySelector(".core-icons-svgSprite"); + expect(svgIconClassName).not.to.be.null; }); it("should render correctly with icon class string", () => { - shallow().should.matchSnapshot(); + const { container } = render(); + const iconClassName = container.querySelector(".icon-developer"); + expect(iconClassName).not.to.be.null; }); - it("should render correctly with no iconSpec", () => { - shallow().should.matchSnapshot(); + it("should render correctly with no web svg iconSpec", () => { + const { container } = render(); + const webComponent = container.querySelector("svg-loader"); + expect(webComponent).not.to.be.null; }); }); diff --git a/ui/core-react/src/test/icons/SvgSprite.test.tsx b/ui/core-react/src/test/icons/SvgSprite.test.tsx index 090c098bc1a3..da5cabb13632 100644 --- a/ui/core-react/src/test/icons/SvgSprite.test.tsx +++ b/ui/core-react/src/test/icons/SvgSprite.test.tsx @@ -2,6 +2,7 @@ * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ +/* eslint-disable deprecation/deprecation */ import { mount, shallow } from "enzyme"; import * as React from "react"; import { SvgSprite } from "../../core-react";