Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Icons autoflip logic #625

Merged
merged 9 commits into from
Aug 30, 2023
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);
tomi-msft marked this conversation as resolved.
Show resolved Hide resolved
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
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
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
Loading