@khanacademy/wonder-blocks-card 0.0.0-PR2816-20251007213621 → 0.0.0-PR2816-20251007220942
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 +1 -1
- package/dist/es/index.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/src/__tests__/components/card.test.tsx +182 -0
- package/src/components/card.tsx +62 -31
package/CHANGELOG.md
CHANGED
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
|
|
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
|
|
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-PR2816-
|
|
3
|
+
"version": "0.0.0-PR2816-20251007220942",
|
|
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-icon-button": "10.5.2",
|
|
17
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": {
|
|
@@ -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
|
});
|
package/src/components/card.tsx
CHANGED
|
@@ -191,56 +191,87 @@ const Card = React.forwardRef(function Card(
|
|
|
191
191
|
);
|
|
192
192
|
});
|
|
193
193
|
|
|
194
|
-
|
|
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 = ({
|
|
195
222
|
background,
|
|
196
223
|
borderRadius,
|
|
197
224
|
paddingSize,
|
|
198
225
|
elevation,
|
|
199
|
-
}: StyleProps) => {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
},
|
|
215
|
-
elevation: {
|
|
216
|
-
none: "none",
|
|
217
|
-
low: boxShadow.low,
|
|
218
|
-
},
|
|
219
|
-
} as const;
|
|
226
|
+
}: StyleProps): string => {
|
|
227
|
+
return `${background || "default"}-${borderRadius || "small"}-${
|
|
228
|
+
paddingSize || "small"
|
|
229
|
+
}-${elevation || "none"}`;
|
|
230
|
+
};
|
|
231
|
+
|
|
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
|
+
}
|
|
220
241
|
|
|
242
|
+
const {background, borderRadius, paddingSize, elevation} = props;
|
|
221
243
|
const isBackgroundColorStyle =
|
|
222
244
|
background === "base-subtle" || background === "base-default";
|
|
223
245
|
|
|
224
|
-
|
|
246
|
+
// Generate new styles
|
|
247
|
+
const newStyles = StyleSheet.create({
|
|
225
248
|
root: {
|
|
249
|
+
// Background styles
|
|
226
250
|
...(isBackgroundColorStyle && {
|
|
227
251
|
backgroundColor: styleMap.backgroundColor[background],
|
|
228
252
|
}),
|
|
229
|
-
//
|
|
230
|
-
...(!isBackgroundColorStyle &&
|
|
231
|
-
background
|
|
232
|
-
|
|
233
|
-
|
|
253
|
+
// Background image styles
|
|
254
|
+
...(!isBackgroundColorStyle &&
|
|
255
|
+
background && {
|
|
256
|
+
background: `url(${background})`,
|
|
257
|
+
backgroundSize: "cover",
|
|
258
|
+
}),
|
|
259
|
+
// Common styles
|
|
234
260
|
borderColor: semanticColor.core.border.neutral.subtle,
|
|
235
261
|
borderStyle: "solid",
|
|
236
|
-
borderRadius: borderRadius && styleMap.borderRadius[borderRadius],
|
|
237
262
|
borderWidth: border.width.thin,
|
|
238
|
-
boxShadow: elevation && styleMap.elevation[elevation],
|
|
239
|
-
padding: paddingSize && styleMap.padding[paddingSize],
|
|
240
263
|
minInlineSize: sizing.size_280,
|
|
241
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],
|
|
242
269
|
},
|
|
243
270
|
});
|
|
271
|
+
|
|
272
|
+
// Cache the styles
|
|
273
|
+
dynamicStyles[styleKey] = newStyles;
|
|
274
|
+
return newStyles;
|
|
244
275
|
};
|
|
245
276
|
|
|
246
277
|
export default Card;
|