@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.
- package/CHANGELOG.md +12 -0
- package/dist/components/switch.d.ts +1 -1
- package/dist/es/index.js +176 -81
- package/dist/index.js +175 -84
- package/dist/themes/default.d.ts +62 -0
- package/dist/themes/khanmigo.d.ts +65 -0
- package/dist/themes/themed-switch.d.ts +80 -0
- package/package.json +2 -3
- package/src/components/__tests__/switch.test.tsx +33 -14
- package/src/components/switch.tsx +123 -99
- package/src/themes/default.ts +73 -0
- package/src/themes/khanmigo.ts +28 -0
- package/src/themes/themed-switch.tsx +42 -0
- package/tsconfig-build.json +1 -0
- package/tsconfig-build.tsbuildinfo +1 -1
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
<
|
|
90
|
-
{
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
135
|
+
const themedSharedStyles: ThemedStylesFn<SwitchThemeContract> = (theme) => ({
|
|
132
136
|
hidden: {
|
|
133
137
|
opacity: 0,
|
|
134
|
-
height:
|
|
135
|
-
width:
|
|
138
|
+
height: theme.size.height.none,
|
|
139
|
+
width: theme.size.width.none,
|
|
136
140
|
},
|
|
137
141
|
switch: {
|
|
138
142
|
display: "inline-flex",
|
|
139
|
-
height:
|
|
140
|
-
width:
|
|
141
|
-
borderRadius:
|
|
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:
|
|
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:
|
|
158
|
-
left:
|
|
159
|
-
height:
|
|
160
|
-
width:
|
|
161
|
-
borderRadius:
|
|
162
|
-
backgroundColor:
|
|
163
|
-
transition:
|
|
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:
|
|
168
|
-
left:
|
|
173
|
+
top: theme.spacing.icon.position,
|
|
174
|
+
left: theme.spacing.icon.position,
|
|
169
175
|
zIndex: 1,
|
|
170
|
-
transition:
|
|
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}-${
|
|
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,
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
|
207
|
+
backgroundColor: disabled
|
|
208
|
+
? theme.color.bg.switch.disabledOn
|
|
209
|
+
: theme.color.bg.switch.on,
|
|
196
210
|
":active": {
|
|
197
|
-
backgroundColor:
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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:
|
|
219
|
+
transform: theme.spacing.transform.default,
|
|
211
220
|
},
|
|
212
221
|
icon: {
|
|
213
|
-
color: disabled
|
|
214
|
-
|
|
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
|
|
231
|
+
backgroundColor: disabled
|
|
232
|
+
? theme.color.bg.switch.disabledOff
|
|
233
|
+
: theme.color.bg.switch.off,
|
|
221
234
|
":active": {
|
|
222
|
-
backgroundColor:
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
|
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
|
+
}
|
package/tsconfig-build.json
CHANGED