@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.
@@ -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
- // Invalid: small + regular
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
- // Invalid: medium + bold
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 CommonProps = Pick<AriaProps, "aria-hidden" | "aria-label"> & {
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
- * The icon to display. This is a reference to the icon asset
47
- * (imported as a static SVG file).
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
- icon: PhosphorIconSmall;
51
- };
36
+ size?: IconSize;
52
37
 
53
- type PropsForMediumIcon = CommonProps & {
54
38
  /**
55
- * The icon size (24px). Defaults to `medium`.
39
+ * The icon to display. This is a reference to the icon asset (imported as a
40
+ * static SVG file).
56
41
  *
57
- * __NOTE:__ medium icons only support `regular` and `fill` weights. **Make
58
- * sure you are not using a `bold` icon.**
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
- size?: "medium";
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
- * The different icon weights for small icons.
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 PhosphorIconMedium = PhosphorRegular | PhosphorFill;
9
+ export type IconSize = "small" | "medium" | "large" | "xlarge";
@@ -1,83 +1,4 @@
1
- import {getPathForIcon, viewportPixelsForSize} from "./icon-util";
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", () => {
@@ -1,4 +1,4 @@
1
- import type {IconAsset, IconSize} from "./icon-assets";
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
- };