@khanacademy/wonder-blocks-switch 1.0.6 → 1.1.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.
@@ -1,22 +1,29 @@
1
1
  import * as React from "react";
2
- import {StyleSheet} from "aphrodite";
2
+ import {CSSProperties, StyleSheet} from "aphrodite";
3
3
 
4
4
  import {
5
5
  AriaProps,
6
- UniqueIDProvider,
7
6
  View,
8
7
  addStyle,
8
+ useUniqueIdWithMock,
9
9
  } from "@khanacademy/wonder-blocks-core";
10
- import Color, {mix} from "@khanacademy/wonder-blocks-color";
11
10
  import Icon from "@khanacademy/wonder-blocks-icon";
12
- import Spacing from "@khanacademy/wonder-blocks-spacing";
11
+ import {
12
+ ThemedStylesFn,
13
+ useScopedTheme,
14
+ useStyles,
15
+ } from "@khanacademy/wonder-blocks-theming";
16
+ import ThemedSwitch, {
17
+ SwitchThemeContext,
18
+ SwitchThemeContract,
19
+ } from "../themes/themed-switch";
13
20
 
14
21
  type Props = Pick<
15
22
  AriaProps,
16
23
  "aria-labelledby" | "aria-label" | "aria-describedby"
17
24
  > & {
18
25
  /**
19
- * Whether this compoonent is checked.
26
+ * Whether this component is checked.
20
27
  */
21
28
  checked: boolean;
22
29
  /**
@@ -46,7 +53,7 @@ type Props = Pick<
46
53
  const StyledSpan = addStyle("span");
47
54
  const StyledInput = addStyle("input");
48
55
 
49
- const Switch = React.forwardRef(function Switch(
56
+ const SwitchCore = React.forwardRef(function SwitchCore(
50
57
  props: Props,
51
58
  ref: React.ForwardedRef<HTMLInputElement>,
52
59
  ) {
@@ -62,6 +69,12 @@ const Switch = React.forwardRef(function Switch(
62
69
  testId,
63
70
  } = props;
64
71
 
72
+ const ids = useUniqueIdWithMock("labeled-field");
73
+ const uniqueId = id ?? ids.get("labeled-field-id");
74
+
75
+ const {theme, themeName} = useScopedTheme(SwitchThemeContext);
76
+ const sharedStyles = useStyles(themedSharedStyles, theme);
77
+
65
78
  const handleClick = () => {
66
79
  if (!disabled && onChange) {
67
80
  onChange(!checked);
@@ -71,8 +84,10 @@ const Switch = React.forwardRef(function Switch(
71
84
 
72
85
  const stateStyles = _generateStyles(
73
86
  checked,
74
- disabled,
75
87
  onChange !== undefined,
88
+ disabled,
89
+ theme,
90
+ themeName,
76
91
  );
77
92
 
78
93
  let styledIcon: React.ReactElement<typeof Icon> | undefined;
@@ -81,70 +96,61 @@ const Switch = React.forwardRef(function Switch(
81
96
  size: "small",
82
97
  style: [sharedStyles.icon, stateStyles.icon],
83
98
  "aria-hidden": true,
84
- ...icon.props,
85
99
  } as Partial<React.ComponentProps<typeof Icon>>);
86
100
  }
87
101
 
88
102
  return (
89
- <UniqueIDProvider mockOnFirstRender={true} scope="switch">
90
- {(ids) => {
91
- const uniqueId = id || ids.get("switch");
92
-
93
- return (
94
- <View
95
- onClick={handleClick}
96
- style={[
97
- sharedStyles.switch,
98
- stateStyles.switch,
99
- disabled && sharedStyles.disabled,
100
- ]}
101
- testId={testId}
102
- >
103
- <StyledInput
104
- aria-describedby={ariaDescribedBy}
105
- aria-label={ariaLabel}
106
- aria-labelledby={ariaLabelledBy}
107
- checked={checked}
108
- disabled={disabled}
109
- id={uniqueId}
110
- // Need to specify because this is a controlled React component, but we
111
- // handle the clicks on the outer View
112
- onChange={handleChange}
113
- ref={ref}
114
- role="switch"
115
- // Input is visually hidden because we use a view and span to render
116
- // the actual switch. The input is used for accessibility.
117
- style={sharedStyles.hidden}
118
- type="checkbox"
119
- />
120
- {icon && styledIcon}
121
- <StyledSpan
122
- style={[sharedStyles.slider, stateStyles.slider]}
123
- />
124
- </View>
125
- );
126
- }}
127
- </UniqueIDProvider>
103
+ <View
104
+ onClick={handleClick}
105
+ style={[
106
+ sharedStyles.switch,
107
+ stateStyles.switch,
108
+ disabled && sharedStyles.disabled,
109
+ ]}
110
+ testId={testId}
111
+ >
112
+ <StyledInput
113
+ aria-describedby={ariaDescribedBy}
114
+ aria-label={ariaLabel}
115
+ aria-labelledby={ariaLabelledBy}
116
+ checked={checked}
117
+ disabled={disabled}
118
+ id={uniqueId}
119
+ // Need to specify because this is a controlled React component, but we
120
+ // handle the clicks on the outer View
121
+ onChange={handleChange}
122
+ ref={ref}
123
+ role="switch"
124
+ // Input is visually hidden because we use a view and span to render
125
+ // the actual switch. The input is used for accessibility.
126
+ style={sharedStyles.hidden}
127
+ type="checkbox"
128
+ />
129
+ {icon && styledIcon}
130
+ <StyledSpan style={[sharedStyles.slider, stateStyles.slider]} />
131
+ </View>
128
132
  );
129
133
  });
130
134
 
131
- const sharedStyles = StyleSheet.create({
135
+ const themedSharedStyles: ThemedStylesFn<SwitchThemeContract> = (theme) => ({
132
136
  hidden: {
133
137
  opacity: 0,
134
- height: 0,
135
- width: 0,
138
+ height: theme.size.height.none,
139
+ width: theme.size.width.none,
136
140
  },
137
141
  switch: {
138
142
  display: "inline-flex",
139
- height: Spacing.large_24,
140
- width: `calc(${Spacing.xLarge_32}px + ${Spacing.xSmall_8}px)`,
141
- borderRadius: Spacing.small_12,
143
+ height: theme.size.height.large,
144
+ width: theme.size.width.large,
145
+ borderRadius: theme.border.radius.small,
142
146
  flexShrink: 0,
143
- cursor: "pointer",
144
147
  ":hover": {
145
- outlineOffset: 1,
148
+ outlineOffset: theme.size.offset.default,
149
+ },
150
+ ":focus-within": {
151
+ outline: `solid ${theme.size.width.small}px ${theme.color.outline.default}`,
152
+ outlineOffset: theme.size.offset.default,
146
153
  },
147
- transition: "background-color 0.15s ease-in-out",
148
154
  },
149
155
  disabled: {
150
156
  cursor: "auto",
@@ -154,85 +160,92 @@ const sharedStyles = StyleSheet.create({
154
160
  },
155
161
  slider: {
156
162
  position: "absolute",
157
- top: Spacing.xxxxSmall_2,
158
- left: Spacing.xxxxSmall_2,
159
- height: `calc(${Spacing.medium_16}px + ${Spacing.xxxSmall_4}px)`,
160
- width: `calc(${Spacing.medium_16}px + ${Spacing.xxxSmall_4}px)`,
161
- borderRadius: "50%",
162
- backgroundColor: Color.white,
163
- transition: "transform 0.15s ease-in-out",
163
+ top: theme.spacing.slider.position,
164
+ left: theme.spacing.slider.position,
165
+ height: theme.size.height.medium,
166
+ width: theme.size.width.medium,
167
+ borderRadius: theme.border.radius.full,
168
+ backgroundColor: theme.color.bg.slider.on,
169
+ transition: theme.spacing.transform.transition,
164
170
  },
165
171
  icon: {
166
172
  position: "absolute",
167
- top: Spacing.xxxSmall_4,
168
- left: Spacing.xxxSmall_4,
173
+ top: theme.spacing.icon.position,
174
+ left: theme.spacing.icon.position,
169
175
  zIndex: 1,
170
- transition: "0.15s ease-in-out",
171
- transitionProperty: "transform, color",
176
+ transition: theme.spacing.transform.transition,
172
177
  },
173
178
  });
174
179
 
175
180
  const styles: Record<string, any> = {};
176
181
  const _generateStyles = (
177
182
  checked: boolean,
178
- disabled: boolean,
179
183
  clickable: boolean,
184
+ disabled: boolean,
185
+ theme: SwitchThemeContract,
186
+ themeName: string,
180
187
  ) => {
181
- const checkedStyle = `${checked}-${disabled}-${clickable}`;
188
+ const checkedStyle = `${checked}-${clickable}-${disabled}-${themeName}`;
182
189
  // The styles are cached to avoid creating a new object on every render.
183
190
  if (styles[checkedStyle]) {
184
191
  return styles[checkedStyle];
185
192
  }
186
193
 
187
- let newStyles: Record<string, any> = {};
188
-
189
- const disabledBlue = mix(Color.blue, Color.offBlack50);
190
- const activeBlue = mix(Color.offBlack32, Color.blue);
194
+ let newStyles: Record<string, CSSProperties> = {};
195
+ const sharedSwitchStyles = {
196
+ cursor: clickable ? "pointer" : "auto",
197
+ ":hover": {
198
+ outline: clickable
199
+ ? `solid ${theme.size.width.small}px ${theme.color.outline.default}`
200
+ : "none",
201
+ },
202
+ };
191
203
 
192
204
  if (checked) {
193
205
  newStyles = {
194
206
  switch: {
195
- backgroundColor: disabled ? disabledBlue : Color.blue,
207
+ backgroundColor: disabled
208
+ ? theme.color.bg.switch.disabledOn
209
+ : theme.color.bg.switch.on,
196
210
  ":active": {
197
- backgroundColor: !disabled && clickable && activeBlue,
198
- },
199
- ":focus-within": {
200
- outline: `solid ${Spacing.xxxxSmall_2}px ${Color.blue}`,
201
- outlineOffset: 1,
202
- },
203
- ":hover": {
204
- outline: clickable
205
- ? `solid ${Spacing.xxxxSmall_2}px ${Color.blue}`
206
- : "none",
211
+ backgroundColor:
212
+ !disabled && clickable
213
+ ? theme.color.bg.switch.activeOn
214
+ : undefined,
207
215
  },
216
+ ...sharedSwitchStyles,
208
217
  },
209
218
  slider: {
210
- transform: `translateX(${Spacing.medium_16}px)`,
219
+ transform: theme.spacing.transform.default,
211
220
  },
212
221
  icon: {
213
- color: disabled ? disabledBlue : Color.blue,
214
- transform: `translateX(${Spacing.medium_16}px)`,
222
+ color: disabled
223
+ ? theme.color.bg.icon.disabledOn
224
+ : theme.color.bg.icon.on,
225
+ transform: theme.spacing.transform.default,
215
226
  },
216
227
  };
217
228
  } else {
218
229
  newStyles = {
219
230
  switch: {
220
- backgroundColor: disabled ? Color.offBlack32 : Color.offBlack50,
231
+ backgroundColor: disabled
232
+ ? theme.color.bg.switch.disabledOff
233
+ : theme.color.bg.switch.off,
221
234
  ":active": {
222
- backgroundColor: !disabled && clickable && Color.offBlack64,
223
- },
224
- ":focus-within": {
225
- outline: `solid ${Spacing.xxxxSmall_2}px ${Color.blue}`,
226
- outlineOffset: 1,
227
- },
228
- ":hover": {
229
- outline: clickable
230
- ? `solid ${Spacing.xxxxSmall_2}px ${Color.blue}`
231
- : "none",
235
+ backgroundColor:
236
+ !disabled && clickable
237
+ ? theme.color.bg.switch.activeOff
238
+ : undefined,
232
239
  },
240
+ ...sharedSwitchStyles,
241
+ },
242
+ slider: {
243
+ backgroundColor: theme.color.bg.slider.off,
233
244
  },
234
245
  icon: {
235
- color: disabled ? Color.offBlack32 : Color.offBlack50,
246
+ color: disabled
247
+ ? theme.color.bg.icon.disabledOff
248
+ : theme.color.bg.icon.off,
236
249
  },
237
250
  };
238
251
  }
@@ -241,6 +254,17 @@ const _generateStyles = (
241
254
  return styles[checkedStyle];
242
255
  };
243
256
 
257
+ const Switch = React.forwardRef(function Switch(
258
+ props: Props,
259
+ ref: React.ForwardedRef<HTMLInputElement>,
260
+ ) {
261
+ return (
262
+ <ThemedSwitch>
263
+ <SwitchCore {...props} ref={ref} />
264
+ </ThemedSwitch>
265
+ );
266
+ });
267
+
244
268
  Switch.displayName = "Switch";
245
269
 
246
270
  export default Switch;
@@ -0,0 +1,73 @@
1
+ import {tokens} from "@khanacademy/wonder-blocks-theming";
2
+
3
+ const theme = {
4
+ color: {
5
+ bg: {
6
+ switch: {
7
+ off: tokens.color.offBlack50,
8
+ disabledOff: tokens.color.offBlack32,
9
+ activeOff: tokens.color.offBlack64,
10
+ on: tokens.color.blue,
11
+ disabledOn: tokens.color.fadedBlue,
12
+ activeOn: tokens.color.activeBlue,
13
+ },
14
+ slider: {
15
+ on: tokens.color.white,
16
+ off: tokens.color.white,
17
+ },
18
+ icon: {
19
+ on: tokens.color.blue,
20
+ disabledOn: tokens.color.fadedBlue,
21
+ off: tokens.color.offBlack50,
22
+ disabledOff: tokens.color.offBlack32,
23
+ },
24
+ },
25
+ outline: {
26
+ default: tokens.color.blue,
27
+ },
28
+ },
29
+ border: {
30
+ radius: {
31
+ // slider
32
+ small: tokens.spacing.small_12,
33
+ // switch
34
+ full: tokens.border.radius.full,
35
+ },
36
+ },
37
+ size: {
38
+ height: {
39
+ none: 0,
40
+ // switch
41
+ medium: 20,
42
+ // slider
43
+ large: tokens.spacing.large_24,
44
+ },
45
+ width: {
46
+ none: 0,
47
+ small: tokens.spacing.xxxxSmall_2,
48
+ // NOTE: This token is specific to the Switch component
49
+ // switch
50
+ medium: 20,
51
+ // NOTE: This token is specific to the Switch component
52
+ // slider
53
+ large: 40,
54
+ },
55
+ offset: {
56
+ default: 1,
57
+ },
58
+ },
59
+ spacing: {
60
+ slider: {
61
+ position: tokens.spacing.xxxxSmall_2,
62
+ },
63
+ icon: {
64
+ position: tokens.spacing.xxxSmall_4,
65
+ },
66
+ transform: {
67
+ default: `translateX(${tokens.spacing.medium_16}px)`,
68
+ transition: "transform 0.15s ease-in-out",
69
+ },
70
+ },
71
+ };
72
+
73
+ export default theme;
@@ -0,0 +1,28 @@
1
+ import {mergeTheme, tokens} from "@khanacademy/wonder-blocks-theming";
2
+ import defaultTheme from "./default";
3
+
4
+ /**
5
+ * The overrides for khanmigo theme for a switch.
6
+ */
7
+ const theme = mergeTheme(defaultTheme, {
8
+ color: {
9
+ bg: {
10
+ switch: {
11
+ off: tokens.color.white50,
12
+ disabledOff: tokens.color.white32,
13
+ activeOff: tokens.color.white50,
14
+ disabledOn: tokens.color.activeBlue,
15
+ },
16
+ slider: {
17
+ off: tokens.color.eggplant,
18
+ },
19
+ icon: {
20
+ off: tokens.color.white,
21
+ disabledOff: tokens.color.white50,
22
+ disabledOn: tokens.color.activeBlue,
23
+ },
24
+ },
25
+ },
26
+ });
27
+
28
+ export default theme;
@@ -0,0 +1,42 @@
1
+ import * as React from "react";
2
+ import {
3
+ createThemeContext,
4
+ ThemeSwitcherContext,
5
+ } from "@khanacademy/wonder-blocks-theming";
6
+
7
+ import defaultTheme from "./default";
8
+ import khanmigoTheme from "./khanmigo";
9
+
10
+ type Props = {
11
+ children: React.ReactNode;
12
+ };
13
+
14
+ /**
15
+ * The themes available to the Switch component.
16
+ */
17
+ const themes = {
18
+ default: defaultTheme,
19
+ khanmigo: khanmigoTheme,
20
+ };
21
+
22
+ export type SwitchThemeContract = typeof defaultTheme;
23
+
24
+ /**
25
+ * The context that provides the theme to the Switch component.
26
+ * This is generally consumed via the `useScopedTheme` hook.
27
+ */
28
+ export const SwitchThemeContext = createThemeContext(defaultTheme);
29
+
30
+ /**
31
+ * ThemedSwitch is a component that provides a theme to the <Switch/> component.
32
+ */
33
+ export default function ThemedSwitch(props: Props) {
34
+ const currentTheme = React.useContext(ThemeSwitcherContext);
35
+
36
+ const theme = themes[currentTheme as keyof typeof themes] || defaultTheme;
37
+ return (
38
+ <SwitchThemeContext.Provider value={theme}>
39
+ {props.children}
40
+ </SwitchThemeContext.Provider>
41
+ );
42
+ }
@@ -10,5 +10,6 @@
10
10
  {"path": "../wonder-blocks-core/tsconfig-build.json"},
11
11
  {"path": "../wonder-blocks-icon/tsconfig-build.json"},
12
12
  {"path": "../wonder-blocks-spacing/tsconfig-build.json"},
13
+ {"path": "../wonder-blocks-theming/tsconfig-build.json"},
13
14
  ]
14
15
  }