From 8caea79b111d806f62b7d0e2e0c288739247b77e Mon Sep 17 00:00:00 2001 From: Tomi Olubeko Date: Tue, 5 Dec 2023 14:42:17 -0800 Subject: [PATCH 1/9] v8 registerIcons compat --- .github/CODEOWNERS | 1 + .../react-icons-compat/.babelrc.json | 4 + .../react-icons-compat/.eslintrc.json | 4 + .../react-icons-compat/.storybook/main.js | 14 ++ .../react-icons-compat/.storybook/preview.js | 7 + .../.storybook/tsconfig.json | 10 + .../react-icons-compat/.swcrc | 30 +++ .../react-icons-compat/LICENSE | 15 ++ .../react-icons-compat/README.md | 5 + .../config/api-extractor.json | 4 + .../react-icons-compat/config/tests.js | 1 + .../react-icons-compat/docs/Spec.md | 63 +++++ .../etc/react-icons-compat.api.md | 0 .../react-icons-compat/jest.config.js | 21 ++ .../react-icons-compat/just.config.ts | 5 + .../react-icons-compat/package.json | 68 ++++++ .../react-icons-compat/project.json | 8 + .../react-icons-compat/src/GlobalSettings.ts | 122 ++++++++++ .../react-icons-compat/src/getWindow.ts | 30 +++ .../react-icons-compat/src/icon.ts | 219 ++++++++++++++++++ .../react-icons-compat/src/index.ts | 7 + .../src/testing/isConformant.ts | 15 ++ .../react-icons-compat/src/warn.ts | 30 +++ .../react-icons-compat/tsconfig.json | 25 ++ .../react-icons-compat/tsconfig.lib.json | 22 ++ .../react-icons-compat/tsconfig.spec.json | 17 ++ tsconfig.base.all.json | 9 +- tsconfig.base.json | 1 + 28 files changed, 753 insertions(+), 4 deletions(-) create mode 100644 packages/react-components/react-icons-compat/.babelrc.json create mode 100644 packages/react-components/react-icons-compat/.eslintrc.json create mode 100644 packages/react-components/react-icons-compat/.storybook/main.js create mode 100644 packages/react-components/react-icons-compat/.storybook/preview.js create mode 100644 packages/react-components/react-icons-compat/.storybook/tsconfig.json create mode 100644 packages/react-components/react-icons-compat/.swcrc create mode 100644 packages/react-components/react-icons-compat/LICENSE create mode 100644 packages/react-components/react-icons-compat/README.md create mode 100644 packages/react-components/react-icons-compat/config/api-extractor.json create mode 100644 packages/react-components/react-icons-compat/config/tests.js create mode 100644 packages/react-components/react-icons-compat/docs/Spec.md create mode 100644 packages/react-components/react-icons-compat/etc/react-icons-compat.api.md create mode 100644 packages/react-components/react-icons-compat/jest.config.js create mode 100644 packages/react-components/react-icons-compat/just.config.ts create mode 100644 packages/react-components/react-icons-compat/package.json create mode 100644 packages/react-components/react-icons-compat/project.json create mode 100644 packages/react-components/react-icons-compat/src/GlobalSettings.ts create mode 100644 packages/react-components/react-icons-compat/src/getWindow.ts create mode 100644 packages/react-components/react-icons-compat/src/icon.ts create mode 100644 packages/react-components/react-icons-compat/src/index.ts create mode 100644 packages/react-components/react-icons-compat/src/testing/isConformant.ts create mode 100644 packages/react-components/react-icons-compat/src/warn.ts create mode 100644 packages/react-components/react-icons-compat/tsconfig.json create mode 100644 packages/react-components/react-icons-compat/tsconfig.lib.json create mode 100644 packages/react-components/react-icons-compat/tsconfig.spec.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 849628174c5d3..f998ba38ec5cd 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -253,6 +253,7 @@ packages/react-components/react-infolabel @microsoft/cxe-red @sopranopillow packages/react-components/react-list-preview @microsoft/teams-prg packages/react-components/react-motions-preview @microsoft/teams-prg packages/react-components/react-teaching-popover-preview @microsoft/xc-uxe @Mitch-At-Work +packages/react-components/react-icons-compat @microsoft/cxe-red # <%= NX-CODEOWNER-PLACEHOLDER %> ## Components diff --git a/packages/react-components/react-icons-compat/.babelrc.json b/packages/react-components/react-icons-compat/.babelrc.json new file mode 100644 index 0000000000000..45fb71ca16d2c --- /dev/null +++ b/packages/react-components/react-icons-compat/.babelrc.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../.babelrc-v9.json", + "plugins": ["annotate-pure-calls", "@babel/transform-react-pure-annotations"] +} diff --git a/packages/react-components/react-icons-compat/.eslintrc.json b/packages/react-components/react-icons-compat/.eslintrc.json new file mode 100644 index 0000000000000..ceea884c70dcc --- /dev/null +++ b/packages/react-components/react-icons-compat/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "extends": ["plugin:@fluentui/eslint-plugin/react"], + "root": true +} diff --git a/packages/react-components/react-icons-compat/.storybook/main.js b/packages/react-components/react-icons-compat/.storybook/main.js new file mode 100644 index 0000000000000..26536b61b387f --- /dev/null +++ b/packages/react-components/react-icons-compat/.storybook/main.js @@ -0,0 +1,14 @@ +const rootMain = require('../../../../.storybook/main'); + +module.exports = /** @type {Omit} */ ({ + ...rootMain, + stories: [...rootMain.stories, '../stories/**/*.stories.mdx', '../stories/**/index.stories.@(ts|tsx)'], + addons: [...rootMain.addons], + webpackFinal: (config, options) => { + const localConfig = { ...rootMain.webpackFinal(config, options) }; + + // add your own webpack tweaks if needed + + return localConfig; + }, +}); diff --git a/packages/react-components/react-icons-compat/.storybook/preview.js b/packages/react-components/react-icons-compat/.storybook/preview.js new file mode 100644 index 0000000000000..1939500a3d18c --- /dev/null +++ b/packages/react-components/react-icons-compat/.storybook/preview.js @@ -0,0 +1,7 @@ +import * as rootPreview from '../../../../.storybook/preview'; + +/** @type {typeof rootPreview.decorators} */ +export const decorators = [...rootPreview.decorators]; + +/** @type {typeof rootPreview.parameters} */ +export const parameters = { ...rootPreview.parameters }; diff --git a/packages/react-components/react-icons-compat/.storybook/tsconfig.json b/packages/react-components/react-icons-compat/.storybook/tsconfig.json new file mode 100644 index 0000000000000..ea89218a3d916 --- /dev/null +++ b/packages/react-components/react-icons-compat/.storybook/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "", + "allowJs": true, + "checkJs": true, + "types": ["static-assets", "environment", "storybook__addons"] + }, + "include": ["../stories/**/*.stories.ts", "../stories/**/*.stories.tsx", "*.js"] +} diff --git a/packages/react-components/react-icons-compat/.swcrc b/packages/react-components/react-icons-compat/.swcrc new file mode 100644 index 0000000000000..b4ffa86dee306 --- /dev/null +++ b/packages/react-components/react-icons-compat/.swcrc @@ -0,0 +1,30 @@ +{ + "$schema": "https://json.schemastore.org/swcrc", + "exclude": [ + "/testing", + "/**/*.cy.ts", + "/**/*.cy.tsx", + "/**/*.spec.ts", + "/**/*.spec.tsx", + "/**/*.test.ts", + "/**/*.test.tsx" + ], + "jsc": { + "parser": { + "syntax": "typescript", + "tsx": true, + "decorators": false, + "dynamicImport": false + }, + "externalHelpers": true, + "transform": { + "react": { + "runtime": "classic", + "useSpread": true + } + }, + "target": "es2019" + }, + "minify": false, + "sourceMaps": true +} diff --git a/packages/react-components/react-icons-compat/LICENSE b/packages/react-components/react-icons-compat/LICENSE new file mode 100644 index 0000000000000..1dd50a927d13d --- /dev/null +++ b/packages/react-components/react-icons-compat/LICENSE @@ -0,0 +1,15 @@ +@fluentui/react-icons-compat + +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ""Software""), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED _AS IS_, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Note: Usage of the fonts and icons referenced in Fluent UI React is subject to the terms listed at https://aka.ms/fluentui-assets-license diff --git a/packages/react-components/react-icons-compat/README.md b/packages/react-components/react-icons-compat/README.md new file mode 100644 index 0000000000000..57ebf3d84dff6 --- /dev/null +++ b/packages/react-components/react-icons-compat/README.md @@ -0,0 +1,5 @@ +# @fluentui/react-icons-compat + +**React Icons utility functions to help with migration from v8 to v9 [Fluent UI React](https://react.fluentui.dev/)** + +It is recommended to use this package only if you still need to use functions such as `registerIcons` from v8 icons. Otherwise it is recommended that you use the v9 icons directly from `@fluentui/react-icons`. diff --git a/packages/react-components/react-icons-compat/config/api-extractor.json b/packages/react-components/react-icons-compat/config/api-extractor.json new file mode 100644 index 0000000000000..e533bf30b48a2 --- /dev/null +++ b/packages/react-components/react-icons-compat/config/api-extractor.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "@fluentui/scripts-api-extractor/api-extractor.common.v-next.json" +} diff --git a/packages/react-components/react-icons-compat/config/tests.js b/packages/react-components/react-icons-compat/config/tests.js new file mode 100644 index 0000000000000..2e211ae9e2142 --- /dev/null +++ b/packages/react-components/react-icons-compat/config/tests.js @@ -0,0 +1 @@ +/** Jest test setup file. */ diff --git a/packages/react-components/react-icons-compat/docs/Spec.md b/packages/react-components/react-icons-compat/docs/Spec.md new file mode 100644 index 0000000000000..a5f24c96adb5d --- /dev/null +++ b/packages/react-components/react-icons-compat/docs/Spec.md @@ -0,0 +1,63 @@ +# @fluentui/react-icons-compat Spec + +## Background + +_Description and use cases of this component_ + +## Prior Art + +_Include background research done for this component_ + +- _Link to Open UI research_ +- _Link to comparison of v7 and v0_ +- _Link to GitHub epic issue for the converged component_ + +## Sample Code + +_Provide some representative example code that uses the proposed API for the component_ + +## Variants + +_Describe visual or functional variants of this control, if applicable. For example, a slider could have a 2D variant._ + +## API + +_List the **Props** and **Slots** proposed for the component. Ideally this would just be a link to the component's `.types.ts` file_ + +## Structure + +- _**Public**_ +- _**Internal**_ +- _**DOM** - how the component will be rendered as HTML elements_ + +## Migration + +_Describe what will need to be done to upgrade from the existing implementations:_ + +- _Migration from v8_ +- _Migration from v0_ + +## Behaviors + +_Explain how the component will behave in use, including:_ + +- _Component States_ +- _Interaction_ + - _Keyboard_ + - _Cursor_ + - _Touch_ + - _Screen readers_ + +## Accessibility + +Base accessibility information is included in the design document. After the spec is filled and review, outcomes from it need to be communicated to design and incorporated in the design document. + +- Decide whether to use **native element** or folow **ARIA** and provide reasons +- Identify the **[ARIA](https://www.w3.org/TR/wai-aria-practices-1.2/) pattern** and, if the component is listed there, follow its specification as possible. +- Identify accessibility **variants**, the `role` ([ARIA roles](https://www.w3.org/TR/wai-aria-1.1/#role_definitions)) of the component, its `slots` and `aria-*` props. +- Describe the **keyboard navigation**: Tab Oder and Arrow Key Navigation. Describe any other keyboard **shortcuts** used +- Specify texts for **state change announcements** - [ARIA live regions + ](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions) (number of available items in dropdown, error messages, confirmations, ...) +- Identify UI parts that appear on **hover or focus** and specify keyboard and screen reader interaction with them +- List cases when **focus** needs to be **trapped** in sections of the UI (for dialogs and popups or for hierarchical navigation) +- List cases when **focus** needs to be **moved programatically** (if parts of the UI are appearing/disappearing or other cases) diff --git a/packages/react-components/react-icons-compat/etc/react-icons-compat.api.md b/packages/react-components/react-icons-compat/etc/react-icons-compat.api.md new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/react-components/react-icons-compat/jest.config.js b/packages/react-components/react-icons-compat/jest.config.js new file mode 100644 index 0000000000000..42ab7c30b5c9d --- /dev/null +++ b/packages/react-components/react-icons-compat/jest.config.js @@ -0,0 +1,21 @@ +// @ts-check + +/** + * @type {import('@jest/types').Config.InitialOptions} + */ +module.exports = { + displayName: 'react-icons-compat', + preset: '../../../jest.preset.js', + transform: { + '^.+\\.tsx?$': [ + 'ts-jest', + { + tsconfig: '/tsconfig.spec.json', + isolatedModules: true, + }, + ], + }, + coverageDirectory: './coverage', + setupFilesAfterEnv: ['./config/tests.js'], + snapshotSerializers: ['@griffel/jest-serializer'], +}; diff --git a/packages/react-components/react-icons-compat/just.config.ts b/packages/react-components/react-icons-compat/just.config.ts new file mode 100644 index 0000000000000..b7b2c9a33bf43 --- /dev/null +++ b/packages/react-components/react-icons-compat/just.config.ts @@ -0,0 +1,5 @@ +import { preset, task } from '@fluentui/scripts-tasks'; + +preset(); + +task('build', 'build:react-components').cached?.(); diff --git a/packages/react-components/react-icons-compat/package.json b/packages/react-components/react-icons-compat/package.json new file mode 100644 index 0000000000000..f672e8006091d --- /dev/null +++ b/packages/react-components/react-icons-compat/package.json @@ -0,0 +1,68 @@ +{ + "name": "@fluentui/react-icons-compat", + "version": "0.0.0", + "private": true, + "description": "New fluentui react package", + "main": "lib-commonjs/index.js", + "module": "lib/index.js", + "typings": "./dist/index.d.ts", + "sideEffects": false, + "files": [ + "*.md", + "dist/*.d.ts", + "lib", + "lib-commonjs" + ], + "repository": { + "type": "git", + "url": "https://github.com/microsoft/fluentui" + }, + "license": "MIT", + "scripts": { + "build": "just-scripts build", + "clean": "just-scripts clean", + "generate-api": "just-scripts generate-api", + "lint": "just-scripts lint", + "start": "yarn storybook", + "storybook": "start-storybook", + "test": "jest --passWithNoTests", + "test-ssr": "test-ssr \"./stories/**/*.stories.tsx\"", + "type-check": "tsc -b tsconfig.json" + }, + "devDependencies": { + "@fluentui/eslint-plugin": "*", + "@fluentui/react-conformance": "*", + "@fluentui/react-conformance-griffel": "*", + "@fluentui/scripts-api-extractor": "*", + "@fluentui/scripts-tasks": "*" + }, + "dependencies": { + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "@types/react-dom": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0", + "react-dom": ">=16.8.0 <19.0.0" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "node": "./lib-commonjs/index.js", + "import": "./lib/index.js", + "require": "./lib-commonjs/index.js" + }, + "./package.json": "./package.json" + }, + "beachball": { + "disallowedChangeTypes": [ + "major", + "prerelease" + ] + } +} diff --git a/packages/react-components/react-icons-compat/project.json b/packages/react-components/react-icons-compat/project.json new file mode 100644 index 0000000000000..81714d8fd9bc9 --- /dev/null +++ b/packages/react-components/react-icons-compat/project.json @@ -0,0 +1,8 @@ +{ + "name": "@fluentui/react-icons-compat", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "sourceRoot": "packages/react-components/react-icons-compat/src", + "tags": ["platform:web", "vNext", "compat"], + "implicitDependencies": [] +} diff --git a/packages/react-components/react-icons-compat/src/GlobalSettings.ts b/packages/react-components/react-icons-compat/src/GlobalSettings.ts new file mode 100644 index 0000000000000..995bc1d347534 --- /dev/null +++ b/packages/react-components/react-icons-compat/src/GlobalSettings.ts @@ -0,0 +1,122 @@ +import { getWindow } from './getWindow'; + +/** + * Storing global state in local module variables has issues when more than one copy + * if the module gets loaded on the page (due to a bundling error or simply by consuming + * a prebundled script.) + * + * This file contains helpers to deal with the getting and setting local state, and allows + * callers to get called back when it mutates. + */ + +const GLOBAL_SETTINGS_PROP_NAME = '__globalSettings__'; +const CALLBACK_STATE_PROP_NAME = '__callbacks__'; + +let _counter = 0; + +/** + * Change description used for change callbacks in GlobalSettings. + * + * @public + * {@docCategory IChangeDescription} + */ +export interface IChangeDescription { + key: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + oldValue: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + value: any; +} + +/** + * Change event callback. + * + * @public + * {@docCategory IChangeEventCallback} + */ +export interface IChangeEventCallback { + __id__?: string; + (changeDescription?: IChangeDescription): void; +} + +/** + * Global settings helper, which stores settings in the global (window) namespace. + * If window is not provided, it will store settings in module scope. Provides a + * way to observe changes as well when their values change. + * + * @public + * {@docCategory GlobalSettings} + */ +export class GlobalSettings { + public static getValue(key: string, defaultValue?: T | (() => T)): T { + const globalSettings = _getGlobalSettings(); + + if (globalSettings[key] === undefined) { + globalSettings[key] = typeof defaultValue === 'function' ? (defaultValue as Function)() : defaultValue; + } + + return globalSettings[key]; + } + + public static setValue(key: string, value: T): T { + const globalSettings = _getGlobalSettings(); + const callbacks = globalSettings[CALLBACK_STATE_PROP_NAME]; + let oldValue = globalSettings[key]; + + if (value !== oldValue) { + globalSettings[key] = value; + + let changeDescription = { + oldValue, + value, + key, + }; + + for (let id in callbacks) { + if (callbacks.hasOwnProperty(id)) { + callbacks[id](changeDescription); + } + } + } + + return value; + } + + public static addChangeListener(cb: IChangeEventCallback): void { + // Note: we use generated ids on the callbacks to create a map of the callbacks, which optimizes removal. + // (It's faster to delete a key than it is to look up the index of an object and splice an array.) + let id = cb.__id__; + const callbacks = _getCallbacks(); + + if (!id) { + id = cb.__id__ = String(_counter++); + } + + callbacks[id] = cb; + } + + public static removeChangeListener(cb: IChangeEventCallback): void { + const callbacks = _getCallbacks(); + delete callbacks[cb.__id__ as string]; + } +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function _getGlobalSettings(): { [key: string]: any } { + const win = getWindow(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const globalObj: { [key: string]: any } = win || {}; + + if (!globalObj[GLOBAL_SETTINGS_PROP_NAME]) { + globalObj[GLOBAL_SETTINGS_PROP_NAME] = { + [CALLBACK_STATE_PROP_NAME]: {}, + }; + } + + return globalObj[GLOBAL_SETTINGS_PROP_NAME]; +} + +function _getCallbacks(): { [key: string]: () => void } { + const globalSettings = _getGlobalSettings(); + return globalSettings[CALLBACK_STATE_PROP_NAME]; +} diff --git a/packages/react-components/react-icons-compat/src/getWindow.ts b/packages/react-components/react-icons-compat/src/getWindow.ts new file mode 100644 index 0000000000000..19ff343ee9c65 --- /dev/null +++ b/packages/react-components/react-icons-compat/src/getWindow.ts @@ -0,0 +1,30 @@ +import { canUseDOM } from '@fluentui/react-utilities'; + +let _window: Window | undefined = undefined; + +// Note: Accessing "window" in IE11 is somewhat expensive, and calling "typeof window" +// hits a memory leak, whereas aliasing it and calling "typeof _window" does not. +// Caching the window value at the file scope lets us minimize the impact. +try { + _window = window; +} catch (e) { + /* no-op */ +} + +/** + * Helper to get the window object. The helper will make sure to use a cached variable + * of "window", to avoid overhead and memory leaks in IE11. Note that in popup scenarios the + * window object won't match the "global" window object, and for these scenarios, you should + * pass in an element hosted within the popup. + * + * @public + */ +export function getWindow(rootElement?: Element | null): Window | undefined { + if (!canUseDOM() || typeof _window === 'undefined') { + return undefined; + } else { + const el = rootElement as Element; + + return el && el.ownerDocument && el.ownerDocument.defaultView ? el.ownerDocument.defaultView : _window; + } +} diff --git a/packages/react-components/react-icons-compat/src/icon.ts b/packages/react-components/react-icons-compat/src/icon.ts new file mode 100644 index 0000000000000..12742b7dee901 --- /dev/null +++ b/packages/react-components/react-icons-compat/src/icon.ts @@ -0,0 +1,219 @@ +import { GlobalSettings } from './GlobalSettings'; +import { warn } from './warn'; + +export interface IIconSubset { + icons: { + [key: string]: string | JSX.Element; + }; + /** + * Indicates to the icon renderer that it is safe to merge any props on the original `Icon` element + * onto the child content element registered for the icon which are valid for HTML images. + */ + mergeImageProps?: boolean; +} + +export interface IIconSubsetRecord extends IIconSubset { + isRegistered?: boolean; + className?: string; +} + +export interface IIconRecord { + code: string | undefined; + subset: IIconSubsetRecord; +} + +export interface IIconOptions { + /** + * By default, registering the same set of icons will generate a console warning per duplicate icon + * registered, because this scenario can create unexpected consequences. + * + * Some scenarios include: + * + * Icon set was previously registered using a different base url. + * Icon set was previously registered but a different version was provided. + * Icons in a previous registered set overlap with a new set. + * + * To simply ignore previously registered icons, you can specify to disable warnings. This means + * that if an icon which was previous registered is registered again, it will be silently ignored. + * However, consider whether the problems listed above will cause issues. + **/ + disableWarnings: boolean; + + /** + * @deprecated Use `disableWarnings` instead. + */ + warnOnMissingIcons?: boolean; +} + +export interface IIconRecords { + __options: IIconOptions; + __remapped: { [key: string]: string }; + [key: string]: IIconRecord | {}; +} + +const ICON_SETTING_NAME = 'icons'; + +const _iconSettings = GlobalSettings.getValue(ICON_SETTING_NAME, { + __options: { + disableWarnings: false, + warnOnMissingIcons: true, + }, + __remapped: {}, +}); + +/** + * Normalizes an icon name for consistent mapping. + * Current implementation is to convert the icon name to lower case. + * + * @param name - Icon name to normalize. + * @returns {string} Normalized icon name to use for indexing and mapping. + */ +const normalizeIconName = (name: string): string => name.toLowerCase(); + +/** + * Registers a given subset of icons. + * + * @param iconSubset - the icon subset definition. + */ +export function registerIcons(iconSubset: IIconSubset, options?: Partial): void { + let subset = { + ...iconSubset, + isRegistered: false, + className: undefined, + }; + let { icons } = iconSubset; + + // Grab options, optionally mix user provided ones on top. + options = options ? { ..._iconSettings.__options, ...options } : _iconSettings.__options; + + for (const iconName in icons) { + if (icons.hasOwnProperty(iconName)) { + const code = icons[iconName]; + const normalizedIconName = normalizeIconName(iconName); + + if (_iconSettings[normalizedIconName]) { + _warnDuplicateIcon(iconName); + } else { + _iconSettings[normalizedIconName] = { + code, + subset, + } as IIconRecord; + } + } + } +} + +/** + * Unregisters icons by name. + * + * @param iconNames - List of icons to unregister. + */ +export function unregisterIcons(iconNames: string[]): void { + const options = _iconSettings.__options; + + for (const iconName of iconNames) { + const normalizedIconName = normalizeIconName(iconName); + if (_iconSettings[normalizedIconName]) { + delete _iconSettings[normalizedIconName]; + } else { + // Warn that we are trying to delete an icon that doesn't exist + if (!options.disableWarnings) { + warn(`The icon "${iconName}" tried to unregister but was not registered.`); + } + } + + // Delete any aliases for this iconName + if (_iconSettings.__remapped[normalizedIconName]) { + delete _iconSettings.__remapped[normalizedIconName]; + } + + // Delete any items that were an alias for this iconName + Object.keys(_iconSettings.__remapped).forEach((key: string) => { + if (_iconSettings.__remapped[key] === normalizedIconName) { + delete _iconSettings.__remapped[key]; + } + }); + } +} + +/** + * Remaps one icon name to another. + */ +export function registerIconAlias(iconName: string, mappedToName: string): void { + _iconSettings.__remapped[normalizeIconName(iconName)] = normalizeIconName(mappedToName); +} + +/** + * Gets an icon definition. If an icon is requested but the subset has yet to be registered, + * it will get registered immediately. + * + * @public + * @param name - Name of icon. + */ +export function getIcon(name?: string): IIconRecord | undefined { + let icon: IIconRecord | undefined = undefined; + const options = _iconSettings.__options; + + name = name ? normalizeIconName(name) : ''; + name = _iconSettings.__remapped[name] || name; + + if (name) { + icon = _iconSettings[name!] as IIconRecord; + + if (icon) { + let { subset } = icon; + if (subset) { + if (!subset.isRegistered) { + subset.isRegistered = true; + } + } + } else { + // eslint-disable-next-line deprecation/deprecation + if (!options.disableWarnings && options.warnOnMissingIcons) { + warn( + `The icon "${name}" was used but not registered. See https://github.com/microsoft/fluentui/wiki/Using-icons for more information.`, + ); + } + } + } + + return icon; +} + +/** + * Sets the icon options. + * + * @public + */ +export function setIconOptions(options: Partial): void { + _iconSettings.__options = { + ..._iconSettings.__options, + ...options, + }; +} + +let _missingIcons: string[] = []; +let _missingIconsTimer: ReturnType | undefined = undefined; + +function _warnDuplicateIcon(iconName: string): void { + const options = _iconSettings.__options; + const warningDelay = 2000; + const maxIconsInMessage = 10; + + if (!options.disableWarnings) { + _missingIcons.push(iconName); + if (_missingIconsTimer === undefined) { + _missingIconsTimer = setTimeout(() => { + warn( + `Some icons were re-registered. Applications should only call registerIcons for any given ` + + `icon once. Redefining what an icon is may have unintended consequences. Duplicates ` + + `include: \n` + + _missingIcons.slice(0, maxIconsInMessage).join(', ') + + (_missingIcons.length > maxIconsInMessage ? ` (+ ${_missingIcons.length - maxIconsInMessage} more)` : ''), + ); + _missingIconsTimer = undefined; + _missingIcons = []; + }, warningDelay); + } + } +} diff --git a/packages/react-components/react-icons-compat/src/index.ts b/packages/react-components/react-icons-compat/src/index.ts new file mode 100644 index 0000000000000..546d31896183c --- /dev/null +++ b/packages/react-components/react-icons-compat/src/index.ts @@ -0,0 +1,7 @@ +export { getWindow } from './getWindow'; +export { GlobalSettings } from './GlobalSettings'; +export type { IChangeEventCallback, IChangeDescription } from './GlobalSettings'; +export { setWarningCallback, warn } from './warn'; +export type { ISettingsMap } from './warn'; +export { registerIcons, registerIconAlias, unregisterIcons, getIcon, setIconOptions } from './icon'; +export type { IIconRecord, IIconOptions, IIconSubset, IIconSubsetRecord, IIconRecords } from './icon'; diff --git a/packages/react-components/react-icons-compat/src/testing/isConformant.ts b/packages/react-components/react-icons-compat/src/testing/isConformant.ts new file mode 100644 index 0000000000000..a3d988f29a172 --- /dev/null +++ b/packages/react-components/react-icons-compat/src/testing/isConformant.ts @@ -0,0 +1,15 @@ +import { isConformant as baseIsConformant } from '@fluentui/react-conformance'; +import type { IsConformantOptions, TestObject } from '@fluentui/react-conformance'; +import griffelTests from '@fluentui/react-conformance-griffel'; + +export function isConformant( + testInfo: Omit, 'componentPath'> & { componentPath?: string }, +) { + const defaultOptions: Partial> = { + tsConfig: { configName: 'tsconfig.spec.json' }, + componentPath: require.main?.filename.replace('.test', ''), + extraTests: griffelTests as TestObject, + }; + + baseIsConformant(defaultOptions, testInfo); +} diff --git a/packages/react-components/react-icons-compat/src/warn.ts b/packages/react-components/react-icons-compat/src/warn.ts new file mode 100644 index 0000000000000..ab50c45e8141b --- /dev/null +++ b/packages/react-components/react-icons-compat/src/warn.ts @@ -0,0 +1,30 @@ +/* eslint-disable no-console */ + +let _warningCallback: ((message: string) => void) | undefined = undefined; + +export type ISettingsMap = { [P in keyof T]?: string }; + +/** + * Sends a warning to console, if the api is present. + * + * @public + * @param message - Warning message. + */ +export function warn(message: string): void { + if (_warningCallback && process.env.NODE_ENV !== 'production') { + _warningCallback(message); + } else if (console && console.warn) { + console.warn(message); + } +} + +/** + * Configures the warning callback. Passing in undefined will reset it to use the default + * console.warn function. + * + * @public + * @param warningCallback - Callback to override the generated warnings. + */ +export function setWarningCallback(warningCallback?: (message: string) => void): void { + _warningCallback = warningCallback; +} diff --git a/packages/react-components/react-icons-compat/tsconfig.json b/packages/react-components/react-icons-compat/tsconfig.json new file mode 100644 index 0000000000000..1941a041d46c1 --- /dev/null +++ b/packages/react-components/react-icons-compat/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "target": "ES2019", + "noEmit": true, + "isolatedModules": true, + "importHelpers": true, + "jsx": "react", + "noUnusedLocals": true, + "preserveConstEnums": true + }, + "include": [], + "files": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + }, + { + "path": "./.storybook/tsconfig.json" + } + ] +} diff --git a/packages/react-components/react-icons-compat/tsconfig.lib.json b/packages/react-components/react-icons-compat/tsconfig.lib.json new file mode 100644 index 0000000000000..6f90cf95c005b --- /dev/null +++ b/packages/react-components/react-icons-compat/tsconfig.lib.json @@ -0,0 +1,22 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "lib": ["ES2019", "dom"], + "declaration": true, + "declarationDir": "../../../dist/out-tsc/types", + "outDir": "../../../dist/out-tsc", + "inlineSources": true, + "types": ["static-assets", "environment"] + }, + "exclude": [ + "./src/testing/**", + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.test.ts", + "**/*.test.tsx", + "**/*.stories.ts", + "**/*.stories.tsx" + ], + "include": ["./src/**/*.ts", "./src/**/*.tsx"] +} diff --git a/packages/react-components/react-icons-compat/tsconfig.spec.json b/packages/react-components/react-icons-compat/tsconfig.spec.json new file mode 100644 index 0000000000000..911456fe4b4d9 --- /dev/null +++ b/packages/react-components/react-icons-compat/tsconfig.spec.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "CommonJS", + "outDir": "dist", + "types": ["jest", "node"] + }, + "include": [ + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.test.ts", + "**/*.test.tsx", + "**/*.d.ts", + "./src/testing/**/*.ts", + "./src/testing/**/*.tsx" + ] +} diff --git a/tsconfig.base.all.json b/tsconfig.base.all.json index d8469954d5f76..b5c9692a5f722 100644 --- a/tsconfig.base.all.json +++ b/tsconfig.base.all.json @@ -92,6 +92,7 @@ "@fluentui/react-aria": ["packages/react-components/react-aria/src/index.ts"], "@fluentui/react-avatar": ["packages/react-components/react-avatar/src/index.ts"], "@fluentui/react-badge": ["packages/react-components/react-badge/src/index.ts"], + "@fluentui/react-breadcrumb": ["packages/react-components/react-breadcrumb/src/index.ts"], "@fluentui/react-button": ["packages/react-components/react-button/src/index.ts"], "@fluentui/react-calendar-compat": ["packages/react-components/react-calendar-compat/src/index.ts"], "@fluentui/react-card": ["packages/react-components/react-card/src/index.ts"], @@ -155,6 +156,9 @@ "@fluentui/react-tabs": ["packages/react-components/react-tabs/src/index.ts"], "@fluentui/react-tabster": ["packages/react-components/react-tabster/src/index.ts"], "@fluentui/react-tags": ["packages/react-components/react-tags/src/index.ts"], + "@fluentui/react-teaching-popover-preview": [ + "packages/react-components/react-teaching-popover-preview/src/index.ts" + ], "@fluentui/react-text": ["packages/react-components/react-text/src/index.ts"], "@fluentui/react-textarea": ["packages/react-components/react-textarea/src/index.ts"], "@fluentui/react-theme": ["packages/react-components/react-theme/src/index.ts"], @@ -171,10 +175,7 @@ "@fluentui/theme-designer": ["packages/react-components/theme-designer/src/index.ts"], "@fluentui/tokens": ["packages/tokens/src/index.ts"], "@fluentui/workspace-plugin": ["tools/workspace-plugin/src/index.ts"], - "@fluentui/react-breadcrumb": ["packages/react-components/react-breadcrumb/src/index.ts"], - "@fluentui/react-teaching-popover-preview": [ - "packages/react-components/react-teaching-popover-preview/src/index.ts" - ] + "@fluentui/react-icons-compat": ["packages/react-components/react-icons-compat/src/index.ts"] } } } diff --git a/tsconfig.base.json b/tsconfig.base.json index 2100ca922f881..0e891a4b9319f 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -44,6 +44,7 @@ "@fluentui/react-drawer": ["packages/react-components/react-drawer/src/index.ts"], "@fluentui/react-field": ["packages/react-components/react-field/src/index.ts"], "@fluentui/react-focus-management": ["packages/react-focus-management/src/index.ts"], + "@fluentui/react-icons-compat": ["packages/react-components/react-icons-compat/src/index.ts"], "@fluentui/react-image": ["packages/react-components/react-image/src/index.ts"], "@fluentui/react-infobutton": ["packages/react-components/react-infobutton/src/index.ts"], "@fluentui/react-infolabel": ["packages/react-components/react-infolabel/src/index.ts"], From 907769541ceaad852937fb4177e1feaa6728d9fd Mon Sep 17 00:00:00 2001 From: KHMakoto Date: Tue, 5 Dec 2023 16:40:26 -0800 Subject: [PATCH 2/9] Fixing build issue (hacky workaround). --- .../etc/react-icons-compat.api.md | 112 ++++++++++++++++++ .../react-icons-compat/package.json | 11 +- .../react-icons-compat/src/icon.ts | 3 + 3 files changed, 119 insertions(+), 7 deletions(-) diff --git a/packages/react-components/react-icons-compat/etc/react-icons-compat.api.md b/packages/react-components/react-icons-compat/etc/react-icons-compat.api.md index e69de29bb2d1d..c20ec5a2e74fe 100644 --- a/packages/react-components/react-icons-compat/etc/react-icons-compat.api.md +++ b/packages/react-components/react-icons-compat/etc/react-icons-compat.api.md @@ -0,0 +1,112 @@ +## API Report File for "@fluentui/react-icons-compat" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +// @public +export function getIcon(name?: string): IIconRecord | undefined; + +// @public +export function getWindow(rootElement?: Element | null): Window | undefined; + +// @public +export class GlobalSettings { + // (undocumented) + static addChangeListener(cb: IChangeEventCallback): void; + // (undocumented) + static getValue(key: string, defaultValue?: T | (() => T)): T; + // (undocumented) + static removeChangeListener(cb: IChangeEventCallback): void; + // (undocumented) + static setValue(key: string, value: T): T; +} + +// @public +export interface IChangeDescription { + // (undocumented) + key: string; + // (undocumented) + oldValue: any; + // (undocumented) + value: any; +} + +// @public +export interface IChangeEventCallback { + // (undocumented) + (changeDescription?: IChangeDescription): void; + // (undocumented) + __id__?: string; +} + +// @public (undocumented) +export interface IIconOptions { + disableWarnings: boolean; + // @deprecated (undocumented) + warnOnMissingIcons?: boolean; +} + +// @public (undocumented) +export interface IIconRecord { + // (undocumented) + code: string | undefined; + // (undocumented) + subset: IIconSubsetRecord; +} + +// @public (undocumented) +export interface IIconRecords { + // (undocumented) + [key: string]: IIconRecord | {}; + // (undocumented) + __options: IIconOptions; + // (undocumented) + __remapped: { + [key: string]: string; + }; +} + +// @public (undocumented) +export interface IIconSubset { + // (undocumented) + icons: { + [key: string]: string | JSX.Element; + }; + mergeImageProps?: boolean; +} + +// @public (undocumented) +export interface IIconSubsetRecord extends IIconSubset { + // (undocumented) + className?: string; + // (undocumented) + isRegistered?: boolean; +} + +// @public (undocumented) +export type ISettingsMap = { + [P in keyof T]?: string; +}; + +// @public +export function registerIconAlias(iconName: string, mappedToName: string): void; + +// @public +export function registerIcons(iconSubset: IIconSubset, options?: Partial): void; + +// @public +export function setIconOptions(options: Partial): void; + +// @public +export function setWarningCallback(warningCallback?: (message: string) => void): void; + +// @public +export function unregisterIcons(iconNames: string[]): void; + +// @public +export function warn(message: string): void; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-icons-compat/package.json b/packages/react-components/react-icons-compat/package.json index f672e8006091d..d3fb08a66ab78 100644 --- a/packages/react-components/react-icons-compat/package.json +++ b/packages/react-components/react-icons-compat/package.json @@ -37,18 +37,15 @@ "@fluentui/scripts-tasks": "*" }, "dependencies": { - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-theme": "^9.1.16", "@fluentui/react-utilities": "^9.15.2", "@griffel/react": "^1.5.14", "@swc/helpers": "^0.5.1" }, "peerDependencies": { - "@types/react": ">=16.8.0 <19.0.0", - "@types/react-dom": ">=16.8.0 <19.0.0", - "react": ">=16.8.0 <19.0.0", - "react-dom": ">=16.8.0 <19.0.0" + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" }, "exports": { ".": { diff --git a/packages/react-components/react-icons-compat/src/icon.ts b/packages/react-components/react-icons-compat/src/icon.ts index 12742b7dee901..cb38823d484d3 100644 --- a/packages/react-components/react-icons-compat/src/icon.ts +++ b/packages/react-components/react-icons-compat/src/icon.ts @@ -1,6 +1,9 @@ +import * as React from 'react'; import { GlobalSettings } from './GlobalSettings'; import { warn } from './warn'; +export type T = React.ReactNode; + export interface IIconSubset { icons: { [key: string]: string | JSX.Element; From bbf36ada11913670d25b9a58f2c0379d65929dab Mon Sep 17 00:00:00 2001 From: Tomi Olubeko Date: Tue, 5 Dec 2023 18:24:56 -0800 Subject: [PATCH 3/9] apply suggestions from code review --- .../etc/react-icons-compat.api.md | 66 ++++--------------- .../react-icons-compat/src/GlobalSettings.ts | 15 ++--- .../react-icons-compat/src/icon.ts | 37 +++++------ .../react-icons-compat/src/index.ts | 7 +- .../react-icons-compat/src/warn.ts | 30 --------- 5 files changed, 36 insertions(+), 119 deletions(-) delete mode 100644 packages/react-components/react-icons-compat/src/warn.ts diff --git a/packages/react-components/react-icons-compat/etc/react-icons-compat.api.md b/packages/react-components/react-icons-compat/etc/react-icons-compat.api.md index c20ec5a2e74fe..f595fd88a3689 100644 --- a/packages/react-components/react-icons-compat/etc/react-icons-compat.api.md +++ b/packages/react-components/react-icons-compat/etc/react-icons-compat.api.md @@ -5,62 +5,29 @@ ```ts // @public -export function getIcon(name?: string): IIconRecord | undefined; - -// @public -export function getWindow(rootElement?: Element | null): Window | undefined; - -// @public -export class GlobalSettings { - // (undocumented) - static addChangeListener(cb: IChangeEventCallback): void; - // (undocumented) - static getValue(key: string, defaultValue?: T | (() => T)): T; - // (undocumented) - static removeChangeListener(cb: IChangeEventCallback): void; - // (undocumented) - static setValue(key: string, value: T): T; -} - -// @public -export interface IChangeDescription { - // (undocumented) - key: string; - // (undocumented) - oldValue: any; - // (undocumented) - value: any; -} - -// @public -export interface IChangeEventCallback { - // (undocumented) - (changeDescription?: IChangeDescription): void; - // (undocumented) - __id__?: string; -} +export function getIcon(name?: string): IconRecord | undefined; // @public (undocumented) -export interface IIconOptions { +export interface IconOptions { disableWarnings: boolean; // @deprecated (undocumented) warnOnMissingIcons?: boolean; } // @public (undocumented) -export interface IIconRecord { +export interface IconRecord { // (undocumented) code: string | undefined; // (undocumented) - subset: IIconSubsetRecord; + subset: IconSubsetRecord; } // @public (undocumented) -export interface IIconRecords { +export interface IconRecords { // (undocumented) - [key: string]: IIconRecord | {}; + [key: string]: IconRecord | {}; // (undocumented) - __options: IIconOptions; + __options: IconOptions; // (undocumented) __remapped: { [key: string]: string; @@ -68,7 +35,7 @@ export interface IIconRecords { } // @public (undocumented) -export interface IIconSubset { +export interface IconSubset { // (undocumented) icons: { [key: string]: string | JSX.Element; @@ -77,36 +44,25 @@ export interface IIconSubset { } // @public (undocumented) -export interface IIconSubsetRecord extends IIconSubset { +export interface IconSubsetRecord extends IconSubset { // (undocumented) className?: string; // (undocumented) isRegistered?: boolean; } -// @public (undocumented) -export type ISettingsMap = { - [P in keyof T]?: string; -}; - // @public export function registerIconAlias(iconName: string, mappedToName: string): void; // @public -export function registerIcons(iconSubset: IIconSubset, options?: Partial): void; - -// @public -export function setIconOptions(options: Partial): void; +export function registerIcons(iconSubset: IconSubset, options?: Partial): void; // @public -export function setWarningCallback(warningCallback?: (message: string) => void): void; +export function setIconOptions(options: Partial): void; // @public export function unregisterIcons(iconNames: string[]): void; -// @public -export function warn(message: string): void; - // (No @packageDocumentation comment for this package) ``` diff --git a/packages/react-components/react-icons-compat/src/GlobalSettings.ts b/packages/react-components/react-icons-compat/src/GlobalSettings.ts index 995bc1d347534..02ce8bcf6ad9e 100644 --- a/packages/react-components/react-icons-compat/src/GlobalSettings.ts +++ b/packages/react-components/react-icons-compat/src/GlobalSettings.ts @@ -2,7 +2,7 @@ import { getWindow } from './getWindow'; /** * Storing global state in local module variables has issues when more than one copy - * if the module gets loaded on the page (due to a bundling error or simply by consuming + * of the module gets loaded on the page (due to a bundling error or simply by consuming * a prebundled script.) * * This file contains helpers to deal with the getting and setting local state, and allows @@ -18,9 +18,8 @@ let _counter = 0; * Change description used for change callbacks in GlobalSettings. * * @public - * {@docCategory IChangeDescription} */ -export interface IChangeDescription { +export interface ChangeDescription { key: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any oldValue: any; @@ -32,11 +31,10 @@ export interface IChangeDescription { * Change event callback. * * @public - * {@docCategory IChangeEventCallback} */ -export interface IChangeEventCallback { +export interface ChangeEventCallback { __id__?: string; - (changeDescription?: IChangeDescription): void; + (changeDescription?: ChangeDescription): void; } /** @@ -45,7 +43,6 @@ export interface IChangeEventCallback { * way to observe changes as well when their values change. * * @public - * {@docCategory GlobalSettings} */ export class GlobalSettings { public static getValue(key: string, defaultValue?: T | (() => T)): T { @@ -82,7 +79,7 @@ export class GlobalSettings { return value; } - public static addChangeListener(cb: IChangeEventCallback): void { + public static addChangeListener(cb: ChangeEventCallback): void { // Note: we use generated ids on the callbacks to create a map of the callbacks, which optimizes removal. // (It's faster to delete a key than it is to look up the index of an object and splice an array.) let id = cb.__id__; @@ -95,7 +92,7 @@ export class GlobalSettings { callbacks[id] = cb; } - public static removeChangeListener(cb: IChangeEventCallback): void { + public static removeChangeListener(cb: ChangeEventCallback): void { const callbacks = _getCallbacks(); delete callbacks[cb.__id__ as string]; } diff --git a/packages/react-components/react-icons-compat/src/icon.ts b/packages/react-components/react-icons-compat/src/icon.ts index cb38823d484d3..04ecded0060d3 100644 --- a/packages/react-components/react-icons-compat/src/icon.ts +++ b/packages/react-components/react-icons-compat/src/icon.ts @@ -1,10 +1,9 @@ import * as React from 'react'; import { GlobalSettings } from './GlobalSettings'; -import { warn } from './warn'; export type T = React.ReactNode; -export interface IIconSubset { +export interface IconSubset { icons: { [key: string]: string | JSX.Element; }; @@ -15,17 +14,17 @@ export interface IIconSubset { mergeImageProps?: boolean; } -export interface IIconSubsetRecord extends IIconSubset { +export interface IconSubsetRecord extends IconSubset { isRegistered?: boolean; className?: string; } -export interface IIconRecord { +export interface IconRecord { code: string | undefined; - subset: IIconSubsetRecord; + subset: IconSubsetRecord; } -export interface IIconOptions { +export interface IconOptions { /** * By default, registering the same set of icons will generate a console warning per duplicate icon * registered, because this scenario can create unexpected consequences. @@ -48,15 +47,15 @@ export interface IIconOptions { warnOnMissingIcons?: boolean; } -export interface IIconRecords { - __options: IIconOptions; +export interface IconRecords { + __options: IconOptions; __remapped: { [key: string]: string }; - [key: string]: IIconRecord | {}; + [key: string]: IconRecord | {}; } const ICON_SETTING_NAME = 'icons'; -const _iconSettings = GlobalSettings.getValue(ICON_SETTING_NAME, { +const _iconSettings = GlobalSettings.getValue(ICON_SETTING_NAME, { __options: { disableWarnings: false, warnOnMissingIcons: true, @@ -78,7 +77,7 @@ const normalizeIconName = (name: string): string => name.toLowerCase(); * * @param iconSubset - the icon subset definition. */ -export function registerIcons(iconSubset: IIconSubset, options?: Partial): void { +export function registerIcons(iconSubset: IconSubset, options?: Partial): void { let subset = { ...iconSubset, isRegistered: false, @@ -100,7 +99,7 @@ export function registerIcons(iconSubset: IIconSubset, options?: Partial): void { +export function setIconOptions(options: Partial): void { _iconSettings.__options = { ..._iconSettings.__options, ...options, @@ -207,7 +206,7 @@ function _warnDuplicateIcon(iconName: string): void { _missingIcons.push(iconName); if (_missingIconsTimer === undefined) { _missingIconsTimer = setTimeout(() => { - warn( + console.warn( `Some icons were re-registered. Applications should only call registerIcons for any given ` + `icon once. Redefining what an icon is may have unintended consequences. Duplicates ` + `include: \n` + diff --git a/packages/react-components/react-icons-compat/src/index.ts b/packages/react-components/react-icons-compat/src/index.ts index 546d31896183c..9749ac0ff1c79 100644 --- a/packages/react-components/react-icons-compat/src/index.ts +++ b/packages/react-components/react-icons-compat/src/index.ts @@ -1,7 +1,2 @@ -export { getWindow } from './getWindow'; -export { GlobalSettings } from './GlobalSettings'; -export type { IChangeEventCallback, IChangeDescription } from './GlobalSettings'; -export { setWarningCallback, warn } from './warn'; -export type { ISettingsMap } from './warn'; export { registerIcons, registerIconAlias, unregisterIcons, getIcon, setIconOptions } from './icon'; -export type { IIconRecord, IIconOptions, IIconSubset, IIconSubsetRecord, IIconRecords } from './icon'; +export type { IconRecord, IconOptions, IconSubset, IconSubsetRecord, IconRecords } from './icon'; diff --git a/packages/react-components/react-icons-compat/src/warn.ts b/packages/react-components/react-icons-compat/src/warn.ts deleted file mode 100644 index ab50c45e8141b..0000000000000 --- a/packages/react-components/react-icons-compat/src/warn.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* eslint-disable no-console */ - -let _warningCallback: ((message: string) => void) | undefined = undefined; - -export type ISettingsMap = { [P in keyof T]?: string }; - -/** - * Sends a warning to console, if the api is present. - * - * @public - * @param message - Warning message. - */ -export function warn(message: string): void { - if (_warningCallback && process.env.NODE_ENV !== 'production') { - _warningCallback(message); - } else if (console && console.warn) { - console.warn(message); - } -} - -/** - * Configures the warning callback. Passing in undefined will reset it to use the default - * console.warn function. - * - * @public - * @param warningCallback - Callback to override the generated warnings. - */ -export function setWarningCallback(warningCallback?: (message: string) => void): void { - _warningCallback = warningCallback; -} From 8715ed7e58fe6d910a9d6c7f1fd2edc0e3c20474 Mon Sep 17 00:00:00 2001 From: Tomi Olubeko Date: Wed, 6 Dec 2023 14:49:28 -0800 Subject: [PATCH 4/9] fix lint error --- .../react-icons-compat/src/GlobalSettings.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-components/react-icons-compat/src/GlobalSettings.ts b/packages/react-components/react-icons-compat/src/GlobalSettings.ts index 02ce8bcf6ad9e..b160e787d3bba 100644 --- a/packages/react-components/react-icons-compat/src/GlobalSettings.ts +++ b/packages/react-components/react-icons-compat/src/GlobalSettings.ts @@ -58,18 +58,18 @@ export class GlobalSettings { public static setValue(key: string, value: T): T { const globalSettings = _getGlobalSettings(); const callbacks = globalSettings[CALLBACK_STATE_PROP_NAME]; - let oldValue = globalSettings[key]; + const oldValue = globalSettings[key]; if (value !== oldValue) { globalSettings[key] = value; - let changeDescription = { + const changeDescription = { oldValue, value, key, }; - for (let id in callbacks) { + for (const id in callbacks) { if (callbacks.hasOwnProperty(id)) { callbacks[id](changeDescription); } From 120e50a1de4d45ce022cddd4568bf4926ba4d994 Mon Sep 17 00:00:00 2001 From: Tomi Olubeko Date: Wed, 6 Dec 2023 15:21:13 -0800 Subject: [PATCH 5/9] fix lint errors in icon.ts --- packages/react-components/react-icons-compat/src/icon.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/react-components/react-icons-compat/src/icon.ts b/packages/react-components/react-icons-compat/src/icon.ts index 04ecded0060d3..998e4429787b3 100644 --- a/packages/react-components/react-icons-compat/src/icon.ts +++ b/packages/react-components/react-icons-compat/src/icon.ts @@ -78,12 +78,12 @@ const normalizeIconName = (name: string): string => name.toLowerCase(); * @param iconSubset - the icon subset definition. */ export function registerIcons(iconSubset: IconSubset, options?: Partial): void { - let subset = { + const subset = { ...iconSubset, isRegistered: false, className: undefined, }; - let { icons } = iconSubset; + const { icons } = iconSubset; // Grab options, optionally mix user provided ones on top. options = options ? { ..._iconSettings.__options, ...options } : _iconSettings.__options; @@ -120,6 +120,7 @@ export function unregisterIcons(iconNames: string[]): void { } else { // Warn that we are trying to delete an icon that doesn't exist if (!options.disableWarnings) { + // eslint-disable-next-line no-console console.warn(`The icon "${iconName}" tried to unregister but was not registered.`); } } @@ -163,7 +164,7 @@ export function getIcon(name?: string): IconRecord | undefined { icon = _iconSettings[name!] as IconRecord; if (icon) { - let { subset } = icon; + const { subset } = icon; if (subset) { if (!subset.isRegistered) { subset.isRegistered = true; @@ -172,6 +173,7 @@ export function getIcon(name?: string): IconRecord | undefined { } else { // eslint-disable-next-line deprecation/deprecation if (!options.disableWarnings && options.warnOnMissingIcons) { + // eslint-disable-next-line no-console console.warn( `The icon "${name}" was used but not registered. See https://github.com/microsoft/fluentui/wiki/Using-icons for more information.`, ); @@ -206,6 +208,7 @@ function _warnDuplicateIcon(iconName: string): void { _missingIcons.push(iconName); if (_missingIconsTimer === undefined) { _missingIconsTimer = setTimeout(() => { + // eslint-disable-next-line no-console console.warn( `Some icons were re-registered. Applications should only call registerIcons for any given ` + `icon once. Redefining what an icon is may have unintended consequences. Duplicates ` + From 120fad084df61f988403b25737f031a685c08437 Mon Sep 17 00:00:00 2001 From: Tomi Olubeko Date: Sun, 7 Jan 2024 22:07:43 -0800 Subject: [PATCH 6/9] replace instance of global window object with fluent provider --- .../react-icons-compat/src/getWindow.ts | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/packages/react-components/react-icons-compat/src/getWindow.ts b/packages/react-components/react-icons-compat/src/getWindow.ts index 19ff343ee9c65..a8c90fd558649 100644 --- a/packages/react-components/react-icons-compat/src/getWindow.ts +++ b/packages/react-components/react-icons-compat/src/getWindow.ts @@ -1,16 +1,6 @@ +import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; import { canUseDOM } from '@fluentui/react-utilities'; -let _window: Window | undefined = undefined; - -// Note: Accessing "window" in IE11 is somewhat expensive, and calling "typeof window" -// hits a memory leak, whereas aliasing it and calling "typeof _window" does not. -// Caching the window value at the file scope lets us minimize the impact. -try { - _window = window; -} catch (e) { - /* no-op */ -} - /** * Helper to get the window object. The helper will make sure to use a cached variable * of "window", to avoid overhead and memory leaks in IE11. Note that in popup scenarios the @@ -19,7 +9,10 @@ try { * * @public */ -export function getWindow(rootElement?: Element | null): Window | undefined { +export function getWindow(rootElement?: Element | null): Window | null | undefined { + const fluentProvider = useFluent(); + const targetDocument = fluentProvider?.targetDocument; + const _window = targetDocument?.defaultView; if (!canUseDOM() || typeof _window === 'undefined') { return undefined; } else { From 368d53967144bb6965de15f83f8f22871f9c17e6 Mon Sep 17 00:00:00 2001 From: Tomi Olubeko Date: Sun, 7 Jan 2024 22:25:11 -0800 Subject: [PATCH 7/9] move call to useFluent() outside function scope --- .../react-components/react-icons-compat/src/getWindow.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/react-components/react-icons-compat/src/getWindow.ts b/packages/react-components/react-icons-compat/src/getWindow.ts index a8c90fd558649..3c4896287b9fe 100644 --- a/packages/react-components/react-icons-compat/src/getWindow.ts +++ b/packages/react-components/react-icons-compat/src/getWindow.ts @@ -1,6 +1,10 @@ import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; import { canUseDOM } from '@fluentui/react-utilities'; +const fluentProvider = useFluent(); +const targetDocument = fluentProvider?.targetDocument; +const _window = targetDocument?.defaultView; + /** * Helper to get the window object. The helper will make sure to use a cached variable * of "window", to avoid overhead and memory leaks in IE11. Note that in popup scenarios the @@ -10,9 +14,6 @@ import { canUseDOM } from '@fluentui/react-utilities'; * @public */ export function getWindow(rootElement?: Element | null): Window | null | undefined { - const fluentProvider = useFluent(); - const targetDocument = fluentProvider?.targetDocument; - const _window = targetDocument?.defaultView; if (!canUseDOM() || typeof _window === 'undefined') { return undefined; } else { From 2297d70958c3500a63a91a4c25845f3a30832318 Mon Sep 17 00:00:00 2001 From: Tomi Olubeko Date: Sun, 7 Jan 2024 22:57:14 -0800 Subject: [PATCH 8/9] add disable-no-restricted-globals instead of useFluent hook --- .../react-icons-compat/src/getWindow.ts | 42 ++++++++++++++++--- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/packages/react-components/react-icons-compat/src/getWindow.ts b/packages/react-components/react-icons-compat/src/getWindow.ts index 3c4896287b9fe..9edea949d3a9e 100644 --- a/packages/react-components/react-icons-compat/src/getWindow.ts +++ b/packages/react-components/react-icons-compat/src/getWindow.ts @@ -1,9 +1,16 @@ -import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; import { canUseDOM } from '@fluentui/react-utilities'; -const fluentProvider = useFluent(); -const targetDocument = fluentProvider?.targetDocument; -const _window = targetDocument?.defaultView; +let _window: Window | undefined = undefined; + +// Note: Accessing "window" in IE11 is somewhat expensive, and calling "typeof window" +// hits a memory leak, whereas aliasing it and calling "typeof _window" does not. +// Caching the window value at the file scope lets us minimize the impact. +try { + // eslint-disable-next-line no-restricted-globals + _window = window; +} catch (e) { + /* no-op */ +} /** * Helper to get the window object. The helper will make sure to use a cached variable @@ -13,7 +20,7 @@ const _window = targetDocument?.defaultView; * * @public */ -export function getWindow(rootElement?: Element | null): Window | null | undefined { +export function getWindow(rootElement?: Element | null): Window | undefined { if (!canUseDOM() || typeof _window === 'undefined') { return undefined; } else { @@ -22,3 +29,28 @@ export function getWindow(rootElement?: Element | null): Window | null | undefin return el && el.ownerDocument && el.ownerDocument.defaultView ? el.ownerDocument.defaultView : _window; } } + +// import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; +// import { canUseDOM } from '@fluentui/react-utilities'; + +// const fluentProvider = useFluent(); +// const targetDocument = fluentProvider?.targetDocument; +// const _window = targetDocument?.defaultView; + +// /** +// * Helper to get the window object. The helper will make sure to use a cached variable +// * of "window", to avoid overhead and memory leaks in IE11. Note that in popup scenarios the +// * window object won't match the "global" window object, and for these scenarios, you should +// * pass in an element hosted within the popup. +// * +// * @public +// */ +// export function getWindow(rootElement?: Element | null): Window | null | undefined { +// if (!canUseDOM() || typeof _window === 'undefined') { +// return undefined; +// } else { +// const el = rootElement as Element; + +// return el && el.ownerDocument && el.ownerDocument.defaultView ? el.ownerDocument.defaultView : _window; +// } +// } From 72f8e7e4dd929be5e3100d3b23f240c1ddb95dda Mon Sep 17 00:00:00 2001 From: Tomi Olubeko Date: Mon, 8 Jan 2024 14:07:20 -0800 Subject: [PATCH 9/9] remove commented out code --- .../react-icons-compat/src/getWindow.ts | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/packages/react-components/react-icons-compat/src/getWindow.ts b/packages/react-components/react-icons-compat/src/getWindow.ts index 9edea949d3a9e..91c1b6f5d43ab 100644 --- a/packages/react-components/react-icons-compat/src/getWindow.ts +++ b/packages/react-components/react-icons-compat/src/getWindow.ts @@ -29,28 +29,3 @@ export function getWindow(rootElement?: Element | null): Window | undefined { return el && el.ownerDocument && el.ownerDocument.defaultView ? el.ownerDocument.defaultView : _window; } } - -// import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; -// import { canUseDOM } from '@fluentui/react-utilities'; - -// const fluentProvider = useFluent(); -// const targetDocument = fluentProvider?.targetDocument; -// const _window = targetDocument?.defaultView; - -// /** -// * Helper to get the window object. The helper will make sure to use a cached variable -// * of "window", to avoid overhead and memory leaks in IE11. Note that in popup scenarios the -// * window object won't match the "global" window object, and for these scenarios, you should -// * pass in an element hosted within the popup. -// * -// * @public -// */ -// export function getWindow(rootElement?: Element | null): Window | null | undefined { -// if (!canUseDOM() || typeof _window === 'undefined') { -// return undefined; -// } else { -// const el = rootElement as Element; - -// return el && el.ownerDocument && el.ownerDocument.defaultView ? el.ownerDocument.defaultView : _window; -// } -// }