@khanacademy/wonder-blocks-card 0.0.0-PR2813-20251007190409 → 0.0.0-PR2816-20251007220407

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 CHANGED
@@ -1,13 +1,10 @@
1
1
  # @khanacademy/wonder-blocks-card
2
2
 
3
- ## 0.0.0-PR2813-20251007190409
3
+ ## 0.0.0-PR2816-20251007220407
4
4
 
5
- ### Patch Changes
5
+ ### Minor Changes
6
6
 
7
- - Updated dependencies [c8b93d9]
8
- - @khanacademy/wonder-blocks-core@0.0.0-PR2813-20251007190409
9
- - @khanacademy/wonder-blocks-icon-button@0.0.0-PR2813-20251007190409
10
- - @khanacademy/wonder-blocks-tokens@14.0.0
7
+ - be020e9: Adds support for image backgrounds via the background prop
11
8
 
12
9
  ## 1.1.0
13
10
 
package/dist/es/index.js CHANGED
@@ -9,6 +9,6 @@ import { focusStyles } from '@khanacademy/wonder-blocks-styles';
9
9
 
10
10
  const DismissButton=props=>{const{onClick,style,testId}=props;return jsx(IconButton,{icon:xIcon,"aria-label":props["aria-label"]||"Close",onClick:onClick,kind:"tertiary",actionType:"neutral",style:[componentStyles.root,style],testId:testId})};const componentStyles=StyleSheet.create({root:{position:"absolute",insetInlineEnd:sizing.size_080,top:sizing.size_080,zIndex:1,":focus":focusStyles.focus[":focus-visible"]}});
11
11
 
12
- const Card=React.forwardRef(function Card(props,ref){const{styles,labels,tag,testId,background="base-default",borderRadius="small",paddingSize="small",elevation="none",children,onDismiss,inert}=props;const componentStyles=getComponentStyles({background,borderRadius,paddingSize,elevation});return jsxs(View,{"aria-label":labels?.cardAriaLabel,style:[componentStyles.root,styles?.root],ref:ref,tag:tag,testId:testId,inert:inert?"":undefined,children:[onDismiss?jsx(DismissButton,{"aria-label":labels?.dismissButtonAriaLabel||"Close",onClick:e=>onDismiss?.(e)}):null,children]})});const getComponentStyles=({background,borderRadius,paddingSize,elevation})=>{const styleMap={backgroundColor:{"base-subtle":semanticColor.core.background.base.subtle,"base-default":semanticColor.core.background.base.default},borderRadius:{small:border.radius.radius_080,medium:border.radius.radius_120},padding:{none:sizing.size_0,small:sizing.size_160,medium:sizing.size_240},elevation:{none:"none",low:boxShadow.low}};return StyleSheet.create({root:{backgroundColor:background&&styleMap.backgroundColor[background],borderColor:semanticColor.core.border.neutral.subtle,borderStyle:"solid",borderRadius:borderRadius&&styleMap.borderRadius[borderRadius],borderWidth:border.width.thin,boxShadow:elevation&&styleMap.elevation[elevation],padding:paddingSize&&styleMap.padding[paddingSize],minInlineSize:sizing.size_280,position:"relative"}})};
12
+ const Card=React.forwardRef(function Card(props,ref){const{styles,labels,tag,testId,background="base-default",borderRadius="small",paddingSize="small",elevation="none",children,onDismiss,inert}=props;const componentStyles=getComponentStyles({background,borderRadius,paddingSize,elevation});return jsxs(View,{"aria-label":labels?.cardAriaLabel,style:[componentStyles.root,styles?.root],ref:ref,tag:tag,testId:testId,inert:inert?"":undefined,children:[onDismiss?jsx(DismissButton,{"aria-label":labels?.dismissButtonAriaLabel||"Close",onClick:e=>onDismiss?.(e)}):null,children]})});const styleMap={backgroundColor:{"base-subtle":semanticColor.core.background.base.subtle,"base-default":semanticColor.core.background.base.default},borderRadius:{small:border.radius.radius_080,medium:border.radius.radius_120},padding:{none:sizing.size_0,small:sizing.size_160,medium:sizing.size_240},elevation:{none:"none",low:boxShadow.low}};const dynamicStyles={};const getStyleKey=({background,borderRadius,paddingSize,elevation})=>{return `${background||"default"}-${borderRadius||"small"}-${paddingSize||"small"}-${elevation||"none"}`};const getComponentStyles=props=>{const styleKey=getStyleKey(props);if(dynamicStyles[styleKey]){return dynamicStyles[styleKey]}const{background,borderRadius,paddingSize,elevation}=props;const isBackgroundColorStyle=background==="base-subtle"||background==="base-default";const newStyles=StyleSheet.create({root:{...isBackgroundColorStyle&&{backgroundColor:styleMap.backgroundColor[background]},...!isBackgroundColorStyle&&background&&{background:`url(${background})`,backgroundSize:"cover"},borderColor:semanticColor.core.border.neutral.subtle,borderStyle:"solid",borderWidth:border.width.thin,minInlineSize:sizing.size_280,position:"relative",borderRadius:borderRadius&&styleMap.borderRadius[borderRadius],boxShadow:elevation&&styleMap.elevation[elevation],padding:paddingSize&&styleMap.padding[paddingSize]}});dynamicStyles[styleKey]=newStyles;return newStyles};
13
13
 
