Skip to content

Commit

Permalink
Icons autoflip logic (#625)
Browse files Browse the repository at this point in the history
* Update build script to add rtl autoflipping logic

* add rtl script

* Update useContext logic

* Update build scripts and clean up
  • Loading branch information
tomi-msft authored Aug 30, 2023
1 parent 6cdd705 commit ef89316
Show file tree
Hide file tree
Showing 8 changed files with 170 additions and 21 deletions.
109 changes: 109 additions & 0 deletions importer/rtlMetadata.js
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
17 changes: 13 additions & 4 deletions packages/react-icons/convert-font.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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");
Expand All @@ -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)

Expand All @@ -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) => {
Expand Down Expand Up @@ -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);
}
Expand Down
16 changes: 12 additions & 4 deletions packages/react-icons/convert.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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);
}
Expand Down Expand Up @@ -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)
Expand All @@ -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);
}
});
Expand Down
10 changes: 6 additions & 4 deletions packages/react-icons/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/react-icons/src/contexts/IconDirectionContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ const IconDirectionContextDefaultValue: IconDirectionContextValue = {};

export const IconDirectionContextProvider = IconDirectionContext.Provider;

export const useIconContext = () => React.useContext(IconDirectionContext) ?? IconDirectionContextDefaultValue;
export const useIconContext = () => React.useContext(IconDirectionContext) ? React.useContext(IconDirectionContext) : IconDirectionContextDefaultValue
8 changes: 6 additions & 2 deletions packages/react-icons/src/utils/createFluentIcon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLElement>) => {
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"
};
Expand Down
8 changes: 6 additions & 2 deletions packages/react-icons/src/utils/fonts/createFluentFontIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,16 @@ const useRootStyles = makeStyles({
},
});

export function createFluentFontIcon(displayName: string, codepoint: string, font: FontFile, fontSize?: number): React.FC<FluentIconsProps<React.HTMLAttributes<HTMLElement>>> & { codepoint: string} {
export type CreateFluentFontIconOptions = {
flipInRtl?: boolean;
}

export function createFluentFontIcon(displayName: string, codepoint: string, font: FontFile, fontSize?: number, options?: CreateFluentFontIconOptions): React.FC<FluentIconsProps<React.HTMLAttributes<HTMLElement>>> & { codepoint: string} {
const Component: React.FC<FluentIconsProps<React.HTMLAttributes<HTMLElement>>> & { codepoint: string} = (props) => {
useStaticStyles();
const styles = useRootStyles();
const className = mergeClasses(styles.root, styles[font], props.className);
const state = useIconState<React.HTMLAttributes<HTMLElement>>({...props, className});
const state = useIconState<React.HTMLAttributes<HTMLElement>>({...props, className}, { flipInRtl: options?.flipInRtl });


// We want to keep the same API surface as the SVG icons, so translate `primaryFill` to `color`
Expand Down
21 changes: 17 additions & 4 deletions packages/react-icons/src/utils/useIconState.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useIconContext } from "../contexts";
import { FluentIconsProps } from "./FluentIconsProps.types";
import { makeStyles, mergeClasses } from "@griffel/react";

Expand All @@ -9,10 +10,17 @@ const useRootStyles = makeStyles({
"@media (forced-colors: active)": {
forcedColorAdjust: 'auto',
}
},
rtl : {
transform: 'scaleX(-1)'
}
});

export const useIconState = <TBaseAttributes extends (React.SVGAttributes<SVGElement> | React.HTMLAttributes<HTMLElement>) = React.SVGAttributes<SVGElement>>(props: FluentIconsProps<TBaseAttributes>): Omit<FluentIconsProps<TBaseAttributes>, 'primaryFill'> => {
export type UseIconStateOptions = {
flipInRtl?: boolean;
}

export const useIconState = <TBaseAttributes extends (React.SVGAttributes<SVGElement> | React.HTMLAttributes<HTMLElement>) = React.SVGAttributes<SVGElement>>(props: FluentIconsProps<TBaseAttributes>, options?: UseIconStateOptions): Omit<FluentIconsProps<TBaseAttributes>, 'primaryFill'> => {
const { title, primaryFill = "currentColor", ...rest } = props;
const state = {
...rest,
Expand All @@ -21,9 +29,14 @@ export const useIconState = <TBaseAttributes extends (React.SVGAttributes<SVGEle
} as Omit<FluentIconsProps<TBaseAttributes>, '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;
}
Expand Down

0 comments on commit ef89316

Please sign in to comment.