diff --git a/importer/rtlMetadata.js b/importer/rtlMetadata.js new file mode 100644 index 0000000000..51c35e1c08 --- /dev/null +++ b/importer/rtlMetadata.js @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +const fs = require("fs"); +const path = require("path"); +const process = require("process"); +const argv = require("yargs").boolean("selector").default("selector", false).boolean("keepdirs").default("keepdirs", false).argv; +const _ = require("lodash"); + +const SRC_PATH = argv.source; +const DEST_FILE = argv.dest; + +if (!SRC_PATH) { + throw new Error("Icon source folder not specified by --source"); +} +if (!DEST_FILE) { + throw new Error("Output destination file not specified by --dest"); +} + +const destFolder = path.dirname(DEST_FILE); +if (!fs.existsSync(destFolder)) { + fs.mkdirSync(destFolder); +} + +processFolder(SRC_PATH); +const result ={} +function processFolder(srcPath) { + fs.readdir(srcPath, function (err, files) { + if (err) { + console.error("Could not list the directory.", err); + process.exit(1); + } + + files.forEach(function (file, index) { + var srcFile = path.join(srcPath, file); + + fs.stat(srcFile, function (error, stat) { + if (error) { + console.error("Error stating file.", error); + return; + } + + if (stat.isDirectory()) { + processFolder(srcFile) + return; + } else if (file.startsWith('.')) { + // Skip invisible files + return; + } else if (file.startsWith('_')) { + // Skip invalid file names + return; + } else if (file === 'metadata.json') { + // If it's a metadata file, read and parse its content + fs.readFile(srcFile, 'utf8', (err, data) => { + if (err) { + console.error('Error reading metadata file:', err); + return; + } + try { + // Parse the json content + let metadata = JSON.parse(data); + let iconSize = metadata.size; + let iconName = metadata.name; + const directionType = metadata.directionType; + if (!directionType) { //ignore files with no directionType + return; + } + iconName = iconName.replace(/\s+/g, '') //remove space + iconName = iconName.replace(iconName.substring(0, 1), iconName.substring(0, 1).toUpperCase()) // capitalize the first letter + + iconSize.forEach((size, i) => { //iterate through the size file and create entries for each icon file + let tempName = iconName; + + tempName = tempName + size + "Filled"; + result[tempName] = directionType; + + tempName = iconName; + tempName = tempName + size + "Regular"; + result[tempName] = directionType; + + }) + let tempName = iconName + tempName = tempName + "Filled"; + result[tempName] = directionType; + + tempName = iconName; + tempName = tempName + "Regular"; + result[tempName] = directionType; + + convertToJson(result) + + } catch (error) { + console.error('Error parsing JSON in metadata file:', error); + } + }) + } + }) + }) + }) +} + +function convertToJson(result) { + const compiledJson = JSON.stringify(result, null, 2); + fs.writeFile(DEST_FILE, compiledJson, 'utf8', (err) => { + if (err) { + console.error('Error writing to JSON fIle: ', err) + } + }) +} diff --git a/packages/react-icons/convert-font.js b/packages/react-icons/convert-font.js index c67b3a04fa..6c19a39b00 100644 --- a/packages/react-icons/convert-font.js +++ b/packages/react-icons/convert-font.js @@ -3,12 +3,14 @@ // @ts-check const fs = require("fs/promises"); +const fsS = require("fs"); const path = require("path"); const process = require("process"); const argv = require("yargs").boolean("selector").default("selector", false).argv; const _ = require("lodash"); const mkdirp = require('mkdirp'); const { promisify } = require('util'); +const { option } = require("yargs"); const glob = promisify(require('glob')); // @ts-ignore @@ -17,6 +19,9 @@ const SRC_PATH = argv.source; const DEST_PATH = argv.dest; // @ts-ignore const CODEPOINT_DEST_PATH = argv.codepointDest; +// @ts-ignore +const RTL_FILE = argv.rtl; + if (!SRC_PATH) { throw new Error("Icon source folder not specified by --source"); @@ -27,6 +32,9 @@ if (!DEST_PATH) { if (!CODEPOINT_DEST_PATH) { throw new Error("Output destination folder for codepoint map not specified by --dest"); } +if (!RTL_FILE) { + throw new Error("RTL file not specified by --rtl"); +} processFiles(SRC_PATH, DEST_PATH) @@ -49,7 +57,7 @@ async function processFiles(src, dest) { // make file for sized icons const sizedIconPath = path.join(dest, 'sizedIcons'); - const sizedIconContents = await processFolder(src, CODEPOINT_DEST_PATH, false) + const sizedIconContents = await processFolder(src, CODEPOINT_DEST_PATH, false); await cleanFolder(sizedIconPath); await Promise.all(sizedIconContents.map(async (chunk, i) => { @@ -136,14 +144,15 @@ async function generateCodepointMapForWebpackPlugin(destPath, iconEntries, resiz function generateReactIconEntries(iconEntries, resizable) { /** @type {string[]} */ const iconExports = []; + const metadata = JSON.parse(fsS.readFileSync(RTL_FILE, 'utf-8')); for (const [iconName, codepoint] of Object.entries(iconEntries)) { let destFilename = getReactIconNameFromGlyphName(iconName, resizable); - + var flipInRtl = metadata[destFilename] === 'mirror'; 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]}` - });`; + }, ${resizable ? undefined : ` ${/(?<=_)\d+(?=_filled|_regular)/.exec(iconName)?.[0]}` + }${flipInRtl ? `, { flipInRtl: true }` : ''});`; iconExports.push(jsCode); } diff --git a/packages/react-icons/convert.js b/packages/react-icons/convert.js index 5c6b92fd1b..cf951325f1 100644 --- a/packages/react-icons/convert.js +++ b/packages/react-icons/convert.js @@ -8,7 +8,7 @@ const _ = require("lodash"); const SRC_PATH = argv.source; const DEST_PATH = argv.dest; -const TSX_EXTENSION = '.tsx' +const RTL_FILE = argv.rtl; if (!SRC_PATH) { throw new Error("Icon source folder not specified by --source"); @@ -17,6 +17,10 @@ if (!DEST_PATH) { throw new Error("Output destination folder not specified by --dest"); } +if (!RTL_FILE) { + throw new Error("RTL file not specified by --rtl"); +} + if (!fs.existsSync(DEST_PATH)) { fs.mkdirSync(DEST_PATH); } @@ -88,9 +92,11 @@ function processFolder(srcPath, destPath, resizable) { var files = fs.readdirSync(srcPath) /** @type string[] */ const iconExports = []; + var metadata = JSON.parse(fs.readFileSync(RTL_FILE, 'utf-8')); + //console.log(metadata); files.forEach(function (file, index) { var srcFile = path.join(srcPath, file) - if (fs.lstatSync(srcFile).isDirectory()) { + if (fs.lstatSync(srcFile).isDirectory() || !file.endsWith('.svg')) { // for now, ignore subdirectories/localization, until we have a plan for handling it // Will likely involve appending the lang/locale to the end of the friendly name for the unique component name // var joinedDestPath = path.join(destPath, file) @@ -102,17 +108,19 @@ function processFolder(srcPath, destPath, resizable) { if(resizable && !file.includes("20")) { return } - var iconName = file.substr(0, file.length - 4) // strip '.svg' + 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 destFilename = destFilename.replace(destFilename.substring(0, 1), destFilename.substring(0, 1).toUpperCase()) // capitalize the first letter + var flipInRtl = metadata[destFilename] === 'mirror'; //checks rtl.json to see if icon is autoflippable var iconContent = fs.readFileSync(srcFile, { encoding: "utf8" }) 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}]));` + const options = flipInRtl ? `, { flipInRtl: true }` : ''; + var jsCode = `export const ${destFilename} = (/*#__PURE__*/createFluentIcon('${destFilename}', ${width}, [${paths}]${options}));` iconExports.push(jsCode); } }); diff --git a/packages/react-icons/package.json b/packages/react-icons/package.json index 3b88d9e2dc..582b9a898c 100644 --- a/packages/react-icons/package.json +++ b/packages/react-icons/package.json @@ -13,19 +13,21 @@ }, "scripts": { "clean": "find ./src -type f ! -name \"wrapIcon.tsx\" -name \"*.tsx\" -delete", - "cleanSvg": "rm -rf ./intermediate", + "clean:svg": "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 --rtl=./intermediate/rtl.json", + "convert:fonts": "node convert-font.js --source=./src/utils/fonts --dest=./src/fonts --codepointDest=./src/utils/fonts --rtl=./intermediate/rtl.json", "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", "generate:font": "npm run generate:font-regular && npm run generate:font-filled && npm run generate:font-resizable", + "generate:rtl": "node ../../importer/rtlMetadata.js --source=../../assets --dest=./intermediate/rtl.json", "rollup": "node ./generateRollup.js", "optimize": "svgo --config svgo.config.js --folder=./intermediate --precision=2", "unfill": "find ./intermediate -type f -name \"*.svg\" -exec sed -i.bak 's/fill=\"none\"//g' {} \\; && find ./intermediate -type f -name \"*.bak\" -delete", - "build": "npm run copy && npm run generate:font && npm run optimize && npm run unfill && npm run convert:svg && npm run convert:fonts && npm run cleanSvg && npm run build:esm && npm run build:cjs && npm run copy:font-files", + "build": "npm run copy && npm run generate:font && npm run generate:rtl && npm run optimize && npm run unfill && npm run convert:svg && npm run convert:fonts && npm run clean:svg && npm run build:esm && npm run build:cjs && npm run copy:font-files", + "build:svg": "npm run copy && npm run generate:rtl && npm run optimize && npm run unfill && npm run convert:svg && npm run clean:svg && npm run build:esm && npm run build:cjs", "build:cjs": "tsc --module commonjs --outDir lib-cjs && babel lib-cjs --out-dir lib-cjs", "build:esm": "tsc && babel lib --out-dir lib" }, diff --git a/packages/react-icons/src/contexts/IconDirectionContext.ts b/packages/react-icons/src/contexts/IconDirectionContext.ts index 3f030baf9d..0dc4667965 100644 --- a/packages/react-icons/src/contexts/IconDirectionContext.ts +++ b/packages/react-icons/src/contexts/IconDirectionContext.ts @@ -10,4 +10,4 @@ const IconDirectionContextDefaultValue: IconDirectionContextValue = {}; export const IconDirectionContextProvider = IconDirectionContext.Provider; -export const useIconContext = () => React.useContext(IconDirectionContext) ?? IconDirectionContextDefaultValue; \ No newline at end of file +export const useIconContext = () => React.useContext(IconDirectionContext) ? React.useContext(IconDirectionContext) : IconDirectionContextDefaultValue \ No newline at end of file diff --git a/packages/react-icons/src/utils/createFluentIcon.ts b/packages/react-icons/src/utils/createFluentIcon.ts index 59f9bb38f2..dc287d08bf 100644 --- a/packages/react-icons/src/utils/createFluentIcon.ts +++ b/packages/react-icons/src/utils/createFluentIcon.ts @@ -7,11 +7,15 @@ export type FluentIcon = { displayName?: string; } -export const createFluentIcon = (displayName: string, width: string, paths: string[]): FluentIcon => { +export type CreateFluentIconOptions = { + flipInRtl?: boolean; +} + +export const createFluentIcon = (displayName: string, width: string, paths: string[], options?: CreateFluentIconOptions): FluentIcon => { const viewBoxWidth = width === "1em" ? "20" : width; const Icon = React.forwardRef((props: FluentIconsProps, ref: React.Ref) => { 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, { flipInRtl: options?.flipInRtl }), // 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 ref, width, height: width, viewBox: `0 0 ${viewBoxWidth} ${viewBoxWidth}`, xmlns: "http://www.w3.org/2000/svg" }; diff --git a/packages/react-icons/src/utils/fonts/createFluentFontIcon.tsx b/packages/react-icons/src/utils/fonts/createFluentFontIcon.tsx index 078013631a..eb98855611 100644 --- a/packages/react-icons/src/utils/fonts/createFluentFontIcon.tsx +++ b/packages/react-icons/src/utils/fonts/createFluentFontIcon.tsx @@ -69,12 +69,16 @@ const useRootStyles = makeStyles({ }, }); -export function createFluentFontIcon(displayName: string, codepoint: string, font: FontFile, fontSize?: number): React.FC>> & { codepoint: string} { +export type CreateFluentFontIconOptions = { + flipInRtl?: boolean; +} + +export function createFluentFontIcon(displayName: string, codepoint: string, font: FontFile, fontSize?: number, options?: CreateFluentFontIconOptions): 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}, { flipInRtl: options?.flipInRtl }); // 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..8b29de49d5 100644 --- a/packages/react-icons/src/utils/useIconState.tsx +++ b/packages/react-icons/src/utils/useIconState.tsx @@ -1,3 +1,4 @@ +import { useIconContext } from "../contexts"; import { FluentIconsProps } from "./FluentIconsProps.types"; import { makeStyles, mergeClasses } from "@griffel/react"; @@ -9,10 +10,17 @@ const useRootStyles = makeStyles({ "@media (forced-colors: active)": { forcedColorAdjust: 'auto', } + }, + rtl : { + transform: 'scaleX(-1)' } }); -export const useIconState = | React.HTMLAttributes) = React.SVGAttributes>(props: FluentIconsProps): Omit, 'primaryFill'> => { +export type UseIconStateOptions = { + flipInRtl?: boolean; +} + +export const useIconState = | React.HTMLAttributes) = React.SVGAttributes>(props: FluentIconsProps, options?: UseIconStateOptions): Omit, 'primaryFill'> => { const { title, primaryFill = "currentColor", ...rest } = props; const state = { ...rest, @@ -21,9 +29,14 @@ export const useIconState = , 'primaryFill'>; const styles = useRootStyles(); - - state.className = mergeClasses(styles.root, state.className); - + const iconContext = useIconContext(); + + state.className = mergeClasses( + styles.root, + options?.flipInRtl && iconContext?.textDirection === 'rtl' && styles.rtl, + state.className + ); + if (title) { state['aria-label'] = title; }