14
14
  export { Card };
package/dist/index.js CHANGED
@@ -37,6 +37,6 @@ var IconButton__default = /*#__PURE__*/_interopDefaultLegacy(IconButton);
37
37
 
38
38
  const DismissButton=props=>{const{onClick,style,testId}=props;return jsxRuntime.jsx(IconButton__default["default"],{icon:xIcon__default["default"],"aria-label":props["aria-label"]||"Close",onClick:onClick,kind:"tertiary",actionType:"neutral",style:[componentStyles.root,style],testId:testId})};const componentStyles=aphrodite.StyleSheet.create({root:{position:"absolute",insetInlineEnd:wonderBlocksTokens.sizing.size_080,top:wonderBlocksTokens.sizing.size_080,zIndex:1,":focus":wonderBlocksStyles.focusStyles.focus[":focus-visible"]}});
39
39
 
40
- const Card=React__namespace.forwardRef(function Card(props,ref){const{styles,labels,tag,testId,background="base-default",borderRadius="small",paddingSize="small",elevation="none",children,onDismiss,inert}=props;const componentStyles=getComponentStyles({background,borderRadius,paddingSize,elevation});return jsxRuntime.jsxs(wonderBlocksCore.View,{"aria-label":labels?.cardAriaLabel,style:[componentStyles.root,styles?.root],ref:ref,tag:tag,testId:testId,inert:inert?"":undefined,children:[onDismiss?jsxRuntime.jsx(DismissButton,{"aria-label":labels?.dismissButtonAriaLabel||"Close",onClick:e=>onDismiss?.(e)}):null,children]})});const getComponentStyles=({background,borderRadius,paddingSize,elevation})=>{const styleMap={backgroundColor:{"base-subtle":wonderBlocksTokens.semanticColor.core.background.base.subtle,"base-default":wonderBlocksTokens.semanticColor.core.background.base.default},borderRadius:{small:wonderBlocksTokens.border.radius.radius_080,medium:wonderBlocksTokens.border.radius.radius_120},padding:{none:wonderBlocksTokens.sizing.size_0,small:wonderBlocksTokens.sizing.size_160,medium:wonderBlocksTokens.sizing.size_240},elevation:{none:"none",low:wonderBlocksTokens.boxShadow.low}};return aphrodite.StyleSheet.create({root:{backgroundColor:background&&styleMap.backgroundColor[background],borderColor:wonderBlocksTokens.semanticColor.core.border.neutral.subtle,borderStyle:"solid",borderRadius:borderRadius&&styleMap.borderRadius[borderRadius],borderWidth:wonderBlocksTokens.border.width.thin,boxShadow:elevation&&styleMap.elevation[elevation],padding:paddingSize&&styleMap.padding[paddingSize],minInlineSize:wonderBlocksTokens.sizing.size_280,position:"relative"}})};
40
+ const Card=React__namespace.forwardRef(function Card(props,ref){const{styles,labels,tag,testId,background="base-default",borderRadius="small",paddingSize="small",elevation="none",children,onDismiss,inert}=props;const componentStyles=getComponentStyles({background,borderRadius,paddingSize,elevation});return jsxRuntime.jsxs(wonderBlocksCore.View,{"aria-label":labels?.cardAriaLabel,style:[componentStyles.root,styles?.root],ref:ref,tag:tag,testId:testId,inert:inert?"":undefined,children:[onDismiss?jsxRuntime.jsx(DismissButton,{"aria-label":labels?.dismissButtonAriaLabel||"Close",onClick:e=>onDismiss?.(e)}):null,children]})});const styleMap={backgroundColor:{"base-subtle":wonderBlocksTokens.semanticColor.core.background.base.subtle,"base-default":wonderBlocksTokens.semanticColor.core.background.base.default},borderRadius:{small:wonderBlocksTokens.border.radius.radius_080,medium:wonderBlocksTokens.border.radius.radius_120},padding:{none:wonderBlocksTokens.sizing.size_0,small:wonderBlocksTokens.sizing.size_160,medium:wonderBlocksTokens.sizing.size_240},elevation:{none:"none",low:wonderBlocksTokens.boxShadow.low}};const dynamicStyles={};const getStyleKey=({background,borderRadius,paddingSize,elevation})=>{return `${background||"default"}-${borderRadius||"small"}-${paddingSize||"small"}-${elevation||"none"}`};const getComponentStyles=props=>{const styleKey=getStyleKey(props);if(dynamicStyles[styleKey]){return dynamicStyles[styleKey]}const{background,borderRadius,paddingSize,elevation}=props;const isBackgroundColorStyle=background==="base-subtle"||background==="base-default";const newStyles=aphrodite.StyleSheet.create({root:{...isBackgroundColorStyle&&{backgroundColor:styleMap.backgroundColor[background]},...!isBackgroundColorStyle&&background&&{background:`url(${background})`,backgroundSize:"cover"},borderColor:wonderBlocksTokens.semanticColor.core.border.neutral.subtle,borderStyle:"solid",borderWidth:wonderBlocksTokens.border.width.thin,minInlineSize:wonderBlocksTokens.sizing.size_280,position:"relative",borderRadius:borderRadius&&styleMap.borderRadius[borderRadius],boxShadow:elevation&&styleMap.elevation[elevation],padding:paddingSize&&styleMap.padding[paddingSize]}});dynamicStyles[styleKey]=newStyles;return newStyles};
41
41
 
