diff --git a/packages/react-icons/convert-font.js b/packages/react-icons/convert-font.js index c67b3a04fa..003ab74ba9 100644 --- a/packages/react-icons/convert-font.js +++ b/packages/react-icons/convert-font.js @@ -17,6 +17,8 @@ const SRC_PATH = argv.source; const DEST_PATH = argv.dest; // @ts-ignore const CODEPOINT_DEST_PATH = argv.codepointDest; +// @ts-ignore +const RTL_FILTER_PATH =argv.filter; if (!SRC_PATH) { throw new Error("Icon source folder not specified by --source"); @@ -27,6 +29,9 @@ if (!DEST_PATH) { if (!CODEPOINT_DEST_PATH) { throw new Error("Output destination folder for codepoint map not specified by --dest"); } +if (!RTL_FILTER_PATH) { + throw new Error("Filter folder not specified by --filter"); +} processFiles(SRC_PATH, DEST_PATH) @@ -62,7 +67,6 @@ async function processFiles(src, dest) { const indexPath = path.join(dest, 'index.tsx') // Finally add the interface definition and then write out the index. indexContents.push('export { FluentIconsProps } from \'../utils/FluentIconsProps.types\''); - indexContents.push('export { default as wrapIcon } from \'../utils/wrapIcon\''); indexContents.push('export { default as bundleIcon } from \'../utils/bundleIcon\''); indexContents.push('export * from \'../utils/useIconState\''); indexContents.push('export * from \'../utils/constants\''); @@ -79,6 +83,8 @@ async function processFiles(src, dest) { * @returns { Promise } - chunked icon files to insert */ async function processFolder(srcPath, codepointMapDestFolder, resizable) { + const filterFile = fs.readFile(RTL_FILTER_PATH, { encoding: 'utf8' }) + var rtlArray = (await filterFile).split(/\r?\n/); var files = await glob(resizable ? 'FluentSystemIcons-Resizable.json' : 'FluentSystemIcons-{Filled,Regular}.json', { cwd: srcPath, absolute: true }); /** @type string[] */ @@ -86,7 +92,7 @@ async function processFolder(srcPath, codepointMapDestFolder, resizable) { await Promise.all(files.map(async (srcFile, index) => { /** @type {Record} */ const iconEntries = JSON.parse(await fs.readFile(srcFile, 'utf8')); - iconExports.push(...generateReactIconEntries(iconEntries, resizable)); + iconExports.push(...generateReactIconEntries(iconEntries, resizable, rtlArray)); return generateCodepointMapForWebpackPlugin( path.resolve(codepointMapDestFolder, path.basename(srcFile)), @@ -133,16 +139,17 @@ async function generateCodepointMapForWebpackPlugin(destPath, iconEntries, resiz * @param {boolean} resizable * @returns {string[]} */ -function generateReactIconEntries(iconEntries, resizable) { +function generateReactIconEntries(iconEntries, resizable, rtlArray) { /** @type {string[]} */ const iconExports = []; + var shouldAutoFlip; for (const [iconName, codepoint] of Object.entries(iconEntries)) { + shouldAutoFlip = rtlArray.includes(iconName); let destFilename = getReactIconNameFromGlyphName(iconName, resizable); - var jsCode = `export const ${destFilename} = /*#__PURE__*/createFluentFontIcon(${JSON.stringify(destFilename) }, ${JSON.stringify(String.fromCodePoint(codepoint)) }, ${resizable ? 2 /* Resizable */ : /filled$/i.test(iconName) ? 0 /* Filled */ : 1 /* Regular */ - }${resizable ? '' : `, ${/(?<=_)\d+(?=_filled|_regular)/.exec(iconName)[0]}` + }, ${shouldAutoFlip} ${resizable ? '' : `, ${/(?<=_)\d+(?=_filled|_regular)/.exec(iconName)[0]}` });`; iconExports.push(jsCode); diff --git a/packages/react-icons/convert.js b/packages/react-icons/convert.js index a92546bb89..2471971006 100644 --- a/packages/react-icons/convert.js +++ b/packages/react-icons/convert.js @@ -8,6 +8,7 @@ const _ = require("lodash"); const SRC_PATH = argv.source; const DEST_PATH = argv.dest; +const RTL_FILTER_PATH =argv.filter; const TSX_EXTENSION = '.tsx' if (!SRC_PATH) { @@ -16,11 +17,17 @@ if (!SRC_PATH) { if (!DEST_PATH) { throw new Error("Output destination folder not specified by --dest"); } +if (!RTL_FILTER_PATH) { + throw new Error("Filter folder not specified by --filter"); +} if (!fs.existsSync(DEST_PATH)) { fs.mkdirSync(DEST_PATH); } +const filterFile = fs.readFileSync(RTL_FILTER_PATH, { encoding: 'utf8' }) +var rtlArray = filterFile.split(/\r?\n/); +//console.log(rtlArray) processFiles(SRC_PATH, DEST_PATH) function processFiles(src, dest) { @@ -64,11 +71,12 @@ function processFiles(src, dest) { const indexPath = path.join(dest, 'index.tsx') // Finally add the interface definition and then write out the index. - indexContents.push('export { FluentIconsProps } from \'./utils/FluentIconsProps.types\''); - indexContents.push('export { default as wrapIcon } from \'./utils/wrapIcon\''); + indexContents.push('export type { FluentIconsProps } from \'./utils/FluentIconsProps.types\''); indexContents.push('export { default as bundleIcon } from \'./utils/bundleIcon\''); indexContents.push('export * from \'./utils/useIconState\''); indexContents.push('export * from \'./utils/constants\''); + indexContents.push('export { IconDirectionContextProvider, useIconContext } from \'./contexts/index\''); + indexContents.push('export type { IconDirectionContextValue } from \'./contexts/index\''); fs.writeFileSync(indexPath, indexContents.join('\n'), (err) => { if (err) throw err; @@ -100,7 +108,9 @@ function processFolder(srcPath, destPath, resizable) { if(resizable && !file.includes("20")) { return } - var iconName = file.substr(0, file.length - 4) // strip '.svg' + // Check to see if the svg should be autoflipped + var shouldAutoFlip = rtlArray.includes(file); + var iconName = file.substring(0, file.length - 4) // strip '.svg' iconName = iconName.replace("ic_fluent_", "") // strip ic_fluent_ iconName = resizable ? iconName.replace("20", "") : iconName var destFilename = _.camelCase(iconName) // We want them to be camelCase, so access_time would become accessTime here @@ -110,7 +120,7 @@ function processFolder(srcPath, destPath, resizable) { const getAttr = (key) => [...iconContent.matchAll(`(?<= ${key}=)".+?"`)].map((v) => v[0]); const width = resizable ? '"1em"' : getAttr("width")[0]; const paths = getAttr("d").join(','); - var jsCode = `export const ${destFilename} = (/*#__PURE__*/createFluentIcon('${destFilename}', ${width}, [${paths}]));` + var jsCode = `export const ${destFilename} = (/*#__PURE__*/createFluentIcon('${destFilename}', ${width}, [${paths}], ${shouldAutoFlip}));` iconExports.push(jsCode); } }); diff --git a/packages/react-icons/package.json b/packages/react-icons/package.json index fdeb076314..94b0b6b75f 100644 --- a/packages/react-icons/package.json +++ b/packages/react-icons/package.json @@ -16,8 +16,8 @@ "cleanSvg": "rm -rf ./intermediate", "copy": "node ../../importer/generate.js --source=../../assets --dest=./intermediate --extension=svg --target=react", "copy:font-files": "cpy './src/utils/fonts/*.{ttf,woff,woff2,json}' ./lib/utils/fonts/. && cpy './src/utils/fonts/*.{ttf,woff,woff2,json}' ./lib-cjs/utils/fonts/.", - "convert:svg": "node convert.js --source=./intermediate --dest=./src", - "convert:fonts": "node convert-font.js --source=./src/utils/fonts --dest=./src/fonts --codepointDest=./src/utils/fonts", + "convert:svg": "node convert.js --source=./intermediate --dest=./src --filter=../../importer/rtl.txt", + "convert:fonts": "node convert-font.js --source=./src/utils/fonts --dest=./src/fonts --codepointDest=./src/utils/fonts --filter=../../importer/rtl.txt", "generate:font-regular": "node ../../importer/generateFont.js --source=intermediate --dest=src/utils/fonts --iconType=Regular --codepoints=../../fonts/FluentSystemIcons-Regular.json", "generate:font-filled": "node ../../importer/generateFont.js --source=intermediate --dest=src/utils/fonts --iconType=Filled --codepoints=../../fonts/FluentSystemIcons-Filled.json", "generate:font-resizable": "node ../../importer/generateFont.js --source=intermediate --dest=src/utils/fonts --iconType=Resizable", @@ -81,4 +81,3 @@ } } } - diff --git a/packages/react-icons/src/contexts/IconDirectionContext.ts b/packages/react-icons/src/contexts/IconDirectionContext.ts new file mode 100644 index 0000000000..83157e9dd8 --- /dev/null +++ b/packages/react-icons/src/contexts/IconDirectionContext.ts @@ -0,0 +1,13 @@ +import * as React from 'react'; + +const IconDirectionContext = React.createContext(undefined); + +export interface IconDirectionContextValue { + textDirection?: 'ltr' | 'rtl' +} + +const IconDirectionContextDefaultValue: IconDirectionContextValue = {}; + +export const IconDirectionContextProvider = IconDirectionContext.Provider; + +export const useIconContext = () => React.useContext(IconDirectionContext) ?? IconDirectionContextDefaultValue; diff --git a/packages/react-icons/src/contexts/index.ts b/packages/react-icons/src/contexts/index.ts new file mode 100644 index 0000000000..f38d48d299 --- /dev/null +++ b/packages/react-icons/src/contexts/index.ts @@ -0,0 +1 @@ +export * from './IconDirectionContext'; \ No newline at end of file diff --git a/packages/react-icons/src/utils/bundleIcon.tsx b/packages/react-icons/src/utils/bundleIcon.tsx index 42184ad315..716a9814bc 100644 --- a/packages/react-icons/src/utils/bundleIcon.tsx +++ b/packages/react-icons/src/utils/bundleIcon.tsx @@ -1,5 +1,6 @@ import * as React from "react"; import { iconFilledClassName, iconRegularClassName } from "./constants"; +import type { FluentIcon } from "../utils/createFluentIcon"; import { FluentIconsProps } from "./FluentIconsProps.types"; import { makeStyles, mergeClasses } from "@griffel/react"; @@ -8,8 +9,8 @@ const useBundledIconStyles = makeStyles({ visible: { display: "inline" } }); -const bundleIcon = (FilledIcon: React.FC, RegularIcon: React.FC) => { - const Component: React.FC = (props) => { +const bundleIcon = (FilledIcon: FluentIcon, RegularIcon: FluentIcon) => { + const Component: FluentIcon = (props: FluentIconsProps) => { const { className, primaryFill = 'currentColor', filled, ...rest } = props; const styles = useBundledIconStyles(); return ( diff --git a/packages/react-icons/src/utils/createFluentIcon.ts b/packages/react-icons/src/utils/createFluentIcon.ts index d7c2c513d2..3875d8ba44 100644 --- a/packages/react-icons/src/utils/createFluentIcon.ts +++ b/packages/react-icons/src/utils/createFluentIcon.ts @@ -7,11 +7,11 @@ export type FluentIcon = { displayName?: string; } -export const createFluentIcon = (displayName: string, width: string, paths: string[]): FluentIcon => { +export const createFluentIcon = (displayName: string, width: string, paths: string[], shouldAutoFlip: boolean): FluentIcon => { const viewBoxWidth = width === "1em" ? "20" : width; const Icon = (props: FluentIconsProps) => { const state = { - ...useIconState(props), // HTML attributes/props for things like accessibility can be passed in, and will be expanded on the svg object at the start of the object + ...useIconState(props, shouldAutoFlip), // HTML attributes/props for things like accessibility can be passed in, and will be expanded on the svg object at the start of the object width, height: width, viewBox: `0 0 ${viewBoxWidth} ${viewBoxWidth}`, xmlns: "http://www.w3.org/2000/svg" }; return React.createElement( diff --git a/packages/react-icons/src/utils/fonts/createFluentFontIcon.tsx b/packages/react-icons/src/utils/fonts/createFluentFontIcon.tsx index 078013631a..6cb7203567 100644 --- a/packages/react-icons/src/utils/fonts/createFluentFontIcon.tsx +++ b/packages/react-icons/src/utils/fonts/createFluentFontIcon.tsx @@ -69,12 +69,12 @@ const useRootStyles = makeStyles({ }, }); -export function createFluentFontIcon(displayName: string, codepoint: string, font: FontFile, fontSize?: number): React.FC>> & { codepoint: string} { +export function createFluentFontIcon(displayName: string, codepoint: string, font: FontFile, shouldAutoFlip: boolean, fontSize?: number): React.FC>> & { codepoint: string} { const Component: React.FC>> & { codepoint: string} = (props) => { useStaticStyles(); const styles = useRootStyles(); const className = mergeClasses(styles.root, styles[font], props.className); - const state = useIconState>({...props, className}); + const state = useIconState>({...props, className}, shouldAutoFlip); // We want to keep the same API surface as the SVG icons, so translate `primaryFill` to `color` diff --git a/packages/react-icons/src/utils/useIconState.tsx b/packages/react-icons/src/utils/useIconState.tsx index 2a49d3331f..8faa8a7402 100644 --- a/packages/react-icons/src/utils/useIconState.tsx +++ b/packages/react-icons/src/utils/useIconState.tsx @@ -1,5 +1,6 @@ import { FluentIconsProps } from "./FluentIconsProps.types"; import { makeStyles, mergeClasses } from "@griffel/react"; +import { useIconContext } from "../contexts"; const useRootStyles = makeStyles({ root: { @@ -9,21 +10,30 @@ const useRootStyles = makeStyles({ "@media (forced-colors: active)": { forcedColorAdjust: 'auto', } + }, + flipped: { + transform: 'scaleX(-1)' } }); -export const useIconState = | React.HTMLAttributes) = React.SVGAttributes>(props: FluentIconsProps): Omit, 'primaryFill'> => { +export const useIconState = | React.HTMLAttributes) = React.SVGAttributes>(props: FluentIconsProps, shouldAutoFlip: boolean): Omit, 'primaryFill'> => { const { title, primaryFill = "currentColor", ...rest } = props; + const state = { ...rest, title: undefined, fill: primaryFill } as Omit, 'primaryFill'>; + const iconContext = useIconContext(); const styles = useRootStyles(); - state.className = mergeClasses(styles.root, state.className); - + if(shouldAutoFlip) { + state.className = mergeClasses(styles.root, iconContext.textDirection === 'rtl' && styles.flipped, state.className); + } else { + state.className = mergeClasses(styles.root, state.className); + } + if (title) { state['aria-label'] = title; } diff --git a/packages/react-icons/src/utils/wrapIcon.tsx b/packages/react-icons/src/utils/wrapIcon.tsx deleted file mode 100644 index 2896a1350f..0000000000 --- a/packages/react-icons/src/utils/wrapIcon.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import * as React from "react"; -import { FluentIconsProps } from "./FluentIconsProps.types"; -import { useIconState } from "./useIconState"; - -const wrapIcon = (Icon: (iconProps: FluentIconsProps) => JSX.Element, displayName?: string) => { - const WrappedIcon = (props: FluentIconsProps) => { - const state = useIconState(props); - return - } - WrappedIcon.displayName = displayName; - return WrappedIcon; -} - -export default wrapIcon; \ No newline at end of file