@khanacademy/wonder-blocks-icon 2.2.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/components/phosphor-icon.d.ts +34 -65
- package/dist/es/index.js +24 -168
- package/dist/index.d.ts +1 -6
- package/dist/index.js +23 -169
- package/dist/types.d.ts +2 -6
- package/dist/util/icon-util.d.ts +1 -16
- package/package.json +1 -1
- package/src/components/__tests__/custom-icon-mock.svg +1 -0
- package/src/components/__tests__/phosphor-icon.test.tsx +16 -4
- package/src/components/__tests__/phosphor-icon.typestest.tsx +2 -8
- package/src/components/phosphor-icon.tsx +43 -53
- package/src/index.ts +1 -11
- package/src/types.ts +3 -6
- package/src/util/icon-util.test.ts +1 -80
- package/src/util/icon-util.ts +1 -49
- package/tsconfig-build.tsbuildinfo +1 -1
- package/dist/components/icon.d.ts +0 -70
- package/dist/util/icon-assets.d.ts +0 -31
- package/src/components/__tests__/icon.test.tsx +0 -152
- package/src/components/icon.tsx +0 -117
- package/src/util/icon-assets.test.ts +0 -35
- package/src/util/icon-assets.ts +0 -83
|
@@ -3,10 +3,6 @@ import PlusCircleRegular from "@phosphor-icons/core/regular/plus-circle.svg";
|
|
|
3
3
|
import PlusCircleBold from "@phosphor-icons/core/bold/plus-circle-bold.svg";
|
|
4
4
|
import PlusCircleFill from "@phosphor-icons/core/fill/plus-circle-fill.svg";
|
|
5
5
|
|
|
6
|
-
// @ts-expect-error - invalid icon weight (duotone)
|
|
7
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
8
|
-
import VideoDuoTone from "@phosphor-icons/core/duotone/video-duotone.svg";
|
|
9
|
-
|
|
10
6
|
import {PhosphorIcon} from "../phosphor-icon";
|
|
11
7
|
|
|
12
8
|
// Valid: small + bold
|
|
@@ -18,10 +14,8 @@ import {PhosphorIcon} from "../phosphor-icon";
|
|
|
18
14
|
// Valid: large + fill
|
|
19
15
|
<PhosphorIcon icon={PlusCircleFill} size="large" color="green" />;
|
|
20
16
|
|
|
21
|
-
//
|
|
22
|
-
// @ts-expect-error - small icons only support `bold` and `fill` weights.
|
|
17
|
+
// Valid: small + regular
|
|
23
18
|
<PhosphorIcon icon={PlusCircleRegular} size="small" color="green" />;
|
|
24
19
|
|
|
25
|
-
//
|
|
26
|
-
// @ts-expect-error - medium icons only support `regular` and `fill` weights.
|
|
20
|
+
// Valid: medium + bold
|
|
27
21
|
<PhosphorIcon icon={PlusCircleBold} size="medium" color="green" />;
|
|
@@ -4,17 +4,13 @@ import {StyleSheet} from "aphrodite";
|
|
|
4
4
|
import {addStyle, AriaProps, StyleType} from "@khanacademy/wonder-blocks-core";
|
|
5
5
|
|
|
6
6
|
import {viewportPixelsForSize} from "../util/icon-util";
|
|
7
|
-
import {
|
|
8
|
-
PhosphorIconAsset,
|
|
9
|
-
PhosphorIconMedium,
|
|
10
|
-
PhosphorIconSmall,
|
|
11
|
-
} from "../types";
|
|
7
|
+
import {IconSize, PhosphorIconAsset} from "../types";
|
|
12
8
|
|
|
13
9
|
// We use a span instead of an img because we want to use the mask-image CSS
|
|
14
10
|
// property.
|
|
15
11
|
const StyledIcon = addStyle("span");
|
|
16
12
|
|
|
17
|
-
type
|
|
13
|
+
type Props = Pick<AriaProps, "aria-hidden" | "aria-label" | "role"> & {
|
|
18
14
|
/**
|
|
19
15
|
* The color of the icon. Will default to `currentColor`, which means that
|
|
20
16
|
* it will take on the CSS `color` value from the parent element.
|
|
@@ -32,56 +28,24 @@ type CommonProps = Pick<AriaProps, "aria-hidden" | "aria-label"> & {
|
|
|
32
28
|
* Test ID used for e2e testing.
|
|
33
29
|
*/
|
|
34
30
|
testId?: string;
|
|
35
|
-
};
|
|
36
31
|
|
|
37
|
-
type PropsForSmallIcon = CommonProps & {
|
|
38
|
-
/**
|
|
39
|
-
* The icon size (16px).
|
|
40
|
-
*
|
|
41
|
-
* __NOTE:__ small icons only support `bold` and `fill` weights. **Make sure
|
|
42
|
-
* you are not using a `regular` icon.**
|
|
43
|
-
*/
|
|
44
|
-
size?: "small";
|
|
45
32
|
/**
|
|
46
|
-
*
|
|
47
|
-
* (
|
|
48
|
-
* __NOTE:__ small icons only support `bold` and `fill` weights.
|
|
33
|
+
* Size of the icon. One of `small` (16), `medium` (24), `large` (48), or
|
|
34
|
+
* `xlarge` (96). Defaults to `small`.
|
|
49
35
|
*/
|
|
50
|
-
|
|
51
|
-
};
|
|
36
|
+
size?: IconSize;
|
|
52
37
|
|
|
53
|
-
type PropsForMediumIcon = CommonProps & {
|
|
54
38
|
/**
|
|
55
|
-
* The icon
|
|
39
|
+
* The icon to display. This is a reference to the icon asset (imported as a
|
|
40
|
+
* static SVG file).
|
|
56
41
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
42
|
+
* It supports the following types:
|
|
43
|
+
* - `PhosphorIconAsset`: a reference to a Phosphor SVG asset.
|
|
44
|
+
* - `string`: an import referencing an arbitrary SVG file.
|
|
59
45
|
*/
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* The icon to display. This is a reference to the icon asset
|
|
63
|
-
* (imported as a static SVG file).
|
|
64
|
-
* __NOTE:__ medium icons only support `regular` and `fill` weights.
|
|
65
|
-
*/
|
|
66
|
-
icon: PhosphorIconMedium;
|
|
46
|
+
icon: PhosphorIconAsset | string;
|
|
67
47
|
};
|
|
68
48
|
|
|
69
|
-
type PropsForOtherSizes = CommonProps & {
|
|
70
|
-
/**
|
|
71
|
-
* large: The icon size (48px).
|
|
72
|
-
* xlarge: The icon size (96px).
|
|
73
|
-
*/
|
|
74
|
-
size?: "large" | "xlarge";
|
|
75
|
-
/**
|
|
76
|
-
* The icon to display. This is a reference to the icon asset
|
|
77
|
-
* (imported as a static SVG file).
|
|
78
|
-
*/
|
|
79
|
-
icon: PhosphorIconAsset;
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
// Define icon size by icon weight
|
|
83
|
-
type Props = PropsForSmallIcon | PropsForMediumIcon | PropsForOtherSizes;
|
|
84
|
-
|
|
85
49
|
/**
|
|
86
50
|
* A `PhosphorIcon` displays a small informational or decorative image as an
|
|
87
51
|
* HTML element that renders a Phosphor Icon SVG available from the
|
|
@@ -123,6 +87,7 @@ export const PhosphorIcon = React.forwardRef(function PhosphorIcon(
|
|
|
123
87
|
|
|
124
88
|
const pixelSize = viewportPixelsForSize(size);
|
|
125
89
|
const classNames = `${className ?? ""}`;
|
|
90
|
+
const iconStyles = _generateStyles(color, pixelSize);
|
|
126
91
|
|
|
127
92
|
return (
|
|
128
93
|
<StyledIcon
|
|
@@ -130,14 +95,12 @@ export const PhosphorIcon = React.forwardRef(function PhosphorIcon(
|
|
|
130
95
|
className={classNames}
|
|
131
96
|
style={[
|
|
132
97
|
styles.svg,
|
|
98
|
+
iconStyles.icon,
|
|
133
99
|
{
|
|
100
|
+
// We still pass inline styles to the icon itself, so we
|
|
101
|
+
// prevent the icon from being overridden by the inline
|
|
102
|
+
// styles.
|
|
134
103
|
maskImage: `url(${icon})`,
|
|
135
|
-
maskSize: "100%",
|
|
136
|
-
maskRepeat: "no-repeat",
|
|
137
|
-
maskPosition: "center",
|
|
138
|
-
backgroundColor: color,
|
|
139
|
-
width: pixelSize,
|
|
140
|
-
height: pixelSize,
|
|
141
104
|
},
|
|
142
105
|
style,
|
|
143
106
|
]}
|
|
@@ -147,12 +110,39 @@ export const PhosphorIcon = React.forwardRef(function PhosphorIcon(
|
|
|
147
110
|
);
|
|
148
111
|
});
|
|
149
112
|
|
|
113
|
+
const dynamicStyles: Record<string, any> = {};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Generates the visual styles for the icon.
|
|
117
|
+
*/
|
|
118
|
+
const _generateStyles = (color: string, size: number) => {
|
|
119
|
+
const iconStyle = `${color}-${size}`;
|
|
120
|
+
// The styles are cached to avoid creating a new object on every render.
|
|
121
|
+
if (styles[iconStyle]) {
|
|
122
|
+
return styles[iconStyle];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const newStyles: Record<string, any> = {
|
|
126
|
+
icon: {
|
|
127
|
+
backgroundColor: color,
|
|
128
|
+
width: size,
|
|
129
|
+
height: size,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
dynamicStyles[iconStyle] = StyleSheet.create(newStyles);
|
|
134
|
+
return dynamicStyles[iconStyle];
|
|
135
|
+
};
|
|
136
|
+
|
|
150
137
|
const styles = StyleSheet.create({
|
|
151
138
|
svg: {
|
|
152
139
|
display: "inline-block",
|
|
153
140
|
verticalAlign: "text-bottom",
|
|
154
141
|
flexShrink: 0,
|
|
155
142
|
flexGrow: 0,
|
|
143
|
+
maskSize: "100%",
|
|
144
|
+
maskRepeat: "no-repeat",
|
|
145
|
+
maskPosition: "center",
|
|
156
146
|
},
|
|
157
147
|
});
|
|
158
148
|
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,2 @@
|
|
|
1
|
-
import Icon from "./components/icon";
|
|
2
|
-
import type {IconAsset, IconSize} from "./util/icon-assets";
|
|
3
|
-
|
|
4
|
-
export * as icons from "./util/icon-assets";
|
|
5
1
|
export {PhosphorIcon} from "./components/phosphor-icon";
|
|
6
|
-
export type {
|
|
7
|
-
PhosphorIconAsset,
|
|
8
|
-
PhosphorIconMedium,
|
|
9
|
-
PhosphorIconSmall,
|
|
10
|
-
} from "./types";
|
|
11
|
-
export type {IconAsset, IconSize};
|
|
12
|
-
export default Icon;
|
|
2
|
+
export type {IconSize, PhosphorIconAsset} from "./types";
|
package/src/types.ts
CHANGED
|
@@ -2,11 +2,8 @@
|
|
|
2
2
|
* All the possible icon weights.
|
|
3
3
|
*/
|
|
4
4
|
export type PhosphorIconAsset = PhosphorRegular | PhosphorBold | PhosphorFill;
|
|
5
|
+
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
-
*/
|
|
8
|
-
export type PhosphorIconSmall = PhosphorBold | PhosphorFill;
|
|
9
|
-
/**
|
|
10
|
-
* The different icon weights for medium icons.
|
|
7
|
+
* All the possible icon weights.
|
|
11
8
|
*/
|
|
12
|
-
export type
|
|
9
|
+
export type IconSize = "small" | "medium" | "large" | "xlarge";
|
|
@@ -1,83 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import type {IconSize, IconAsset} from "./icon-assets";
|
|
4
|
-
|
|
5
|
-
const SIZES = ["small", "medium", "large", "xlarge"];
|
|
6
|
-
|
|
7
|
-
const DUMMY_ICON_MEDIUM_ONLY = {
|
|
8
|
-
medium: "[MEDIUM SVG PATH]",
|
|
9
|
-
} as const;
|
|
10
|
-
|
|
11
|
-
const DUMMY_ICON_WITH_EVERYTHING_ON_IT: IconAsset = {
|
|
12
|
-
small: "[SMALL SVG PATH]",
|
|
13
|
-
medium: "[MEDIUM SVG PATH]",
|
|
14
|
-
large: "[LARGE SVG PATH]",
|
|
15
|
-
xlarge: "[XLARGE SVG PATH]",
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
describe("getPathForIcon", () => {
|
|
19
|
-
test("return the path for the correct size, if available", () => {
|
|
20
|
-
SIZES.forEach((size: any) => {
|
|
21
|
-
const {path, assetSize} = getPathForIcon(
|
|
22
|
-
DUMMY_ICON_WITH_EVERYTHING_ON_IT,
|
|
23
|
-
size,
|
|
24
|
-
);
|
|
25
|
-
expect(
|
|
26
|
-
// @ts-expect-error [FEI-5019] - TS7053 - Element implicitly has an 'any' type because expression of type 'any' can't be used to index type 'IconAsset'.
|
|
27
|
-
path === DUMMY_ICON_WITH_EVERYTHING_ON_IT[size] &&
|
|
28
|
-
assetSize === size,
|
|
29
|
-
).toBeTruthy();
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
test("scale up a small asset rather than scaling down a large one", () => {
|
|
34
|
-
const expectValueForSize = (
|
|
35
|
-
requestedSize: IconSize,
|
|
36
|
-
returnedSize: IconSize,
|
|
37
|
-
) => {
|
|
38
|
-
const iconMissingRequestedSize = {
|
|
39
|
-
...DUMMY_ICON_WITH_EVERYTHING_ON_IT,
|
|
40
|
-
} as const;
|
|
41
|
-
delete iconMissingRequestedSize[requestedSize];
|
|
42
|
-
expect(
|
|
43
|
-
getPathForIcon(iconMissingRequestedSize, requestedSize),
|
|
44
|
-
).toMatchObject({
|
|
45
|
-
assetSize: returnedSize,
|
|
46
|
-
path: DUMMY_ICON_WITH_EVERYTHING_ON_IT[returnedSize],
|
|
47
|
-
});
|
|
48
|
-
};
|
|
49
|
-
expectValueForSize("small", "medium");
|
|
50
|
-
expectValueForSize("medium", "small");
|
|
51
|
-
expectValueForSize("large", "medium");
|
|
52
|
-
expectValueForSize("xlarge", "large");
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test("return a path as long as at least one size is available", () => {
|
|
56
|
-
SIZES.forEach((size: any) => {
|
|
57
|
-
const {path, assetSize} = getPathForIcon(
|
|
58
|
-
DUMMY_ICON_MEDIUM_ONLY,
|
|
59
|
-
size,
|
|
60
|
-
);
|
|
61
|
-
expect(
|
|
62
|
-
path === DUMMY_ICON_MEDIUM_ONLY["medium"] &&
|
|
63
|
-
assetSize === "medium",
|
|
64
|
-
).toBeTruthy();
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test("no valid asset sizes, throws", () => {
|
|
69
|
-
// Arrange
|
|
70
|
-
const iconAsset: IconAsset = {} as any;
|
|
71
|
-
|
|
72
|
-
// Act
|
|
73
|
-
const underTest = () => getPathForIcon(iconAsset, "medium");
|
|
74
|
-
|
|
75
|
-
// Assert
|
|
76
|
-
expect(underTest).toThrowErrorMatchingInlineSnapshot(
|
|
77
|
-
`"Icon does not contain any valid asset sizes!"`,
|
|
78
|
-
);
|
|
79
|
-
});
|
|
80
|
-
});
|
|
1
|
+
import {viewportPixelsForSize} from "./icon-util";
|
|
81
2
|
|
|
82
3
|
describe("viewportPixelsForSize", () => {
|
|
83
4
|
test("return the correct values", () => {
|
package/src/util/icon-util.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {IconSize} from "../types";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* A simple function that tells us how many viewport pixels each icon size
|
|
@@ -11,51 +11,3 @@ export const viewportPixelsForSize = (size: IconSize): number =>
|
|
|
11
11
|
large: 48,
|
|
12
12
|
xlarge: 96,
|
|
13
13
|
}[size]);
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* A utility to find the right asset from an IconAsset to display in an icon
|
|
17
|
-
* at a given IconSize. We're looking for, in the following order:
|
|
18
|
-
* 1. The path for the IconSize (e.g. small, medium) requested
|
|
19
|
-
* 2. A path that's _smaller_ than the size we requested
|
|
20
|
-
* 3. Any path (what remains is one for a larger IconSize)
|
|
21
|
-
*
|
|
22
|
-
* The goal here is to provide a path that looks good at the given size...
|
|
23
|
-
* obviously, if the size that we want is provided, we'll use it. Otherwise we'd
|
|
24
|
-
* rather blow up a smaller, simpler icon than scrunch down a more complex one.
|
|
25
|
-
*/
|
|
26
|
-
export const getPathForIcon = (
|
|
27
|
-
icon: IconAsset,
|
|
28
|
-
size: IconSize,
|
|
29
|
-
): {
|
|
30
|
-
assetSize: IconSize;
|
|
31
|
-
path: string;
|
|
32
|
-
} => {
|
|
33
|
-
if (typeof icon[size] === "number") {
|
|
34
|
-
// Great, we have the IconSize we actually requested
|
|
35
|
-
// @ts-expect-error [FEI-5019] - TS2322 - Type 'string | undefined' is not assignable to type 'string'.
|
|
36
|
-
return {assetSize: size, path: icon[size]};
|
|
37
|
-
} else {
|
|
38
|
-
// Oh, no, we don't have the right IconSize! Let's find the next best
|
|
39
|
-
// one...we prefer to find a smaller icon and blow it up instead of
|
|
40
|
-
// using a larger icon and shrinking it such that detail may be lost.
|
|
41
|
-
const desiredPixelSize = viewportPixelsForSize(size);
|
|
42
|
-
const availableSizes = Object.keys(icon);
|
|
43
|
-
const sortFn = (availableSize: IconSize) => {
|
|
44
|
-
const availablePixelSize = viewportPixelsForSize(availableSize);
|
|
45
|
-
const tooLargeByPixels = availablePixelSize - desiredPixelSize;
|
|
46
|
-
return tooLargeByPixels > 0
|
|
47
|
-
? Number.POSITIVE_INFINITY
|
|
48
|
-
: Math.abs(tooLargeByPixels);
|
|
49
|
-
};
|
|
50
|
-
// @ts-expect-error [FEI-5019] - TS2345 - Argument of type 'string' is not assignable to parameter of type 'keyof IconAsset'. | TS2345 - Argument of type 'string' is not assignable to parameter of type 'keyof IconAsset'.
|
|
51
|
-
const assetSizes = availableSizes.sort((a, b) => sortFn(a) - sortFn(b));
|
|
52
|
-
const bestAssetSize = assetSizes[0];
|
|
53
|
-
// @ts-expect-error [FEI-5019] - TS7053 - Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'IconAsset'.
|
|
54
|
-
if (bestAssetSize && icon[bestAssetSize]) {
|
|
55
|
-
// @ts-expect-error [FEI-5019] - TS2322 - Type 'string' is not assignable to type 'keyof IconAsset'. | TS7053 - Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'IconAsset'.
|
|
56
|
-
return {assetSize: bestAssetSize, path: icon[bestAssetSize]};
|
|
57
|
-
} else {
|
|
58
|
-
throw new Error("Icon does not contain any valid asset sizes!");
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
};
|