42
42
  exports.Card = Card;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khanacademy/wonder-blocks-card",
3
- "version": "0.0.0-PR2813-20251007190409",
3
+ "version": "0.0.0-PR2816-20251007220407",
4
4
  "design": "v1",
5
5
  "description": "Card component for Wonder Blocks.",
6
6
  "main": "dist/index.js",
@@ -13,8 +13,8 @@
13
13
  "access": "public"
14
14
  },
15
15
  "dependencies": {
16
- "@khanacademy/wonder-blocks-core": "0.0.0-PR2813-20251007190409",
17
- "@khanacademy/wonder-blocks-icon-button": "0.0.0-PR2813-20251007190409",
16
+ "@khanacademy/wonder-blocks-core": "12.4.0",
17
+ "@khanacademy/wonder-blocks-icon-button": "10.5.2",
18
18
  "@khanacademy/wonder-blocks-tokens": "14.0.0"
19
19
  },
20
20
  "peerDependencies": {
@@ -23,7 +23,7 @@
23
23
  "@phosphor-icons/core": "^2.0.2"
24
24
  },
25
25
  "devDependencies": {
26
- "@khanacademy/wb-dev-build-settings": "0.0.0-PR2813-20251007190409"
26
+ "@khanacademy/wb-dev-build-settings": "3.2.0"
27
27
  },
