@khanacademy/wonder-blocks-icon-button 4.1.9 → 4.2.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.
@@ -4,6 +4,7 @@ import * as renderer from "react-test-renderer";
4
4
  import {icons} from "@khanacademy/wonder-blocks-icon";
5
5
 
6
6
  import IconButtonCore from "../components/icon-button-core";
7
+ import {IconButtonSize} from "../components/icon-button";
7
8
 
8
9
  const defaultHandlers = {
9
10
  onClick: () => void 0,
@@ -23,7 +24,11 @@ const defaultHandlers = {
23
24
  describe("IconButtonCore", () => {
24
25
  for (const kind of ["primary", "secondary", "tertiary"] as const) {
25
26
  for (const color of ["default", "destructive"] as const) {
26
- for (const size of ["default", "small"] as const) {
27
+ for (const size of [
28
+ "xsmall",
29
+ "small",
30
+ "medium",
31
+ ] as IconButtonSize[]) {
27
32
  for (const light of kind === "primary"
28
33
  ? [true, false]
29
34
  : [false]) {
@@ -52,6 +57,7 @@ describe("IconButtonCore", () => {
52
57
  kind={kind}
53
58
  color={color}
54
59
  light={light}
60
+ size={size}
55
61
  tabIndex={disabled ? -1 : 0}
56
62
  {...stateProps}
57
63
  {...defaultHandlers}
@@ -16,7 +16,11 @@ import type {
16
16
  ChildrenProps,
17
17
  ClickableState,
18
18
  } from "@khanacademy/wonder-blocks-clickable";
19
- import type {SharedProps} from "./icon-button";
19
+ import type {IconButtonSize, SharedProps} from "./icon-button";
20
+ import {
21
+ iconSizeForButtonSize,
22
+ targetPixelsForSize,
23
+ } from "../util/icon-button-util";
20
24
 
21
25
  type Props = SharedProps &
22
26
  ChildrenProps &
@@ -42,7 +46,6 @@ const IconButtonCore: React.ForwardRefExoticComponent<
42
46
  Props
43
47
  >(function IconButtonCore(props: Props, ref) {
44
48
  const {
45
- skipClientNav,
46
49
  color,
47
50
  disabled,
48
51
  focused,
@@ -52,6 +55,8 @@ const IconButtonCore: React.ForwardRefExoticComponent<
52
55
  kind = "primary",
53
56
  light = false,
54
57
  pressed,
58
+ size = "medium",
59
+ skipClientNav,
55
60
  style,
56
61
  testId,
57
62
  waiting: _,
@@ -64,11 +69,10 @@ const IconButtonCore: React.ForwardRefExoticComponent<
64
69
  ? SemanticColor.controlDestructive
65
70
  : SemanticColor.controlDefault;
66
71
 
67
- const buttonStyles = _generateStyles(buttonColor, kind, light);
72
+ const buttonStyles = _generateStyles(buttonColor, kind, light, size);
68
73
 
69
74
  const defaultStyle = [
70
75
  sharedStyles.shared,
71
- disabled && sharedStyles.disabled,
72
76
  buttonStyles.default,
73
77
  disabled && buttonStyles.disabled,
74
78
  !disabled &&
@@ -77,7 +81,13 @@ const IconButtonCore: React.ForwardRefExoticComponent<
77
81
  : (hovered || focused) && buttonStyles.focus),
78
82
  ];
79
83
 
80
- const child = <Icon size="medium" color="currentColor" icon={icon} />;
84
+ const child = (
85
+ <Icon
86
+ size={iconSizeForButtonSize(size)}
87
+ color="currentColor"
88
+ icon={icon}
89
+ />
90
+ );
81
91
 
82
92
  const commonProps = {
83
93
  "data-test-id": testId,
@@ -133,8 +143,6 @@ const sharedStyles = StyleSheet.create({
133
143
  alignItems: "center",
134
144
  justifyContent: "center",
135
145
  boxSizing: "border-box",
136
- height: 40,
137
- width: 40,
138
146
  padding: 0,
139
147
  cursor: "pointer",
140
148
  border: "none",
@@ -150,9 +158,6 @@ const sharedStyles = StyleSheet.create({
150
158
  WebkitTapHighlightColor: "rgba(0,0,0,0)",
151
159
  },
152
160
  },
153
- disabled: {
154
- cursor: "default",
155
- },
156
161
  });
157
162
 
158
163
  const styles: Record<string, any> = {};
@@ -161,8 +166,9 @@ const _generateStyles = (
161
166
  color: string,
162
167
  kind: "primary" | "secondary" | "tertiary",
163
168
  light: boolean,
169
+ size: IconButtonSize,
164
170
  ) => {
165
- const buttonType = color + kind + light.toString();
171
+ const buttonType = `${color}-${kind}-${light}-${size}`;
166
172
  if (styles[buttonType]) {
167
173
  return styles[buttonType];
168
174
  }
@@ -172,9 +178,26 @@ const _generateStyles = (
172
178
  }
173
179
 
174
180
  const {white, offBlack32, offBlack64, offBlack} = Color;
181
+ const defaultColor = ((): string => {
182
+ switch (kind) {
183
+ case "primary":
184
+ return light ? white : color;
185
+ case "secondary":
186
+ return offBlack;
187
+ case "tertiary":
188
+ return offBlack64;
189
+ default:
190
+ throw new Error("IconButton kind not recognized");
191
+ }
192
+ })();
193
+ const pixelsForSize = targetPixelsForSize(size);
175
194
 
176
195
  const newStyles = {
177
- default: {},
196
+ default: {
197
+ height: pixelsForSize,
198
+ width: pixelsForSize,
199
+ color: defaultColor,
200
+ },
178
201
  focus: {
179
202
  color: light ? white : color,
180
203
  borderWidth: 2,
@@ -198,24 +221,6 @@ const _generateStyles = (
198
221
  cursor: "default",
199
222
  },
200
223
  } as const;
201
- if (kind === "primary") {
202
- // @ts-expect-error [FEI-5019] - TS2540 - Cannot assign to 'default' because it is a read-only property.
203
- newStyles["default"] = {
204
- color: light ? white : color,
205
- };
206
- } else if (kind === "secondary") {
207
- // @ts-expect-error [FEI-5019] - TS2540 - Cannot assign to 'default' because it is a read-only property.
208
- newStyles["default"] = {
209
- color: offBlack,
210
- };
211
- } else if (kind === "tertiary") {
212
- // @ts-expect-error [FEI-5019] - TS2540 - Cannot assign to 'default' because it is a read-only property.
213
- newStyles["default"] = {
214
- color: offBlack64,
215
- };
216
- } else {
217
- throw new Error("IconButton kind not recognized");
218
- }
219
224
 
220
225
  styles[buttonType] = StyleSheet.create(newStyles);
221
226
  return styles[buttonType];
@@ -7,6 +7,8 @@ import type {AriaProps, StyleType} from "@khanacademy/wonder-blocks-core";
7
7
  import {Link} from "react-router-dom";
8
8
  import IconButtonCore from "./icon-button-core";
9
9
 
10
+ export type IconButtonSize = "xsmall" | "small" | "medium";
11
+
10
12
  export type SharedProps = Partial<Omit<AriaProps, "aria-disabled">> & {
11
13
  /**
12
14
  * A Wonder Blocks icon asset, an object specifing paths for one or more of
@@ -40,6 +42,12 @@ export type SharedProps = Partial<Omit<AriaProps, "aria-disabled">> & {
40
42
  * Test ID used for e2e testing.
41
43
  */
42
44
  testId?: string;
45
+ /**
46
+ * Size of the icon button.
47
+ * One of `xsmall` (16 icon, 20 target), `small` (24, 32), or `medium (24, 40).
48
+ * Defaults to `medium`.
49
+ */
50
+ size?: IconButtonSize;
43
51
  /**
44
52
  * Optional custom styles.
45
53
  */
@@ -158,15 +166,16 @@ const IconButton: React.ForwardRefExoticComponent<
158
166
  SharedProps
159
167
  >(function IconButton(props: SharedProps, ref) {
160
168
  const {
161
- onClick,
169
+ color = "default",
170
+ disabled = false,
162
171
  href,
172
+ kind = "primary",
173
+ light = false,
174
+ onClick,
175
+ size = "medium",
163
176
  skipClientNav,
164
177
  tabIndex,
165
178
  target,
166
- color = "default",
167
- kind = "primary",
168
- light = false,
169
- disabled = false,
170
179
  ...sharedProps
171
180
  } = props;
172
181
  const renderClickableBehavior = (router: any): React.ReactNode => {
@@ -191,14 +200,15 @@ const IconButton: React.ForwardRefExoticComponent<
191
200
  {...state}
192
201
  {...childrenProps}
193
202
  color={color}
203
+ disabled={disabled}
204
+ href={href}
194
205
  kind={kind}
195
206
  light={light}
196
- disabled={disabled}
207
+ ref={ref}
197
208
  skipClientNav={skipClientNav}
198
- href={href}
209
+ size={size}
199
210
  target={target}
200
211
  tabIndex={tabIndex}
201
- ref={ref}
202
212
  />
203
213
  );
204
214
  }}
@@ -0,0 +1,29 @@
1
+ import {iconSizeForButtonSize, targetPixelsForSize} from "./icon-button-util";
2
+
3
+ describe("iconSizeForButtonSize", () => {
4
+ test.each`
5
+ buttonSize | expectedIconSize
6
+ ${"xsmall"} | ${"small"}
7
+ ${"small"} | ${"medium"}
8
+ ${"medium"} | ${"medium"}
9
+ `(
10
+ "should return $expectedIconSize icon for $buttonSize icon button",
11
+ ({buttonSize, expectedIconSize}) => {
12
+ expect(iconSizeForButtonSize(buttonSize)).toBe(expectedIconSize);
13
+ },
14
+ );
15
+ });
16
+
17
+ describe("targetPixelsForSize", () => {
18
+ test.each`
19
+ size | expectedTargetPixels
20
+ ${"xsmall"} | ${24}
21
+ ${"small"} | ${32}
22
+ ${"medium"} | ${40}
23
+ `(
24
+ "should return $expectedTargetPixels for $size icon button",
25
+ ({size, expectedTargetPixels}) => {
26
+ expect(targetPixelsForSize(size)).toBe(expectedTargetPixels);
27
+ },
28
+ );
29
+ });
@@ -0,0 +1,22 @@
1
+ import {IconSize} from "@khanacademy/wonder-blocks-icon";
2
+ import {IconButtonSize} from "../components/icon-button";
3
+
4
+ /**
5
+ * A function that returns the icon size for a given icon button size.
6
+ */
7
+ export const iconSizeForButtonSize = (size: IconButtonSize): IconSize => {
8
+ switch (size) {
9
+ case "xsmall":
10
+ return "small";
11
+ case "small":
12
+ return "medium";
13
+ case "medium":
14
+ return "medium";
15
+ }
16
+ };
17
+
18
+ /**
19
+ * A function that returns the size of the touch target in pixels for a given icon button size.
20
+ */
21
+ export const targetPixelsForSize = (size: IconButtonSize): number =>
22
+ ({xsmall: 24, small: 32, medium: 40}[size]);