@khanacademy/wonder-blocks-card 0.0.0-PR2902-20251211165942 → 0.0.0-PR2904-20251211205241

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