@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.
- package/CHANGELOG.md +17 -0
- package/dist/es/index.js +351 -0
- package/dist/index.js +577 -0
- package/dist/index.js.flow +2 -0
- package/package.json +33 -0
- package/src/components/__docs__/basic-cell.argtypes.js +178 -0
- package/src/components/__docs__/basic-cell.stories.js +302 -0
- package/src/components/__docs__/detail-cell.argtypes.js +28 -0
- package/src/components/__docs__/detail-cell.stories.js +154 -0
- package/src/components/__tests__/basic-cell.test.js +98 -0
- package/src/components/__tests__/detail-cell.test.js +103 -0
- package/src/components/basic-cell.js +40 -0
- package/src/components/detail-cell.js +100 -0
- package/src/components/internal/__tests__/cell-core.test.js +95 -0
- package/src/components/internal/__tests__/common.test.js +47 -0
- package/src/components/internal/cell-core.js +288 -0
- package/src/components/internal/common.js +73 -0
- package/src/index.js +5 -0
- package/src/util/types.js +129 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# @khanacademy/wonder-blocks-cell
|
|
2
|
+
|
|
3
|
+
## 1.0.0
|
|
4
|
+
### Major Changes
|
|
5
|
+
|
|
6
|
+
- dc6c5f05: Add DetailCell component
|
|
7
|
+
|
|
8
|
+
### Minor Changes
|
|
9
|
+
|
|
10
|
+
- dc6c5f05: Add horizontalRule, style and testId to BasicCell
|
|
11
|
+
- dc6c5f05: Add BasicCell and also title, leftAccessory, rightAccessory props
|
|
12
|
+
- dc6c5f05: Add states (active, disabled, hovered, focused, pressed), onClick and aria-label
|
|
13
|
+
- dc6c5f05: Adds `wonder-blocks-cell` package
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- 2855fa1f: Add WB layout as a dependency
|
package/dist/es/index.js
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import _objectWithoutPropertiesLoose from '@babel/runtime/helpers/objectWithoutPropertiesLoose';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { LabelMedium, LabelLarge, LabelSmall } from '@khanacademy/wonder-blocks-typography';
|
|
4
|
+
import _extends from '@babel/runtime/helpers/extends';
|
|
5
|
+
import { StyleSheet } from 'aphrodite';
|
|
6
|
+
import Clickable from '@khanacademy/wonder-blocks-clickable';
|
|
7
|
+
import { View } from '@khanacademy/wonder-blocks-core';
|
|
8
|
+
import Color, { fade } from '@khanacademy/wonder-blocks-color';
|
|
9
|
+
import { Strut } from '@khanacademy/wonder-blocks-layout';
|
|
10
|
+
import Spacing from '@khanacademy/wonder-blocks-spacing';
|
|
11
|
+
|
|
12
|
+
const CellMeasurements = {
|
|
13
|
+
cellMinHeight: Spacing.xxLarge_48,
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The cell wrapper's gap.
|
|
17
|
+
*/
|
|
18
|
+
cellPadding: {
|
|
19
|
+
paddingVertical: Spacing.xSmall_8,
|
|
20
|
+
paddingHorizontal: Spacing.medium_16
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The extra vertical spacing added to the title/content wrapper.
|
|
25
|
+
*/
|
|
26
|
+
contentVerticalSpacing: Spacing.xxxSmall_4,
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The horizontal spacing between the left and right accessory.
|
|
30
|
+
*/
|
|
31
|
+
accessoryHorizontalSpacing: Spacing.medium_16
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Gets the horizontalRule style based on the variant.
|
|
35
|
+
* @param {HorizontalRuleVariant} horizontalRule The variant of the horizontal
|
|
36
|
+
* rule.
|
|
37
|
+
* @returns A styled horizontal rule.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
const getHorizontalRuleStyles = horizontalRule => {
|
|
41
|
+
switch (horizontalRule) {
|
|
42
|
+
case "inset":
|
|
43
|
+
return [styles$2.horizontalRule, styles$2.horizontalRuleInset];
|
|
44
|
+
|
|
45
|
+
case "full-width":
|
|
46
|
+
return styles$2.horizontalRule;
|
|
47
|
+
|
|
48
|
+
case "none":
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const styles$2 = StyleSheet.create({
|
|
53
|
+
horizontalRule: {
|
|
54
|
+
position: "relative",
|
|
55
|
+
":after": {
|
|
56
|
+
width: "100%",
|
|
57
|
+
content: "''",
|
|
58
|
+
position: "absolute",
|
|
59
|
+
// align to the bottom of the cell
|
|
60
|
+
bottom: 0,
|
|
61
|
+
// align border to the right of the cell
|
|
62
|
+
right: 0,
|
|
63
|
+
height: Spacing.xxxxSmall_2,
|
|
64
|
+
boxShadow: `inset 0px -1px 0px ${Color.offBlack8}`
|
|
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
|
+
});
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Left Accessories can be defined using WB components such as Icon, IconButton,
|
|
77
|
+
* or it can even be used for a custom node/component if needed.
|
|
78
|
+
*/
|
|
79
|
+
const LeftAccessory = ({
|
|
80
|
+
leftAccessory,
|
|
81
|
+
leftAccessoryStyle,
|
|
82
|
+
disabled
|
|
83
|
+
}) => {
|
|
84
|
+
if (!leftAccessory) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(View, {
|
|
89
|
+
style: [styles$1.accessory, disabled && styles$1.accessoryDisabled, _extends({}, leftAccessoryStyle)]
|
|
90
|
+
}, leftAccessory), /*#__PURE__*/React.createElement(Strut, {
|
|
91
|
+
size: CellMeasurements.accessoryHorizontalSpacing
|
|
92
|
+
}));
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Right Accessories can be defined using WB components such as Icon,
|
|
97
|
+
* IconButton, or it can even be used for a custom node/component if needed.
|
|
98
|
+
*/
|
|
99
|
+
const RightAccessory = ({
|
|
100
|
+
rightAccessory,
|
|
101
|
+
rightAccessoryStyle,
|
|
102
|
+
active,
|
|
103
|
+
disabled
|
|
104
|
+
}) => {
|
|
105
|
+
if (!rightAccessory) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Strut, {
|
|
110
|
+
size: CellMeasurements.accessoryHorizontalSpacing
|
|
111
|
+
}), /*#__PURE__*/React.createElement(View, {
|
|
112
|
+
style: [styles$1.accessory, styles$1.accessoryRight, disabled && styles$1.accessoryDisabled, _extends({}, rightAccessoryStyle), active && styles$1.accessoryActive]
|
|
113
|
+
}, rightAccessory));
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* CellCore is the base cell wrapper. It's used as the skeleton/layout that is
|
|
118
|
+
* used by BasicCell and DetailCell (and any other variants).
|
|
119
|
+
*
|
|
120
|
+
* Both variants share how they render their accessories, and the main
|
|
121
|
+
* responsibility of this component is to render the contents that are passed in
|
|
122
|
+
* (using the `children` prop).
|
|
123
|
+
*/
|
|
124
|
+
const CellCore = props => {
|
|
125
|
+
const {
|
|
126
|
+
active,
|
|
127
|
+
children,
|
|
128
|
+
disabled,
|
|
129
|
+
horizontalRule = "inset",
|
|
130
|
+
leftAccessory = undefined,
|
|
131
|
+
leftAccessoryStyle = undefined,
|
|
132
|
+
onClick,
|
|
133
|
+
rightAccessory = undefined,
|
|
134
|
+
rightAccessoryStyle = undefined,
|
|
135
|
+
style,
|
|
136
|
+
testId,
|
|
137
|
+
"aria-label": ariaLabel
|
|
138
|
+
} = props;
|
|
139
|
+
|
|
140
|
+
const renderCell = eventState => {
|
|
141
|
+
const horizontalRuleStyles = getHorizontalRuleStyles(horizontalRule);
|
|
142
|
+
return /*#__PURE__*/React.createElement(View, {
|
|
143
|
+
style: [styles$1.wrapper, // focused applied to the main wrapper to make the border
|
|
144
|
+
// outline part of the wrapper
|
|
145
|
+
(eventState == null ? void 0 : eventState.focused) && styles$1.focused, // custom styles
|
|
146
|
+
style],
|
|
147
|
+
"aria-current": active ? "true" : undefined
|
|
148
|
+
}, /*#__PURE__*/React.createElement(View, {
|
|
149
|
+
style: [styles$1.innerWrapper, horizontalRuleStyles, disabled && styles$1.disabled, active && styles$1.active, // other states applied to the inner wrapper to blend
|
|
150
|
+
// the background color properly
|
|
151
|
+
!disabled && (eventState == null ? void 0 : eventState.hovered) && styles$1.hovered, // active + hovered
|
|
152
|
+
active && (eventState == null ? void 0 : eventState.hovered) && styles$1.activeHovered, !disabled && (eventState == null ? void 0 : eventState.pressed) && styles$1.pressed, // active + pressed
|
|
153
|
+
!disabled && active && (eventState == null ? void 0 : eventState.pressed) && styles$1.activePressed]
|
|
154
|
+
}, /*#__PURE__*/React.createElement(LeftAccessory, {
|
|
155
|
+
leftAccessory: leftAccessory,
|
|
156
|
+
leftAccessoryStyle: leftAccessoryStyle,
|
|
157
|
+
disabled: disabled
|
|
158
|
+
}), /*#__PURE__*/React.createElement(View, {
|
|
159
|
+
style: styles$1.content,
|
|
160
|
+
testId: testId
|
|
161
|
+
}, children), /*#__PURE__*/React.createElement(RightAccessory, {
|
|
162
|
+
rightAccessory: rightAccessory,
|
|
163
|
+
rightAccessoryStyle: rightAccessoryStyle,
|
|
164
|
+
active: active,
|
|
165
|
+
disabled: disabled
|
|
166
|
+
})));
|
|
167
|
+
}; // Pressable cell.
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
if (onClick) {
|
|
171
|
+
return /*#__PURE__*/React.createElement(Clickable, {
|
|
172
|
+
disabled: disabled,
|
|
173
|
+
onClick: onClick,
|
|
174
|
+
hideDefaultFocusRing: true,
|
|
175
|
+
"aria-label": ariaLabel ? ariaLabel : undefined
|
|
176
|
+
}, eventState => renderCell(eventState));
|
|
177
|
+
} // No click event attached, so just render the cell as-is.
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
return renderCell();
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const styles$1 = StyleSheet.create({
|
|
184
|
+
wrapper: {
|
|
185
|
+
background: Color.white,
|
|
186
|
+
color: Color.offBlack,
|
|
187
|
+
minHeight: CellMeasurements.cellMinHeight,
|
|
188
|
+
textAlign: "left"
|
|
189
|
+
},
|
|
190
|
+
innerWrapper: {
|
|
191
|
+
padding: `${CellMeasurements.cellPadding.paddingVertical}px ${CellMeasurements.cellPadding.paddingHorizontal}px`,
|
|
192
|
+
flexDirection: "row",
|
|
193
|
+
flex: 1
|
|
194
|
+
},
|
|
195
|
+
content: {
|
|
196
|
+
alignSelf: "center",
|
|
197
|
+
overflowWrap: "break-word",
|
|
198
|
+
padding: `${CellMeasurements.contentVerticalSpacing}px 0`
|
|
199
|
+
},
|
|
200
|
+
accessory: {
|
|
201
|
+
// Use content width by default.
|
|
202
|
+
minWidth: "auto",
|
|
203
|
+
// Horizontal alignment of the accessory.
|
|
204
|
+
alignItems: "center",
|
|
205
|
+
// Vertical alignment.
|
|
206
|
+
alignSelf: "center"
|
|
207
|
+
},
|
|
208
|
+
accessoryRight: {
|
|
209
|
+
// The right accessory will have this color by default. Unless the
|
|
210
|
+
// accessory element overrides that color internally.
|
|
211
|
+
color: Color.offBlack64,
|
|
212
|
+
// Align the right accessory to the right side of the cell, so we can
|
|
213
|
+
// prevent the accessory from shifting left, if the content is too
|
|
214
|
+
// short.
|
|
215
|
+
marginLeft: "auto"
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* States
|
|
220
|
+
*/
|
|
221
|
+
hovered: {
|
|
222
|
+
background: Color.offBlack8
|
|
223
|
+
},
|
|
224
|
+
// Handling the focus ring internally because clickable doesn't support
|
|
225
|
+
// rounded focus ring.
|
|
226
|
+
focused: {
|
|
227
|
+
borderRadius: Spacing.xxxSmall_4,
|
|
228
|
+
outline: `solid ${Spacing.xxxxSmall_2}px ${Color.blue}`,
|
|
229
|
+
// The focus ring is not visible when there are stacked cells.
|
|
230
|
+
// Using outlineOffset to display the focus ring inside the cell.
|
|
231
|
+
outlineOffset: -Spacing.xxxxSmall_2,
|
|
232
|
+
// To hide the internal corners of the cell.
|
|
233
|
+
overflow: "hidden"
|
|
234
|
+
},
|
|
235
|
+
pressed: {
|
|
236
|
+
background: Color.offBlack16
|
|
237
|
+
},
|
|
238
|
+
active: {
|
|
239
|
+
background: fade(Color.blue, 0.08),
|
|
240
|
+
color: Color.blue
|
|
241
|
+
},
|
|
242
|
+
activeHovered: {
|
|
243
|
+
background: fade(Color.blue, 0.16)
|
|
244
|
+
},
|
|
245
|
+
activePressed: {
|
|
246
|
+
background: fade(Color.blue, 0.24)
|
|
247
|
+
},
|
|
248
|
+
disabled: {
|
|
249
|
+
color: Color.offBlack32
|
|
250
|
+
},
|
|
251
|
+
accessoryActive: {
|
|
252
|
+
color: Color.blue
|
|
253
|
+
},
|
|
254
|
+
accessoryDisabled: {
|
|
255
|
+
color: Color.offBlack,
|
|
256
|
+
opacity: 0.32
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const _excluded$1 = ["title"];
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* BasicCell is the simplest form of the Cell. It is a compacted-height Cell
|
|
264
|
+
* with limited subviews and accessories, to be used for simple lists, like
|
|
265
|
+
* dropdown option items, navigation items, settings dialogs, etc.
|
|
266
|
+
*
|
|
267
|
+
* ### Usage
|
|
268
|
+
*
|
|
269
|
+
* ```jsx
|
|
270
|
+
* import {BasicCell} from "@khanacademy/wonder-blocks-cell";
|
|
271
|
+
*
|
|
272
|
+
* <BasicCell
|
|
273
|
+
* title="Basic cell"
|
|
274
|
+
* rightAccessory={<Icon icon={icons.caretRight} size="medium" />}
|
|
275
|
+
* />
|
|
276
|
+
* ```
|
|
277
|
+
*/
|
|
278
|
+
function BasicCell(props) {
|
|
279
|
+
const {
|
|
280
|
+
title
|
|
281
|
+
} = props,
|
|
282
|
+
coreProps = _objectWithoutPropertiesLoose(props, _excluded$1);
|
|
283
|
+
|
|
284
|
+
return /*#__PURE__*/React.createElement(CellCore, coreProps, typeof title === "string" ? /*#__PURE__*/React.createElement(LabelMedium, null, title) : title);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const _excluded = ["title", "subtitle1", "subtitle2"];
|
|
288
|
+
|
|
289
|
+
const Subtitle = ({
|
|
290
|
+
subtitle,
|
|
291
|
+
disabled
|
|
292
|
+
}) => {
|
|
293
|
+
if (!subtitle) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (typeof subtitle === "string") {
|
|
298
|
+
return /*#__PURE__*/React.createElement(LabelSmall, {
|
|
299
|
+
style: !disabled && styles.subtitle
|
|
300
|
+
}, subtitle);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return subtitle;
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* This is a variant of BasicCell that allows adding subtitles, before and after
|
|
308
|
+
* the cell title.
|
|
309
|
+
*
|
|
310
|
+
* ### Usage
|
|
311
|
+
*
|
|
312
|
+
* ```jsx
|
|
313
|
+
* import {DetailCell} from "@khanacademy/wonder-blocks-cell";
|
|
314
|
+
*
|
|
315
|
+
* <DetailCell
|
|
316
|
+
* leftAccessory={<Icon icon={icons.contentVideo} size="medium" />}
|
|
317
|
+
* subtitle1="Subtitle 1"
|
|
318
|
+
* title="Detail cell"
|
|
319
|
+
* subtitle1="Subtitle 2"
|
|
320
|
+
* rightAccessory={<Icon icon={icons.caretRight} size="medium" />}
|
|
321
|
+
* />
|
|
322
|
+
* ```
|
|
323
|
+
*/
|
|
324
|
+
function DetailCell(props) {
|
|
325
|
+
const {
|
|
326
|
+
title,
|
|
327
|
+
subtitle1,
|
|
328
|
+
subtitle2
|
|
329
|
+
} = props,
|
|
330
|
+
coreProps = _objectWithoutPropertiesLoose(props, _excluded);
|
|
331
|
+
|
|
332
|
+
return /*#__PURE__*/React.createElement(CellCore, coreProps, /*#__PURE__*/React.createElement(Subtitle, {
|
|
333
|
+
subtitle: subtitle1,
|
|
334
|
+
disabled: coreProps.disabled
|
|
335
|
+
}), subtitle1 && /*#__PURE__*/React.createElement(Strut, {
|
|
336
|
+
size: Spacing.xxxxSmall_2
|
|
337
|
+
}), typeof title === "string" ? /*#__PURE__*/React.createElement(LabelLarge, null, title) : title, subtitle2 && /*#__PURE__*/React.createElement(Strut, {
|
|
338
|
+
size: Spacing.xxxxSmall_2
|
|
339
|
+
}), /*#__PURE__*/React.createElement(Subtitle, {
|
|
340
|
+
subtitle: subtitle2,
|
|
341
|
+
disabled: coreProps.disabled
|
|
342
|
+
}));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const styles = StyleSheet.create({
|
|
346
|
+
subtitle: {
|
|
347
|
+
color: Color.offBlack64
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
export { BasicCell, DetailCell };
|