28
28
  "scripts": {
29
29
  "test": "echo \"Error: no test specified\" && exit 1"
@@ -1,6 +1,12 @@
1
1
  import * as React from "react";
2
2
  import {render, screen} from "@testing-library/react";
3
3
  import {userEvent} from "@testing-library/user-event";
4
+ import {
5
+ boxShadow,
6
+ border,
7
+ semanticColor,
8
+ sizing,
9
+ } from "@khanacademy/wonder-blocks-tokens";
4
10
 
5
11
  import Card from "../../components/card";
6
12
 
@@ -191,4 +197,180 @@ describe("Card", () => {
191
197
  expect(secondChild).toBeInTheDocument();
192
198
  });
193
199
  });
200
+
201
+ describe("Style application", () => {
202
+ it("should apply default styles", () => {
203
+ // Arrange
204
+ const testId = "test-card";
205
+
206
+ // Act
207
+ render(<Card testId={testId}>Content</Card>);
208
+ const card = screen.getByTestId(testId);
209
+
210
+ // Assert
211
+ expect(card).toHaveStyle({
212
+ position: "relative",
213
+ borderStyle: "solid",
214
+ });
215
+ });
216
+
217
+ it("should apply base-subtle background", () => {
218
+ // Arrange
219
+ const testId = "test-card";
220
+
221
+ // Act
222
+ render(
223
+ <Card testId={testId} background="base-subtle">
224
+ Content
225
+ </Card>,
226
+ );
227
+ const card = screen.getByTestId(testId);
228
+
229
+ // Assert
230
+ expect(card).toHaveStyle({
231
+ backgroundColor: semanticColor.core.background.base.subtle,
232
+ });
233
+ });
234
+
235
+ it("should apply medium border radius", () => {
236
+ // Arrange
237
+ const testId = "test-card";
238
+
239
+ // Act
240
+ render(
241
+ <Card testId={testId} borderRadius="medium">
242
+ Content
243
+ </Card>,
244
+ );
245
+ const card = screen.getByTestId(testId);
246
+
247
+ // Assert
248
+ expect(card).toHaveStyle({
249
+ borderRadius: border.radius.radius_120,
250
+ });
251
+ });
252
+
253
+ it("should apply medium padding", () => {
254
+ // Arrange
255
+ const testId = "test-card";
256
+
257
+ // Act
258
+ render(
259
+ <Card testId={testId} paddingSize="medium">
260
+ Content
261
+ </Card>,
262
+ );
263
+ const card = screen.getByTestId(testId);
264
+
265
+ // Assert
266
+ expect(card).toHaveStyle({
267
+ padding: sizing.size_240,
268
+ });
269
+ });
270
+
271
+ it("should apply low elevation", () => {
272
+ // Arrange
273
+ const testId = "test-card";
274
+
275
+ // Act
276
+ render(
277
+ <Card testId={testId} elevation="low">
278
+ Content
279
+ </Card>,
280
+ );
281
+ const card = screen.getByTestId(testId);
282
+
283
+ // Assert
284
+ expect(card).toHaveStyle({
285
+ boxShadow: boxShadow.low,
286
+ });
287
+ });
288
+
289
+ it("should apply image background", () => {
290
+ // Arrange
291
+ const testId = "test-card";
292
+ const testImage = Image;
293
+
294
+ // Act
295
+ render(
296
+ <Card testId={testId} background={testImage}>
297
+ Content
298
+ </Card>,
299
+ );
300
+ const card = screen.getByTestId(testId);
301
+
302
+ // Assert
303
+ expect(card).toHaveStyle({
304
+ backgroundSize: "cover",
305
+ });
306
+ });
307
+ });
308
+
309
+ describe("Style application", () => {
310
+ it("should apply custom margin style", () => {
311
+ // Arrange
312
+ const testId = "test-card";
313
+ const customStyle = {marginTop: "10px"};
314
+
315
+ // Act
316
+ render(
317
+ <Card testId={testId} styles={{root: customStyle}}>
318
+ Content
319
+ </Card>,
320
+ );
321
+
322
+ // Assert
323
+ expect(screen.getByTestId(testId)).toHaveStyle(customStyle);
324
+ });
325
+
326
+ it("should apply custom padding style", () => {
327
+ // Arrange
328
+ const testId = "test-card";
329
+ const customStyle = {padding: "20px"};
330
+
331
+ // Act
332
+ render(
333
+ <Card testId={testId} styles={{root: customStyle}}>
334
+ Content
335
+ </Card>,
336
+ );
337
+
338
+ // Assert
339
+ expect(screen.getByTestId(testId)).toHaveStyle(customStyle);
340
+ });
341
+
342
+ it("should apply custom background color", () => {
343
+ // Arrange
344
+ const testId = "test-card";
345
+ const customStyle = {backgroundColor: "rgb(255, 0, 0)"};
346
+
347
+ // Act
348
+ render(
349
+ <Card testId={testId} styles={{root: customStyle}}>
350
+ Content
351
+ </Card>,
352
+ );
353
+
354
+ // Assert
355
+ expect(screen.getByTestId(testId)).toHaveStyle(customStyle);
356
+ });
357
+
358
+ it("should maintain default styles with custom styles", () => {
359
+ // Arrange
360
+ const testId = "test-card";
361
+ const customStyle = {marginTop: "10px"};
362
+
363
+ // Act
364
+ render(
365
+ <Card testId={testId} styles={{root: customStyle}}>
366
+ Content
367
+ </Card>,
368
+ );
369
+
370
+ // Assert
371
+ const element = screen.getByTestId(testId);
372
+ expect(element).toHaveStyle(customStyle);
373
+ expect(element).toHaveStyle({position: "relative"}); // A default style
374
+ });
375
+ });
194
376
  });
