@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
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import {render, screen} from "@testing-library/react";
|
|
4
|
+
|
|
5
|
+
import Icon, {icons} from "@khanacademy/wonder-blocks-icon";
|
|
6
|
+
import {HeadingMedium} from "@khanacademy/wonder-blocks-typography";
|
|
7
|
+
|
|
8
|
+
import BasicCell from "../basic-cell.js";
|
|
9
|
+
|
|
10
|
+
describe("BasicCell", () => {
|
|
11
|
+
it("should render the default BasicCell component", () => {
|
|
12
|
+
// Arrange
|
|
13
|
+
|
|
14
|
+
// Act
|
|
15
|
+
render(<BasicCell title="Basic cell" />);
|
|
16
|
+
|
|
17
|
+
// Assert
|
|
18
|
+
expect(screen.getByText("Basic cell")).toBeInTheDocument();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("should render the title using a Typography element", () => {
|
|
22
|
+
// Arrange
|
|
23
|
+
|
|
24
|
+
// Act
|
|
25
|
+
render(<BasicCell title={<HeadingMedium>Basic cell</HeadingMedium>} />);
|
|
26
|
+
|
|
27
|
+
// Assert
|
|
28
|
+
expect(
|
|
29
|
+
screen.getByRole("heading", {name: "Basic cell"}),
|
|
30
|
+
).toBeInTheDocument();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should render the leftAccessory", () => {
|
|
34
|
+
// Arrange
|
|
35
|
+
|
|
36
|
+
// Act
|
|
37
|
+
render(
|
|
38
|
+
<BasicCell
|
|
39
|
+
title="Basic cell"
|
|
40
|
+
leftAccessory={
|
|
41
|
+
<Icon icon={icons.caretRight} aria-label="Caret icon" />
|
|
42
|
+
}
|
|
43
|
+
/>,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// Assert
|
|
47
|
+
expect(screen.getByLabelText("Caret icon")).toBeInTheDocument();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should render the rightAccessory", () => {
|
|
51
|
+
// Arrange
|
|
52
|
+
|
|
53
|
+
// Act
|
|
54
|
+
render(
|
|
55
|
+
<BasicCell
|
|
56
|
+
title="Basic cell"
|
|
57
|
+
rightAccessory={
|
|
58
|
+
<Icon icon={icons.caretRight} aria-label="Caret icon" />
|
|
59
|
+
}
|
|
60
|
+
/>,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// Assert
|
|
64
|
+
expect(screen.getByLabelText("Caret icon")).toBeInTheDocument();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("should add testId to the content wrapper", () => {
|
|
68
|
+
// Arrange
|
|
69
|
+
|
|
70
|
+
// Act
|
|
71
|
+
render(<BasicCell title="Basic cell" testId="cellId" />);
|
|
72
|
+
|
|
73
|
+
// Assert
|
|
74
|
+
expect(screen.getByTestId("cellId")).toHaveTextContent("Basic cell");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should add a button if onClick is set", () => {
|
|
78
|
+
// Arrange
|
|
79
|
+
|
|
80
|
+
// Act
|
|
81
|
+
render(<BasicCell title="Basic cell" onClick={jest.fn()} />);
|
|
82
|
+
|
|
83
|
+
// Assert
|
|
84
|
+
expect(screen.getByRole("button")).toBeInTheDocument();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("should allow clicking the cell if onClick is set", () => {
|
|
88
|
+
// Arrange
|
|
89
|
+
const onClickMock = jest.fn();
|
|
90
|
+
render(<BasicCell title="Basic cell" onClick={onClickMock} />);
|
|
91
|
+
|
|
92
|
+
// Act
|
|
93
|
+
screen.getByRole("button").click();
|
|
94
|
+
|
|
95
|
+
// Assert
|
|
96
|
+
expect(onClickMock).toHaveBeenCalled();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import {render, screen} from "@testing-library/react";
|
|
4
|
+
|
|
5
|
+
import {HeadingMedium} from "@khanacademy/wonder-blocks-typography";
|
|
6
|
+
|
|
7
|
+
import DetailCell from "../detail-cell.js";
|
|
8
|
+
|
|
9
|
+
describe("DetailCell", () => {
|
|
10
|
+
it("should render the default DetailCell component", () => {
|
|
11
|
+
// Arrange
|
|
12
|
+
|
|
13
|
+
// Act
|
|
14
|
+
render(<DetailCell title="Detail cell" />);
|
|
15
|
+
|
|
16
|
+
// Assert
|
|
17
|
+
expect(screen.getByText("Detail cell")).toBeInTheDocument();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("should render the title using a Typography element", () => {
|
|
21
|
+
// Arrange
|
|
22
|
+
|
|
23
|
+
// Act
|
|
24
|
+
render(
|
|
25
|
+
<DetailCell
|
|
26
|
+
title={<HeadingMedium>Detail cell title</HeadingMedium>}
|
|
27
|
+
/>,
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
// Assert
|
|
31
|
+
expect(
|
|
32
|
+
screen.getByRole("heading", {name: "Detail cell title"}),
|
|
33
|
+
).toBeInTheDocument();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should render the subtitle1 using a plain text", () => {
|
|
37
|
+
// Arrange
|
|
38
|
+
|
|
39
|
+
// Act
|
|
40
|
+
render(
|
|
41
|
+
<DetailCell
|
|
42
|
+
title="Detail cell"
|
|
43
|
+
subtitle1="Detail cell subtitle 1"
|
|
44
|
+
/>,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
// Assert
|
|
48
|
+
expect(screen.getByText("Detail cell subtitle 1")).toBeInTheDocument();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should render the subtitle1 using a Typography element", () => {
|
|
52
|
+
// Arrange
|
|
53
|
+
|
|
54
|
+
// Act
|
|
55
|
+
render(
|
|
56
|
+
<DetailCell
|
|
57
|
+
title="Detail cell"
|
|
58
|
+
subtitle1={
|
|
59
|
+
<HeadingMedium>Detail cell subtitle 1</HeadingMedium>
|
|
60
|
+
}
|
|
61
|
+
/>,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Assert
|
|
65
|
+
expect(
|
|
66
|
+
screen.getByRole("heading", {name: "Detail cell subtitle 1"}),
|
|
67
|
+
).toBeInTheDocument();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should render the subtitle2 using a plain text", () => {
|
|
71
|
+
// Arrange
|
|
72
|
+
|
|
73
|
+
// Act
|
|
74
|
+
render(
|
|
75
|
+
<DetailCell
|
|
76
|
+
title="Detail cell"
|
|
77
|
+
subtitle2="Detail cell subtitle 2"
|
|
78
|
+
/>,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// Assert
|
|
82
|
+
expect(screen.getByText("Detail cell subtitle 2")).toBeInTheDocument();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("should render the subtitle2 using a Typography element", () => {
|
|
86
|
+
// Arrange
|
|
87
|
+
|
|
88
|
+
// Act
|
|
89
|
+
render(
|
|
90
|
+
<DetailCell
|
|
91
|
+
title="Detail cell"
|
|
92
|
+
subtitle2={
|
|
93
|
+
<HeadingMedium>Detail cell subtitle 2</HeadingMedium>
|
|
94
|
+
}
|
|
95
|
+
/>,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// Assert
|
|
99
|
+
expect(
|
|
100
|
+
screen.getByRole("heading", {name: "Detail cell subtitle 2"}),
|
|
101
|
+
).toBeInTheDocument();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
import {LabelMedium} from "@khanacademy/wonder-blocks-typography";
|
|
5
|
+
|
|
6
|
+
import CellCore from "./internal/cell-core.js";
|
|
7
|
+
|
|
8
|
+
import type {CellProps} from "../util/types.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* BasicCell is the simplest form of the Cell. It is a compacted-height Cell
|
|
12
|
+
* with limited subviews and accessories, to be used for simple lists, like
|
|
13
|
+
* dropdown option items, navigation items, settings dialogs, etc.
|
|
14
|
+
*
|
|
15
|
+
* ### Usage
|
|
16
|
+
*
|
|
17
|
+
* ```jsx
|
|
18
|
+
* import {BasicCell} from "@khanacademy/wonder-blocks-cell";
|
|
19
|
+
*
|
|
20
|
+
* <BasicCell
|
|
21
|
+
* title="Basic cell"
|
|
22
|
+
* rightAccessory={<Icon icon={icons.caretRight} size="medium" />}
|
|
23
|
+
* />
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
function BasicCell(props: CellProps): React.Node {
|
|
27
|
+
const {title, ...coreProps} = props;
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<CellCore {...coreProps}>
|
|
31
|
+
{typeof title === "string" ? (
|
|
32
|
+
<LabelMedium>{title}</LabelMedium>
|
|
33
|
+
) : (
|
|
34
|
+
title
|
|
35
|
+
)}
|
|
36
|
+
</CellCore>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default BasicCell;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import {StyleSheet} from "aphrodite";
|
|
4
|
+
|
|
5
|
+
import Color from "@khanacademy/wonder-blocks-color";
|
|
6
|
+
import {Strut} from "@khanacademy/wonder-blocks-layout";
|
|
7
|
+
import Spacing from "@khanacademy/wonder-blocks-spacing";
|
|
8
|
+
import {LabelSmall, LabelLarge} from "@khanacademy/wonder-blocks-typography";
|
|
9
|
+
|
|
10
|
+
import CellCore from "./internal/cell-core.js";
|
|
11
|
+
|
|
12
|
+
import type {CellProps, TypographyText} from "../util/types.js";
|
|
13
|
+
|
|
14
|
+
type SubtitleProps = {|
|
|
15
|
+
subtitle?: TypographyText,
|
|
16
|
+
/**
|
|
17
|
+
* If true, the subtitle will use the alpha color defined in the parent
|
|
18
|
+
* component/element.
|
|
19
|
+
*/
|
|
20
|
+
disabled?: boolean,
|
|
21
|
+
|};
|
|
22
|
+
|
|
23
|
+
const Subtitle = ({subtitle, disabled}: SubtitleProps): React.Node => {
|
|
24
|
+
if (!subtitle) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (typeof subtitle === "string") {
|
|
29
|
+
return (
|
|
30
|
+
<LabelSmall style={!disabled && styles.subtitle}>
|
|
31
|
+
{subtitle}
|
|
32
|
+
</LabelSmall>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return subtitle;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type DetailCellProps = {|
|
|
40
|
+
...CellProps,
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* You can either provide a string or a custom node Typography element (or
|
|
44
|
+
* nothing at all). Both a string or a custom node Typography element will
|
|
45
|
+
* occupy the “Subtitle1” area of the Cell.
|
|
46
|
+
*/
|
|
47
|
+
subtitle1?: TypographyText,
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* You can either provide a string or a custom node Typography element (or
|
|
51
|
+
* nothing at all). Both a string or a custom node Typography element will
|
|
52
|
+
* occupy the “Subtitle2” area of the Cell.
|
|
53
|
+
*/
|
|
54
|
+
subtitle2?: TypographyText,
|
|
55
|
+
|};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* This is a variant of BasicCell that allows adding subtitles, before and after
|
|
59
|
+
* the cell title.
|
|
60
|
+
*
|
|
61
|
+
* ### Usage
|
|
62
|
+
*
|
|
63
|
+
* ```jsx
|
|
64
|
+
* import {DetailCell} from "@khanacademy/wonder-blocks-cell";
|
|
65
|
+
*
|
|
66
|
+
* <DetailCell
|
|
67
|
+
* leftAccessory={<Icon icon={icons.contentVideo} size="medium" />}
|
|
68
|
+
* subtitle1="Subtitle 1"
|
|
69
|
+
* title="Detail cell"
|
|
70
|
+
* subtitle1="Subtitle 2"
|
|
71
|
+
* rightAccessory={<Icon icon={icons.caretRight} size="medium" />}
|
|
72
|
+
* />
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
function DetailCell(props: DetailCellProps): React.Node {
|
|
76
|
+
const {title, subtitle1, subtitle2, ...coreProps} = props;
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<CellCore {...coreProps}>
|
|
80
|
+
<Subtitle subtitle={subtitle1} disabled={coreProps.disabled} />
|
|
81
|
+
{subtitle1 && <Strut size={Spacing.xxxxSmall_2} />}
|
|
82
|
+
{typeof title === "string" ? (
|
|
83
|
+
<LabelLarge>{title}</LabelLarge>
|
|
84
|
+
) : (
|
|
85
|
+
title
|
|
86
|
+
)}
|
|
87
|
+
{/* Add a vertical spacing between the title and the subtitle */}
|
|
88
|
+
{subtitle2 && <Strut size={Spacing.xxxxSmall_2} />}
|
|
89
|
+
<Subtitle subtitle={subtitle2} disabled={coreProps.disabled} />
|
|
90
|
+
</CellCore>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const styles = StyleSheet.create({
|
|
95
|
+
subtitle: {
|
|
96
|
+
color: Color.offBlack64,
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
export default DetailCell;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import {render, screen} from "@testing-library/react";
|
|
4
|
+
|
|
5
|
+
import CellCore from "../cell-core.js";
|
|
6
|
+
|
|
7
|
+
describe("CellCore", () => {
|
|
8
|
+
it("should render the CellCore component", () => {
|
|
9
|
+
// Arrange
|
|
10
|
+
|
|
11
|
+
// Act
|
|
12
|
+
render(
|
|
13
|
+
<CellCore>
|
|
14
|
+
<div>cell core content</div>
|
|
15
|
+
</CellCore>,
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
// Assert
|
|
19
|
+
expect(screen.getByText("cell core content")).toBeInTheDocument();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should NOT add a button by default", () => {
|
|
23
|
+
// Arrange
|
|
24
|
+
|
|
25
|
+
// Act
|
|
26
|
+
render(
|
|
27
|
+
<CellCore>
|
|
28
|
+
<div>cell core content</div>
|
|
29
|
+
</CellCore>,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// Assert
|
|
33
|
+
expect(screen.queryByRole("button")).not.toBeInTheDocument();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should add a button if onClick is set", () => {
|
|
37
|
+
// Arrange
|
|
38
|
+
|
|
39
|
+
// Act
|
|
40
|
+
render(
|
|
41
|
+
<CellCore onClick={jest.fn()}>
|
|
42
|
+
<div>cell core content</div>
|
|
43
|
+
</CellCore>,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// Assert
|
|
47
|
+
expect(screen.getByRole("button")).toBeInTheDocument();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should add aria-label to the button", () => {
|
|
51
|
+
// Arrange
|
|
52
|
+
|
|
53
|
+
// Act
|
|
54
|
+
render(
|
|
55
|
+
<CellCore onClick={jest.fn()} aria-label="some description">
|
|
56
|
+
<div>cell core content</div>
|
|
57
|
+
</CellCore>,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Assert
|
|
61
|
+
expect(
|
|
62
|
+
screen.getByRole("button", {name: "some description"}),
|
|
63
|
+
).toBeInTheDocument();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should add aria-disabled if disabled is set", () => {
|
|
67
|
+
// Arrange
|
|
68
|
+
|
|
69
|
+
// Act
|
|
70
|
+
render(
|
|
71
|
+
<CellCore onClick={jest.fn()} disabled={true}>
|
|
72
|
+
<div>cell core content</div>
|
|
73
|
+
</CellCore>,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// Assert
|
|
77
|
+
expect(screen.getByRole("button")).toBeDisabled();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("should add aria-current if active is set", () => {
|
|
81
|
+
// Arrange
|
|
82
|
+
|
|
83
|
+
// Act
|
|
84
|
+
const {container} = render(
|
|
85
|
+
<CellCore active={true}>
|
|
86
|
+
<div>cell core content</div>
|
|
87
|
+
</CellCore>,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Assert
|
|
91
|
+
// Verify that the root element has the aria-current attribute
|
|
92
|
+
// eslint-disable-next-line testing-library/no-node-access
|
|
93
|
+
expect(container.firstChild).toHaveAttribute("aria-current", "true");
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import {getHorizontalRuleStyles} from "../common.js";
|
|
4
|
+
|
|
5
|
+
describe("getHorizontalRuleStyles", () => {
|
|
6
|
+
it("should get 'inset' styles as an array", () => {
|
|
7
|
+
// Arrange
|
|
8
|
+
|
|
9
|
+
// Act
|
|
10
|
+
const styles = getHorizontalRuleStyles("inset");
|
|
11
|
+
|
|
12
|
+
// Assert
|
|
13
|
+
// Verify that both classes are injected
|
|
14
|
+
expect(styles).toMatchObject([
|
|
15
|
+
{
|
|
16
|
+
_name: /horizontalRule/,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
_name: /horizontalRuleInset/,
|
|
20
|
+
},
|
|
21
|
+
]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should get 'full-width' styles as an object", () => {
|
|
25
|
+
// Arrange
|
|
26
|
+
|
|
27
|
+
// Act
|
|
28
|
+
const styles = getHorizontalRuleStyles("full-width");
|
|
29
|
+
|
|
30
|
+
// Assert
|
|
31
|
+
// Verify that only one class is injected
|
|
32
|
+
expect(styles).toMatchObject({
|
|
33
|
+
_name: /horizontalRule/,
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("should not inject styles with 'none'", () => {
|
|
38
|
+
// Arrange
|
|
39
|
+
|
|
40
|
+
// Act
|
|
41
|
+
const styles = getHorizontalRuleStyles("none");
|
|
42
|
+
|
|
43
|
+
// Assert
|
|
44
|
+
// Verify that we don't inject any styles
|
|
45
|
+
expect(styles).toMatchObject({});
|
|
46
|
+
});
|
|
47
|
+
});
|