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
111 changes: 111 additions & 0 deletions importer/rtlMetadata.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// 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_PATH = argv.dest;

if (!SRC_PATH) {
throw new Error("Icon source folder not specified by --source");
}
if (!DEST_PATH) {
throw new Error("Output destination folder not specified by --dest");
}

if (!fs.existsSync(DEST_PATH)) {
fs.mkdirSync(DEST_PATH);
}

const destFile = path.join(DEST_PATH, 'rtl.json')
tomi-msft marked this conversation as resolved.
Show resolved Hide resolved

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.endsWith('.json')) {
// If it's a metadata file, read and parse its content
tomi-msft marked this conversation as resolved.
Show resolved Hide resolved
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 //get the size array
let iconName = metadata.name; // get the icon name
const directionType = metadata.directionType ? metadata.directionType : 'none'; // check to see if a directionType is in this manifest.json file
if(directionType === 'none') { //ignore files with no directionType
return
}
tomi-msft marked this conversation as resolved.
Show resolved Hide resolved
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);
}
})
}
})
})
})
}
//console.log(result)
tomi-msft marked this conversation as resolved.
Show resolved Hide resolved

function convertToJson(result) {
const compiledJson = JSON.stringify(result, null, 2);
fs.writeFile(destFile, compiledJson, 'utf8', (err) => {
if (err) {
console.error('Error writing to JSON fIle: ', err)
}
})
}
19 changes: 15 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_PATH = 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_PATH) {
throw new Error("RTL folder 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,17 @@ async function generateCodepointMapForWebpackPlugin(destPath, iconEntries, resiz
function generateReactIconEntries(iconEntries, resizable) {
/** @type {string[]} */
const iconExports = [];
let rtlPath = path.join(RTL_PATH, 'rtl.json')
tomi-msft marked this conversation as resolved.
Show resolved Hide resolved
const metadata = JSON.parse(fsS.readFileSync(rtlPath));
for (const [iconName, codepoint] of Object.entries(iconEntries)) {
let destFilename = getReactIconNameFromGlyphName(iconName, resizable);

var flipInRtl = metadata[destFilename] === 'mirror' //checks rtl.json to see if icon is autoflippable
tomi-msft marked this conversation as resolved.
Show resolved Hide resolved
const options = flipInRtl ? ` { flipInRtl: true }` : undefined;
tomi-msft marked this conversation as resolved.
Show resolved Hide resolved
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]}`
tomi-msft marked this conversation as resolved.
Show resolved Hide resolved
}, ${options});`;

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,14 +8,17 @@ const _ = require("lodash");

const SRC_PATH = argv.source;
const DEST_PATH = argv.dest;
const TSX_EXTENSION = '.tsx'
const RTL_PATH = argv.rtl;

if (!SRC_PATH) {
throw new Error("Icon source folder not specified by --source");
}
if (!DEST_PATH) {
throw new Error("Output destination folder not specified by --dest");
}
if (!RTL_PATH) {
throw new Error("RTL folder not specified by --rtl");
}

if (!fs.existsSync(DEST_PATH)) {
fs.mkdirSync(DEST_PATH);
Expand Down Expand Up @@ -88,9 +91,12 @@ function processFolder(srcPath, destPath, resizable) {
var files = fs.readdirSync(srcPath)
/** @type string[] */
const iconExports = [];
let rtlPath = path.join(RTL_PATH, 'rtl.json')
var metadata = JSON.parse(fs.readFileSync(rtlPath));
//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
8 changes: 5 additions & 3 deletions packages/react-icons/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,18 @@
"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 --rtl=./intermediate",
"convert:fonts": "node convert-font.js --source=./src/utils/fonts --dest=./src/fonts --codepointDest=./src/utils/fonts --rtl=./intermediate",
"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",
"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 cleanSvg && npm run build:esm && npm run build:cjs && npm run copy:font-files",
"buildSvg": "npm run copy && npm run generate:rtl && npm run optimize && npm run unfill && npm run convert:svg && npm run cleanSvg && npm run build:esm && npm run build:cjs",
tomi-msft marked this conversation as resolved.
Show resolved Hide resolved
"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
9 changes: 7 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,16 @@ export type FluentIcon = {
displayName?: string;
}

export const createFluentIcon = (displayName: string, width: string, paths: string[]): FluentIcon => {
export type CreateFluentIconOptions = {
flipInRtl?: boolean;
// rtlPaths?: string[];
tomi-msft marked this conversation as resolved.
Show resolved Hide resolved
}

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
@@ -1,5 +1,6 @@
import * as React from 'react';
import { FluentIconsProps } from '../FluentIconsProps.types';
import { CreateFluentIconOptions } from '../createFluentIcon';
import { makeStyles, makeStaticStyles, mergeClasses } from "@griffel/react";
import { useIconState } from '../useIconState';

Expand Down Expand Up @@ -69,12 +70,13 @@ const useRootStyles = makeStyles({
},
});

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

export function createFluentFontIcon(displayName: string, codepoint: string, font: FontFile, fontSize?: number, options?: CreateFluentIconOptions): 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