@khanacademy/wonder-blocks-cell 1.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.
@@ -0,0 +1,288 @@
1
+ // @flow
2
+ import * as React from "react";
3
+ import {StyleSheet} from "aphrodite";
4
+
5
+ import Clickable from "@khanacademy/wonder-blocks-clickable";
6
+ import {View} from "@khanacademy/wonder-blocks-core";
7
+ import Color, {fade} from "@khanacademy/wonder-blocks-color";
8
+ import {Strut} from "@khanacademy/wonder-blocks-layout";
9
+ import Spacing from "@khanacademy/wonder-blocks-spacing";
10
+
11
+ import type {ClickableState} from "@khanacademy/wonder-blocks-clickable";
12
+ import {CellMeasurements, getHorizontalRuleStyles} from "./common.js";
13
+
14
+ import type {CellProps, TypographyText} from "../../util/types.js";
15
+
16
+ type LeftAccessoryProps = {|
17
+ leftAccessory?: CellProps["leftAccessory"],
18
+ leftAccessoryStyle?: CellProps["leftAccessoryStyle"],
19
+ disabled?: CellProps["disabled"],
20
+ |};
21
+
22
+ /**
23
+ * Left Accessories can be defined using WB components such as Icon, IconButton,
24
+ * or it can even be used for a custom node/component if needed.
25
+ */
26
+ const LeftAccessory = ({
27
+ leftAccessory,
28
+ leftAccessoryStyle,
29
+ disabled,
30
+ }: LeftAccessoryProps): React.Node => {
31
+ if (!leftAccessory) {
32
+ return null;
33
+ }
34
+
35
+ return (
36
+ <>
37
+ <View
38
+ style={[
39
+ styles.accessory,
40
+ disabled && styles.accessoryDisabled,
41
+ {...leftAccessoryStyle},
42
+ ]}
43
+ >
44
+ {leftAccessory}
45
+ </View>
46
+ <Strut size={CellMeasurements.accessoryHorizontalSpacing} />
47
+ </>
48
+ );
49
+ };
50
+
51
+ type RightAccessoryProps = {|
52
+ rightAccessory?: CellProps["rightAccessory"],
53
+ rightAccessoryStyle?: CellProps["rightAccessoryStyle"],
54
+ active?: CellProps["active"],
55
+ disabled?: CellProps["disabled"],
56
+ |};
57
+
58
+ /**
59
+ * Right Accessories can be defined using WB components such as Icon,
60
+ * IconButton, or it can even be used for a custom node/component if needed.
61
+ */
62
+ const RightAccessory = ({
63
+ rightAccessory,
64
+ rightAccessoryStyle,
65
+ active,
66
+ disabled,
67
+ }: RightAccessoryProps): React.Node => {
68
+ if (!rightAccessory) {
69
+ return null;
70
+ }
71
+
72
+ return (
73
+ <>
74
+ <Strut size={CellMeasurements.accessoryHorizontalSpacing} />
75
+ <View
76
+ style={[
77
+ styles.accessory,
78
+ styles.accessoryRight,
79
+ disabled && styles.accessoryDisabled,
80
+ {...rightAccessoryStyle},
81
+ active && styles.accessoryActive,
82
+ ]}
83
+ >
84
+ {rightAccessory}
85
+ </View>
86
+ </>
87
+ );
88
+ };
89
+
90
+ type CellCoreProps = {|
91
+ ...$Rest<CellProps, {|title: TypographyText|}>,
92
+
93
+ /**
94
+ * The content of the cell.
95
+ */
96
+ children: React.Node,
97
+ |};
98
+
99
+ /**
100
+ * CellCore is the base cell wrapper. It's used as the skeleton/layout that is
101
+ * used by BasicCell and DetailCell (and any other variants).
102
+ *
103
+ * Both variants share how they render their accessories, and the main
104
+ * responsibility of this component is to render the contents that are passed in
105
+ * (using the `children` prop).
106
+ */
107
+ const CellCore = (props: CellCoreProps): React.Node => {
108
+ const {
109
+ active,
110
+ children,
111
+ disabled,
112
+ horizontalRule = "inset",
113
+ leftAccessory = undefined,
114
+ leftAccessoryStyle = undefined,
115
+ onClick,
116
+ rightAccessory = undefined,
117
+ rightAccessoryStyle = undefined,
118
+ style,
119
+ testId,
120
+ "aria-label": ariaLabel,
121
+ } = props;
122
+
123
+ const renderCell = (eventState?: ClickableState): React.Node => {
124
+ const horizontalRuleStyles = getHorizontalRuleStyles(horizontalRule);
125
+
126
+ return (
127
+ <View
128
+ style={[
129
+ styles.wrapper,
130
+ // focused applied to the main wrapper to make the border
131
+ // outline part of the wrapper
132
+ eventState?.focused && styles.focused,
133
+ // custom styles
134
+ style,
135
+ ]}
136
+ aria-current={active ? "true" : undefined}
137
+ >
138
+ <View
139
+ style={[
140
+ styles.innerWrapper,
141
+ horizontalRuleStyles,
142
+ disabled && styles.disabled,
143
+ active && styles.active,
144
+ // other states applied to the inner wrapper to blend
145
+ // the background color properly
146
+ !disabled && eventState?.hovered && styles.hovered,
147
+ // active + hovered
148
+ active && eventState?.hovered && styles.activeHovered,
149
+ !disabled && eventState?.pressed && styles.pressed,
150
+ // active + pressed
151
+ !disabled &&
152
+ active &&
153
+ eventState?.pressed &&
154
+ styles.activePressed,
155
+ ]}
156
+ >
157
+ {/* Left accessory */}
158
+ <LeftAccessory
159
+ leftAccessory={leftAccessory}
160
+ leftAccessoryStyle={leftAccessoryStyle}
161
+ disabled={disabled}
162
+ />
163
+
164
+ {/* Cell contents */}
165
+ <View style={styles.content} testId={testId}>
166
+ {children}
167
+ </View>
168
+
169
+ {/* Right accessory */}
170
+ <RightAccessory
171
+ rightAccessory={rightAccessory}
172
+ rightAccessoryStyle={rightAccessoryStyle}
173
+ active={active}
174
+ disabled={disabled}
175
+ />
176
+ </View>
177
+ </View>
178
+ );
179
+ };
180
+
181
+ // Pressable cell.
182
+ if (onClick) {
183
+ return (
184
+ <Clickable
185
+ disabled={disabled}
186
+ onClick={onClick}
187
+ hideDefaultFocusRing={true}
188
+ aria-label={ariaLabel ? ariaLabel : undefined}
189
+ >
190
+ {(eventState) => renderCell(eventState)}
191
+ </Clickable>
192
+ );
193
+ }
194
+
195
+ // No click event attached, so just render the cell as-is.
196
+ return renderCell();
197
+ };
198
+
199
+ const styles = StyleSheet.create({
200
+ wrapper: {
201
+ background: Color.white,
202
+ color: Color.offBlack,
203
+ minHeight: CellMeasurements.cellMinHeight,
204
+ textAlign: "left",
205
+ },
206
+
207
+ innerWrapper: {
208
+ padding: `${CellMeasurements.cellPadding.paddingVertical}px ${CellMeasurements.cellPadding.paddingHorizontal}px`,
209
+ flexDirection: "row",
210
+ flex: 1,
211
+ },
212
+
213
+ content: {
214
+ alignSelf: "center",
215
+ overflowWrap: "break-word",
216
+ padding: `${CellMeasurements.contentVerticalSpacing}px 0`,
217
+ },
218
+
219
+ accessory: {
220
+ // Use content width by default.
221
+ minWidth: "auto",
222
+ // Horizontal alignment of the accessory.
223
+ alignItems: "center",
224
+ // Vertical alignment.
225
+ alignSelf: "center",
226
+ },
227
+
228
+ accessoryRight: {
229
+ // The right accessory will have this color by default. Unless the
230
+ // accessory element overrides that color internally.
231
+ color: Color.offBlack64,
232
+ // Align the right accessory to the right side of the cell, so we can
233
+ // prevent the accessory from shifting left, if the content is too
234
+ // short.
235
+ marginLeft: "auto",
236
+ },
237
+
238
+ /**
239
+ * States
240
+ */
241
+ hovered: {
242
+ background: Color.offBlack8,
243
+ },
244
+
245
+ // Handling the focus ring internally because clickable doesn't support
246
+ // rounded focus ring.
247
+ focused: {
248
+ borderRadius: Spacing.xxxSmall_4,
249
+ outline: `solid ${Spacing.xxxxSmall_2}px ${Color.blue}`,
250
+ // The focus ring is not visible when there are stacked cells.
251
+ // Using outlineOffset to display the focus ring inside the cell.
252
+ outlineOffset: -Spacing.xxxxSmall_2,
253
+ // To hide the internal corners of the cell.
254
+ overflow: "hidden",
255
+ },
256
+
257
+ pressed: {
258
+ background: Color.offBlack16,
259
+ },
260
+
261
+ active: {
262
+ background: fade(Color.blue, 0.08),
263
+ color: Color.blue,
264
+ },
265
+
266
+ activeHovered: {
267
+ background: fade(Color.blue, 0.16),
268
+ },
269
+
270
+ activePressed: {
271
+ background: fade(Color.blue, 0.24),
272
+ },
273
+
274
+ disabled: {
275
+ color: Color.offBlack32,
276
+ },
277
+
278
+ accessoryActive: {
279
+ color: Color.blue,
280
+ },
281
+
282
+ accessoryDisabled: {
283
+ color: Color.offBlack,
284
+ opacity: 0.32,
285
+ },
286
+ });
287
+
288
+ export default CellCore;
@@ -0,0 +1,73 @@
1
+ // @flow
2
+ import {StyleSheet} from "aphrodite";
3
+
4
+ import Color from "@khanacademy/wonder-blocks-color";
5
+ import Spacing from "@khanacademy/wonder-blocks-spacing";
6
+
7
+ import type {StyleType} from "@khanacademy/wonder-blocks-core";
8
+ import type {HorizontalRuleVariant} from "../../util/types.js";
9
+
10
+ export const CellMeasurements = {
11
+ cellMinHeight: Spacing.xxLarge_48,
12
+
13
+ /**
14
+ * The cell wrapper's gap.
15
+ */
16
+ cellPadding: {
17
+ paddingVertical: Spacing.xSmall_8,
18
+ paddingHorizontal: Spacing.medium_16,
19
+ },
20
+
21
+ /**
22
+ * The extra vertical spacing added to the title/content wrapper.
23
+ */
24
+ contentVerticalSpacing: Spacing.xxxSmall_4,
25
+
26
+ /**
27
+ * The horizontal spacing between the left and right accessory.
28
+ */
29
+ accessoryHorizontalSpacing: Spacing.medium_16,
30
+ };
31
+
32
+ /**
33
+ * Gets the horizontalRule style based on the variant.
34
+ * @param {HorizontalRuleVariant} horizontalRule The variant of the horizontal
35
+ * rule.
36
+ * @returns A styled horizontal rule.
37
+ */
38
+ export const getHorizontalRuleStyles = (
39
+ horizontalRule: HorizontalRuleVariant,
40
+ ): StyleType => {
41
+ switch (horizontalRule) {
42
+ case "inset":
43
+ return [styles.horizontalRule, styles.horizontalRuleInset];
44
+ case "full-width":
45
+ return styles.horizontalRule;
46
+ case "none":
47
+ return {};
48
+ }
49
+ };
50
+
51
+ const styles = StyleSheet.create({
52
+ horizontalRule: {
53
+ position: "relative",
54
+ ":after": {
55
+ width: "100%",
56
+ content: "''",
57
+ position: "absolute",
58
+ // align to the bottom of the cell
59
+ bottom: 0,
60
+ // align border to the right of the cell
61
+ right: 0,
62
+ height: Spacing.xxxxSmall_2,
63
+ boxShadow: `inset 0px -1px 0px ${Color.offBlack8}`,
64
+ },
65
+ },
66
+
67
+ horizontalRuleInset: {
68
+ ":after": {
69
+ // Inset doesn't include the left padding of the cell.
70
+ width: `calc(100% - ${CellMeasurements.cellPadding.paddingHorizontal}px)`,
71
+ },
72
+ },
73
+ });
package/src/index.js ADDED
@@ -0,0 +1,5 @@
1
+ // @flow
2
+ import BasicCell from "./components/basic-cell.js";
3
+ import DetailCell from "./components/detail-cell.js";
4
+
5
+ export {BasicCell, DetailCell};
@@ -0,0 +1,129 @@
1
+ // @flow
2
+ import * as React from "react";
3
+
4
+ import type {StyleType} from "@khanacademy/wonder-blocks-core";
5
+ import type {Typography} from "@khanacademy/wonder-blocks-typography";
6
+
7
+ /**
8
+ * A set of values that can be used to configure the horizontal rule appearance.
9
+ */
10
+ export type HorizontalRuleVariant = "full-width" | "inset" | "none";
11
+
12
+ /**
13
+ * An element or component that represents an accessory within a cell.
14
+ */
15
+ type Accessory = React.Node;
16
+
17
+ /**
18
+ * A subset of CSS Properties to allow overriding some of the default styles set
19
+ * on the accessory wrapper (loosely based on StyleType).
20
+ */
21
+ export type AccessoryStyle = {|
22
+ /**
23
+ * A subset of valid Spacing values.
24
+ */
25
+ minWidth?: 16 | 24 | 32 | 48,
26
+
27
+ /**
28
+ * To vertically align the accessory.
29
+ */
30
+ alignSelf?: "flex-start" | "flex-end" | "center",
31
+
32
+ /**
33
+ * To horizontally align the accessory.
34
+ */
35
+ alignItems?: "flex-start" | "flex-end" | "center",
36
+ |};
37
+
38
+ /**
39
+ * A union that allows using plain text or WB Typography elements.
40
+ */
41
+ export type TypographyText = string | React.Element<Typography>;
42
+
43
+ /**
44
+ * Common properties for all cells.
45
+ */
46
+ export type CellProps = {|
47
+ /**
48
+ * The title / main content of the cell. You can either provide a string or
49
+ * a Typography component. If a string is provided, typography defaults to
50
+ * LabelLarge.
51
+ */
52
+ title: TypographyText,
53
+
54
+ /**
55
+ * If provided, this adds a left accessory to the cell. Left
56
+ * Accessories can be defined using WB components such as Icon,
57
+ * IconButton, or it can even be used for a custom node/component if
58
+ * needed. What ever is passed in will occupy the "LeftAccessory” area
59
+ * of the Cell.
60
+ */
61
+ leftAccessory?: Accessory,
62
+
63
+ /**
64
+ * Optional custom styles applied to the leftAccessory wrapper. For
65
+ * example, it can be used to set a custom minWidth or a custom
66
+ * alignment.
67
+ *
68
+ * NOTE: leftAccessoryStyle can only be used if leftAccessory is set.
69
+ */
70
+ leftAccessoryStyle?: AccessoryStyle,
71
+
72
+ /**
73
+ * If provided, this adds a right accessory to the cell. Right
74
+ * Accessories can be defined using WB components such as Icon,
75
+ * IconButton, or it can even be used for a custom node/component if
76
+ * needed. What ever is passed in will occupy the “RightAccessory”
77
+ * area of the Cell.
78
+ */
79
+ rightAccessory?: Accessory,
80
+
81
+ /**
82
+ * Optional custom styles applied to the rightAccessory wrapper. For
83
+ * example, it can be used to set a custom minWidth or a custom
84
+ * alignment.
85
+ *
86
+ * NOTE: rightAccessoryStyle can only be used if rightAccessory is
87
+ * set.
88
+ */
89
+ rightAccessoryStyle?: AccessoryStyle,
90
+
91
+ /**
92
+ * Adds a horizontal rule at the bottom of the cell that can be used to
93
+ * separate cells within groups such as lists. Defaults to `inset`.
94
+ */
95
+ horizontalRule?: HorizontalRuleVariant,
96
+
97
+ /**
98
+ * Optional custom styles applied to the cell container.
99
+ */
100
+ style?: StyleType,
101
+
102
+ /**
103
+ * Optional test ID for e2e testing.
104
+ */
105
+ testId?: string,
106
+
107
+ /**
108
+ * Called when the cell is clicked.
109
+ *
110
+ * If not provided, the Cell can’t be hovered and/or pressed (highlighted on
111
+ * hover).
112
+ */
113
+ onClick?: (e: SyntheticEvent<>) => mixed,
114
+
115
+ /**
116
+ * Whether the cell is active (or currently selected).
117
+ */
118
+ active?: boolean,
119
+
120
+ /**
121
+ * Whether the cell is disabled.
122
+ */
123
+ disabled?: boolean,
124
+
125
+ /**
126
+ * Used to announce the cell's content to screen readers.
127
+ */
128
+ "aria-label"?: string,
129
+ |};