@@ -64,6 +64,9 @@ import Card from "../../components/card";
64
64
  Content
65
65
  </Card>;
66
66
 
67
+ // @ts-expect-error - onClick is not allowed on Card wrapper
68
+ <Card onClick={() => {}}>Content</Card>;
69
+
67
70
  /**
68
71
  * Card with different HTML tags
69
72
  */
@@ -79,10 +79,13 @@ type StyleProps = {
79
79
  * This can be one of:
80
80
  * - `"base-subtle"` (color), `semanticColor.core.background.base.subtle`: a light gray background.
81
81
  * - `"base-default"` (color), `semanticColor.core.background.base.default`: a white background.
82
+ * - `Image` (image), a URL string for a background image. Can be an imported image file or a URL string.
83
+ *
84
+ * For additional background styling such as repeat or size, use the `styles.root` prop to pass in custom styles.
82
85
  *
83
86
  * Default: `"base-default"`
84
87
  */
85
- background?: "base-subtle" | "base-default";
88
+ background?: "base-subtle" | "base-default" | typeof Image;
86
89
  /**
87
90
  * The border radius of the card, as a string identifier that matches a border.radius token.
88
91
  * This can be one of:
@@ -188,46 +191,87 @@ const Card = React.forwardRef(function Card(
188
191
  );
189
192
  });
190
193
 
191
- const getComponentStyles = ({
194
+ // Map prop values to tokens
195
+ const styleMap = {
196
+ backgroundColor: {
197
+ "base-subtle": semanticColor.core.background.base.subtle,
198
+ "base-default": semanticColor.core.background.base.default,
199
+ },
200
+ borderRadius: {
201
+ small: border.radius.radius_080,
202
+ medium: border.radius.radius_120,
203
+ },
204
+ padding: {
205
+ none: sizing.size_0,
206
+ small: sizing.size_160,
207
+ medium: sizing.size_240,
208
+ },
209
+ elevation: {
210
+ none: "none",
211
+ low: boxShadow.low,
212
+ },
213
+ } as const;
214
+
215
+ // Cache for dynamically generated styles
216
+ const dynamicStyles: Record<string, any> = {};
217
+
218
+ /**
219
+ * Generates a unique key for caching styles based on prop combinations
220
+ */
221
+ const getStyleKey = ({
192
222
  background,
193
223
  borderRadius,
194
224
  paddingSize,
195
225
  elevation,
196
- }: StyleProps) => {
197
- // Map prop values to tokens
198
- const styleMap = {
199
- backgroundColor: {
200
- "base-subtle": semanticColor.core.background.base.subtle,
201
- "base-default": semanticColor.core.background.base.default,
202
- },
203
- borderRadius: {
204
- small: border.radius.radius_080,
205
- medium: border.radius.radius_120,
206
- },
207
- padding: {
208
- none: sizing.size_0,
209
- small: sizing.size_160,
210
- medium: sizing.size_240,
211
- },
212
- elevation: {
213
- none: "none",
214
- low: boxShadow.low,
215
- },
216
- } as const;
226
+ }: StyleProps): string => {
227
+ return `${background || "default"}-${borderRadius || "small"}-${
228
+ paddingSize || "small"
229
+ }-${elevation || "none"}`;
230
+ };
217
231
 
