@khanacademy/wonder-blocks-card 0.0.0-PR2860-20251112235619 → 0.0.0-PR2876-20251209223105

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,145 +0,0 @@
1
- import * as React from "react";
2
- import {render, screen} from "@testing-library/react";
3
- import {userEvent} from "@testing-library/user-event";
4
-
5
- import {DismissButton} from "../../components/dismiss-button";
6
-
7
- describe("DismissButton", () => {
8
- describe("Basic rendering", () => {
9
- it("should render as a button", () => {
10
- // Arrange & Act
11
- render(<DismissButton />);
12
-
13
- // Assert
14
- expect(screen.getByRole("button")).toBeInTheDocument();
15
- });
16
- });
17
-
18
- describe("Accessibility", () => {
19
- it("should use default aria-label when not provided", () => {
20
- // Arrange & Act
21
- render(<DismissButton />);
22
-
23
- // Assert
24
- expect(
25
- screen.getByRole("button", {name: "Close"}),
26
- ).toBeInTheDocument();
27
- });
28
-
29
- it("should use custom aria-label when provided", () => {
30
- // Arrange & Act
31
- render(<DismissButton aria-label="Dismiss notification" />);
32
-
33
- // Assert
34
- expect(
35
- screen.getByRole("button", {name: "Dismiss notification"}),
36
- ).toBeInTheDocument();
37
- });
38
-
39
- it("should be keyboard accessible", () => {
40
- // Arrange
41
- render(<DismissButton aria-label="Close" />);
42
-
43
- // Act
44
- const button = screen.getByRole("button");
45
- button.focus();
46
-
47
- // Assert
48
- expect(button).toHaveFocus();
49
- });
50
- });
51
-
52
- describe("Event handling", () => {
53
- it("should call onClick when button is clicked", async () => {
54
- // Arrange
55
- const mockOnClick = jest.fn();
56
- render(<DismissButton onClick={mockOnClick} aria-label="Close" />);
57
-
58
- // Act
59
- const button = screen.getByRole("button", {name: "Close"});
60
- await userEvent.click(button);
61
-
62
- // Assert
63
- expect(mockOnClick).toHaveBeenCalledTimes(1);
64
- });
65
-
66
- it("should call onClick with event parameter", async () => {
67
- // Arrange
68
- const mockOnClick = jest.fn();
69
- render(<DismissButton onClick={mockOnClick} aria-label="Close" />);
70
-
71
- // Act
72
- const button = screen.getByRole("button", {name: "Close"});
73
- await userEvent.click(button);
74
-
75
- // Assert
76
- expect(mockOnClick).toHaveBeenCalledWith(
77
- expect.objectContaining({
78
- type: "click",
79
- }),
80
- );
81
- });
82
-
83
- it("should handle missing onClick prop gracefully", async () => {
84
- // Arrange
85
- render(<DismissButton aria-label="Close" />);
86
-
87
- // Act & Assert
88
- const button = screen.getByRole("button", {name: "Close"});
89
- await expect(userEvent.click(button)).resolves.not.toThrow();
90
- });
91
-
92
- it("should support keyboard activation with Enter", async () => {
93
- // Arrange
94
- const mockOnClick = jest.fn();
95
- render(<DismissButton onClick={mockOnClick} aria-label="Close" />);
96
-
97
- // Act
98
- const button = screen.getByRole("button", {name: "Close"});
99
- button.focus();
100
- await userEvent.keyboard("{Enter}");
101
-
102
- // Assert
103
- expect(mockOnClick).toHaveBeenCalledTimes(1);
104
- });
105
-
106
- it("should support keyboard activation with Space", async () => {
107
- // Arrange
108
- const mockOnClick = jest.fn();
109
- render(<DismissButton onClick={mockOnClick} aria-label="Close" />);
110
-
111
- // Act
112
- const button = screen.getByRole("button", {name: "Close"});
113
- button.focus();
114
- await userEvent.keyboard(" ");
115
-
116
- // Assert
117
- expect(mockOnClick).toHaveBeenCalledTimes(1);
118
- });
119
- });
120
-
121
- describe("Test ID support", () => {
122
- it("should set data-testid attribute when testId is provided", () => {
123
- // Arrange
124
- const testId = "dismiss-button-test";
125
- render(<DismissButton testId={testId} aria-label="Close" />);
126
-
127
- // Act
128
- const button = screen.getByTestId(testId);
129
-
130
- // Assert
131
- expect(button).toHaveAttribute("data-testid", testId);
132
- });
133
-
134
- it("should not have data-testid attribute when testId is not provided", () => {
135
- // Arrange
136
- render(<DismissButton aria-label="Close" />);
137
-
138
- // Act
139
- const button = screen.getByRole("button");
140
-
141
- // Assert
142
- expect(button).not.toHaveAttribute("data-testid");
143
- });
144
- });
145
- });
@@ -1,264 +0,0 @@
1
- import * as React from "react";
2
-
3
- import {StyleSheet} from "aphrodite";
4
- import {StyleType, View} from "@khanacademy/wonder-blocks-core";
5
-
6
- import {
7
- boxShadow,
8
- border,
9
- semanticColor,
10
- sizing,
11
- font,
12
- } from "@khanacademy/wonder-blocks-tokens";
13
-
14
- import {DismissButton} from "./dismiss-button";
15
-
16
- /**
17
- * Base props that are shared across all Card configurations
18
- */
19
- type BaseCardProps = {
20
- /**
21
- * Optional styles to be applied to the root element and the dismiss button.
22
- */
23
- styles?: {
24
- root?: StyleType;
25
- dismissButton?: StyleType;
26
- };
27
- /**
28
- * A ref that will be passed to the root element (i.e. the card container).
29
- */
30
- ref?: React.Ref<HTMLElement>;
31
- /**
32
- * The content for the card.
33
- */
34
- children: React.ReactNode;
35
- /**
36
- * An optional attribute to remove this component from the accessibility tree
37
- * and keyboard tab order, such as for inactive cards in a stack.
38
- */
39
- inert?: boolean;
40
- /**
41
- * The test ID used to locate this component in automated tests.
42
- */
43
- testId?: string;
44
- } & StyleProps;
45
-
46
- /**
47
- * A callback function to handle dismissing the card. When this prop is present,
48
- * a dismiss button with an X icon will be rendered.
49
- *
50
- * When `onDismiss` is provided, `labels.dismissButtonAriaLabel` must also be
51
- * provided for accessibility and localization.
52
- */
53
- type DismissProps =
54
- | {
55
- onDismiss: (e?: React.SyntheticEvent) => void;
56
- labels: {dismissButtonAriaLabel: string} & Record<string, any>;
57
- }
58
- | {
59
- onDismiss?: never;
60
- labels?: Record<string, any>;
61
- };
62
-
63
- /**
64
- * Provide a specific HTML tag that overrides the default (`div`).
65
- *
66
- * Notes:
67
- * - When `tag="section"` or `"figure"`, `cardAriaLabel` is required for accessibility.
68
- * - `button` and `a` tags are not allowed - use Wonder Blocks Button and Link components as children instead.
69
- * Valid HTML tags for the Card component.
70
- * Excludes button and anchor tags which should use Wonder Blocks Button and Link components instead.
71
- */
72
- type ValidCardTags = Exclude<keyof JSX.IntrinsicElements, "button" | "a">;
73
-
74
- type TagProps =
75
- | {
76
- // Section and figure require an aria-label
77
- tag: "section" | "figure";
78
- labels: {cardAriaLabel: string} & Record<string, any>;
79
- }
80
- | {
81
- // All other valid tags except button and a
82
- tag?: Exclude<ValidCardTags, "section" | "figure">;
83
- labels?: Record<string, any>;
84
- };
85
-
86
- type StyleProps = {
87
- /**
88
- * The background style of the card, as a string identifier that matches a semanticColor token.
89
- * This can be one of:
90
- * - `"base-subtle"` (color), `semanticColor.core.background.base.subtle`: a light gray background.
91
- * - `"base-default"` (color), `semanticColor.core.background.base.default`: a white background.
92
- * - `Image` (image), a URL string for a background image. Can be an imported image file or a URL string.
93
- *
94
- * For additional background styling such as repeat or size, use the `styles.root` prop to pass in custom styles.
95
- *
96
- * Default: `"base-default"`
97
- */
98
- background?: "base-subtle" | "base-default" | typeof Image | null;
99
- /**
100
- * The border radius of the card, as a string identifier that matches a border.radius token.
101
- * This can be one of:
102
- * - `"radius_080"`, matching `border.radius.radius_080`.
103
- * - `"radius_120"`, matching `border.radius.radius_120`.
104
- *
105
- * Default: `"radius_080"`
106
- */
107
- borderRadius?: "small" | "medium";
108
- /**
109
- * The padding inside the card, as a string identifier that matches a sizing token.
110
- * This can be one of:
111
- * - `"none"`: no padding.
112
- * - `"small"`, matching `sizing.size_160`.
113
- * - `"medium"`, matching `sizing.size_240`.
114
- *
115
- * Default: `"size_160"`
116
- */
117
- paddingSize?: "none" | "small" | "medium";
118
- /**
119
- * The box-shadow for the card, as a string identifier that matches a sizing token.
120
- * This can be one of:
121
- * - `"none"`: no elevation.
122
- * - `"low"`, matching `boxShadow.low`.
123
- *
124
- * Default: `"none"`
125
- */
126
- elevation?: "none" | "low";
127
- };
128
-
129
- type CardProps = BaseCardProps & TagProps & DismissProps;
130
- /**
131
- * The Card component is a flexible, reusable UI building block designed to
132
- * encapsulate content within a structured, visually distinct container.
133
- * Its primary goal is to present grouped or related information in a way that
134
- * is visually consistent, easily scannable, and modular across different
135
- * parts of the application.
136
- *
137
- * Cards provide a defined surface area with clear visual boundaries
138
- * (via border-radius and box-shadow elevation tokens), making them ideal for
139
- * use cases that involve displaying comparable content items side-by-side or
140
- * in structured layouts such as grids, lists, or dashboards.
141
- *
142
- * Note: cards do not set a default width. Width styles should be set by the consumer
143
- * with the `styles.root` prop, or a parent flex or grid container.
144
- *
145
- * ### Usage
146
- *
147
- * ```jsx
148
- * import {Card} from "@khanacademy/wonder-blocks-card";
149
- *
150
- * <Card>
151
- * <Heading>This is a basic card.</Heading>
152
- * </Card>
153
- * ```
154
- *
155
- * When the `onDismiss` prop is provided, a dismiss button will be rendered. In this case, the `labels.dismissButtonAriaLabel` prop is required to provide an accessible label for the dismiss button.
156
- *
157
- * See additional Accessibility docs.
158
- */
159
-
160
- const Card = React.forwardRef(function Card(
161
- props: CardProps,
162
- ref: React.ForwardedRef<HTMLElement>,
163
- ) {
164
- const {
165
- styles,
166
- labels,
167
- tag,
168
- testId,
169
- background = "base-default",
170
- borderRadius = "small",
171
- paddingSize = "small",
172
- elevation = "none",
173
- children,
174
- onDismiss,
175
- inert,
176
- } = props;
177
-
178
- const isBackgroundToken =
179
- background === "base-default" || background === "base-subtle";
180
- const componentStyles = getComponentStyles({
181
- background: isBackgroundToken ? background : null,
182
- borderRadius,
183
- paddingSize,
184
- elevation,
185
- });
186
-
187
- return (
188
- <View
189
- aria-label={labels?.cardAriaLabel}
190
- style={[
191
- componentStyles.root,
192
- !isBackgroundToken && {
193
- background: `url(${background})`,
194
- backgroundSize: "cover",
195
- },
196
- styles?.root,
197
- ]}
198
- ref={ref}
199
- tag={tag}
200
- testId={testId}
201
- {...{inert: inert ? "" : undefined}}
202
- >
203
- {onDismiss ? (
204
- <DismissButton
205
- aria-label={labels?.dismissButtonAriaLabel || "Close"}
206
- onClick={(e) => onDismiss?.(e)}
207
- />
208
- ) : null}
209
- {children}
210
- </View>
211
- );
212
- });
213
-
214
- // Map prop values to tokens
215
- const styleMap = {
216
- backgroundColor: {
217
- "base-subtle": semanticColor.core.background.base.subtle,
218
- "base-default": semanticColor.core.background.base.default,
219
- },
220
- borderRadius: {
221
- small: border.radius.radius_080,
222
- medium: border.radius.radius_120,
223
- },
224
- padding: {
225
- none: sizing.size_0,
226
- small: sizing.size_160,
227
- medium: sizing.size_240,
228
- },
229
- elevation: {
230
- none: "none",
231
- low: boxShadow.low,
232
- },
233
- } as const;
234
-
235
- /**
236
- * Gets the styles for the card based on its props
237
- */
238
- const getComponentStyles = ({
239
- background = "base-default",
240
- borderRadius = "small",
241
- paddingSize = "small",
242
- elevation = "none",
243
- }: StyleProps) => {
244
- const bgColor = background as keyof typeof styleMap.backgroundColor;
245
- return StyleSheet.create({
246
- root: {
247
- backgroundColor: bgColor && styleMap.backgroundColor[bgColor],
248
- // Common styles
249
- borderColor: semanticColor.core.border.neutral.subtle,
250
- borderStyle: "solid",
251
- borderWidth: border.width.thin,
252
- // Apply the system font to cards by default
253
- fontFamily: font.family.sans,
254
- minInlineSize: sizing.size_280,
255
- position: "relative",
256
- // Optional styles based on props
257
- borderRadius: styleMap.borderRadius[borderRadius],
258
- boxShadow: styleMap.elevation[elevation],
259
- padding: styleMap.padding[paddingSize],
260
- },
261
- });
262
- };
263
-
264
- export default Card;
@@ -1,61 +0,0 @@
1
- import * as React from "react";
2
- import {StyleSheet} from "aphrodite";
3
- import xIcon from "@phosphor-icons/core/bold/x-bold.svg";
4
- import IconButton from "@khanacademy/wonder-blocks-icon-button";
5
- import type {StyleType} from "@khanacademy/wonder-blocks-core";
6
- import {focusStyles} from "@khanacademy/wonder-blocks-styles";
7
- import {sizing} from "@khanacademy/wonder-blocks-tokens";
8
-
9
- type Props = {
10
- /** Optional click handler */
11
- onClick?: (e?: React.SyntheticEvent) => unknown;
12
- /** Screen reader label for close button */
13
- "aria-label"?: string;
14
- /** Optional custom styles. */
15
- style?: StyleType;
16
- /**
17
- * Test ID used for e2e testing.
18
- *
19
- * In this case, this component is internal, so `testId` is composed with
20
- * the `testId` passed down from the Dialog variant + a suffix to scope it
21
- * to this component.
22
- *
23
- * @example
24
- * For testId="some-random-id"
25
- * The result will be: `some-random-id-modal-panel`
26
- */
27
- testId?: string;
28
- };
29
-
30
- // TODO[WB-2090]: Update to shared CloseButton component
31
- export const DismissButton = (props: Props) => {
32
- const {onClick, style, testId} = props;
33
- return (
34
- <IconButton
35
- icon={xIcon}
36
- aria-label={props["aria-label"] || "Close"}
37
- onClick={onClick}
38
- kind="tertiary"
39
- actionType="neutral"
40
- style={[componentStyles.root, style]}
41
- testId={testId}
42
- />
43
- );
44
- };
45
-
46
- const componentStyles = StyleSheet.create({
47
- root: {
48
- position: "absolute",
49
- // insetInlineEnd supports both RTL and LTR layouts
50
- insetInlineEnd: sizing.size_080,
51
- top: sizing.size_080,
52
- // This is to allow the button to be tab-ordered before the component
53
- // content but still be above the content.
54
- zIndex: 1,
55
-
56
- // NOTE: IconButton uses :focus-visible, which is not supported for
57
- // programmatic focus. This is a workaround to make sure the focus
58
- // outline is visible when this control is focused.
59
- ":focus": focusStyles.focus[":focus-visible"],
60
- },
61
- });
package/src/index.ts DELETED
@@ -1,3 +0,0 @@
1
- import Card from "./components/card";
2
-
3
- export {Card};
@@ -1,16 +0,0 @@
1
- {
2
- "exclude": ["dist"],
3
- "extends": "../tsconfig-shared.json",
4
- "compilerOptions": {
5
- "outDir": "./dist",
6
- "rootDir": "src",
7
- },
8
- "references": [
9
- {"path": "../wonder-blocks-core/tsconfig-build.json"},
10
- {"path": "../wonder-blocks-icon/tsconfig-build.json"},
11
- {"path": "../wonder-blocks-icon-button/tsconfig-build.json"},
12
- {"path": "../wonder-blocks-tokens/tsconfig-build.json"},
13
- {"path": "../wonder-blocks-button/tsconfig-build.json"},
14
- {"path": "../wonder-blocks-link/tsconfig-build.json"},
15
- ]
16
- }