218
- return StyleSheet.create({
232
+ /**
233
+ * Generates the component styles with caching for better performance
234
+ */
235
+ const getComponentStyles = (props: StyleProps) => {
236
+ const styleKey = getStyleKey(props);
237
+ // Return cached styles if they exist
238
+ if (dynamicStyles[styleKey]) {
239
+ return dynamicStyles[styleKey];
240
+ }
241
+
242
+ const {background, borderRadius, paddingSize, elevation} = props;
243
+ const isBackgroundColorStyle =
244
+ background === "base-subtle" || background === "base-default";
245
+
246
+ // Generate new styles
247
+ const newStyles = StyleSheet.create({
219
248
  root: {
220
- backgroundColor: background && styleMap.backgroundColor[background],
249
+ // Background styles
250
+ ...(isBackgroundColorStyle && {
251
+ backgroundColor: styleMap.backgroundColor[background],
252
+ }),
253
+ // Background image styles
254
+ ...(!isBackgroundColorStyle &&
255
+ background && {
256
+ background: `url(${background})`,
257
+ backgroundSize: "cover",
258
+ }),
259
+ // Common styles
221
260
  borderColor: semanticColor.core.border.neutral.subtle,
222
261
  borderStyle: "solid",
223
- borderRadius: borderRadius && styleMap.borderRadius[borderRadius],
224
262
  borderWidth: border.width.thin,
225
- boxShadow: elevation && styleMap.elevation[elevation],
226
- padding: paddingSize && styleMap.padding[paddingSize],
227
263
  minInlineSize: sizing.size_280,
228
264
  position: "relative",
265
+ // Optional styles based on props
266
+ borderRadius: borderRadius && styleMap.borderRadius[borderRadius],
267
+ boxShadow: elevation && styleMap.elevation[elevation],
268
+ padding: paddingSize && styleMap.padding[paddingSize],
229
269
  },
230
270
  });
271
+
272
+ // Cache the styles
273
+ dynamicStyles[styleKey] = newStyles;
274
+ return newStyles;
231
275
  };
232
276
 
233
277
  export default Card;