@hyphen/hyphen-components 8.0.1 → 9.0.0-beta.1

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.
Files changed (66) hide show
  1. package/dist/css/utilities.css +1 -1
  2. package/dist/css/utilities.css.map +1 -1
  3. package/dist/hyphen-components.cjs.development.js +353 -313
  4. package/dist/hyphen-components.cjs.development.js.map +1 -1
  5. package/dist/hyphen-components.cjs.production.min.js +2 -2
  6. package/dist/hyphen-components.cjs.production.min.js.map +1 -1
  7. package/dist/hyphen-components.esm.js +357 -314
  8. package/dist/hyphen-components.esm.js.map +1 -1
  9. package/dist/index.d.ts +222 -212
  10. package/package.json +6 -5
  11. package/src/components/Alert/Alert.tsx +5 -6
  12. package/src/components/Badge/Badge.test.tsx +12 -0
  13. package/src/components/Badge/Badge.tsx +52 -20
  14. package/src/components/Box/Box.mdx +1 -1
  15. package/src/components/Box/Box.stories.tsx +1 -1
  16. package/src/components/Box/Box.test.tsx +24 -1
  17. package/src/components/Box/Box.tsx +108 -82
  18. package/src/components/Card/Card.tsx +5 -1
  19. package/src/components/Card/components/CardFooter/CardFooter.tsx +0 -4
  20. package/src/components/Card/components/CardHeader/CardHeader.tsx +4 -1
  21. package/src/components/Card/components/CardSection/CardSection.tsx +4 -5
  22. package/src/components/CheckboxInput/CheckboxInput.test.tsx +17 -0
  23. package/src/components/CheckboxInput/CheckboxInput.tsx +15 -4
  24. package/src/components/CheckboxInput/components/Checkbox.test.tsx +36 -0
  25. package/src/components/CheckboxInput/components/Checkbox.tsx +8 -2
  26. package/src/components/Details/Details.test.tsx +8 -0
  27. package/src/components/Details/Details.tsx +5 -2
  28. package/src/components/Details/DetailsSummary.tsx +4 -1
  29. package/src/components/Drawer/Drawer.test.tsx +8 -0
  30. package/src/components/Drawer/Drawer.tsx +1 -1
  31. package/src/components/FormLabel/FormLabel.tsx +6 -3
  32. package/src/components/Formik/FormikCheckboxInput/FormikCheckboxInput.test.tsx +9 -0
  33. package/src/components/Formik/FormikCheckboxInput/FormikCheckboxInput.tsx +1 -0
  34. package/src/components/Formik/FormikSelectInput/FormikSelectInput.tsx +21 -11
  35. package/src/components/Formik/FormikSelectInputNative/FormikSelectInputNative.tsx +1 -1
  36. package/src/components/Formik/FormikTimePicker/FormikTimePicker.tsx +1 -1
  37. package/src/components/Heading/Heading.tsx +5 -6
  38. package/src/components/Icon/Icon.test.tsx +8 -0
  39. package/src/components/Icon/Icon.tsx +6 -7
  40. package/src/components/Modal/Modal.test.tsx +31 -1
  41. package/src/components/Modal/Modal.tsx +13 -7
  42. package/src/components/Popover/Popover.test.tsx +3 -4
  43. package/src/components/RadioGroup/RadioGroup.tsx +11 -6
  44. package/src/components/RadioGroup/RadioInput/RadioInputIcon.tsx +0 -4
  45. package/src/components/SelectInput/SelectInput.test.tsx +18 -0
  46. package/src/components/SelectInput/SelectInput.tsx +47 -27
  47. package/src/components/SelectInputInset/SelectInputInset.tsx +8 -9
  48. package/src/components/SelectInputNative/SelectInputNative.test.tsx +24 -0
  49. package/src/components/SelectInputNative/SelectInputNative.tsx +79 -14
  50. package/src/components/Switch/Switch.tsx +1 -1
  51. package/src/components/Table/Table.mdx +21 -1
  52. package/src/components/Table/Table.stories.tsx +5 -1
  53. package/src/components/Table/Table.tsx +7 -7
  54. package/src/components/Table/TableBody/TableBody.test.tsx +89 -1
  55. package/src/components/Table/TableBody/TableBody.tsx +23 -8
  56. package/src/components/Table/TableHead/TableHead.tsx +6 -6
  57. package/src/components/Table/TableHead/TableHeaderCell/TableHeaderCell.tsx +6 -6
  58. package/src/components/Table/common/TableRow/TableRow.test.tsx +17 -1
  59. package/src/components/Table/common/TableRow/TableRow.tsx +30 -10
  60. package/src/components/TextInput/TextInput.tsx +10 -8
  61. package/src/components/TextInputInset/TextInputInset.tsx +5 -6
  62. package/src/components/TextareaInput/TextareaInput.tsx +4 -5
  63. package/src/components/TextareaInputInset/TextareaInputInset.tsx +10 -17
  64. package/src/components/Toast/Toast.stories.tsx +5 -1
  65. package/src/lib/getColumnKeys.ts +5 -3
  66. package/src/types/index.ts +6 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyphen/hyphen-components",
3
- "version": "8.0.1",
3
+ "version": "9.0.0-beta.1",
4
4
  "license": "MIT",
5
5
  "author": {
6
6
  "name": "@hyphen"
@@ -56,7 +56,7 @@
56
56
  "react-select": "^5.10.0",
57
57
  "react-select-event": "^5.5.1",
58
58
  "uuid": "^11.1.1",
59
- "@hyphen/hyphen-design-tokens": "^8.0.1"
59
+ "@hyphen/hyphen-design-tokens": "^9.0.0-beta.1"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@babel/core": "^7.27.4",
@@ -129,12 +129,13 @@
129
129
  "build-storybook": "storybook build",
130
130
  "build-storybook-docs": "storybook build --docs",
131
131
  "storybook-docs": "storybook dev --docs",
132
- "lint": "eslint src --ext .ts,.tsx",
133
- "lint-fix": "eslint src --ext .ts,.tsx --fix",
132
+ "lint": "eslint src type-tests --ext .ts,.tsx",
133
+ "lint-fix": "eslint src type-tests --ext .ts,.tsx --fix",
134
134
  "size": "size-limit",
135
135
  "start": "tsup --watch",
136
136
  "storybook": "storybook dev -p 6006",
137
- "test": "jest --passWithNoTests",
137
+ "test": "pnpm test:types && jest --passWithNoTests",
138
+ "test:types": "tsc --project tsconfig.type-tests.json",
138
139
  "test-watch": "jest --coverage --watch",
139
140
  "chromatic": "chromatic --only-changed"
140
141
  }
@@ -8,13 +8,13 @@ import React, {
8
8
  memo,
9
9
  } from 'react';
10
10
  import classNames from 'classnames';
11
- import { Box } from '../Box/Box';
11
+ import { Box, BoxProps } from '../Box/Box';
12
12
  import { Icon } from '../Icon/Icon';
13
13
  import styles from './Alert.module.scss';
14
14
  import { AlertVariant } from './Alert.types';
15
15
  import { ALERT_ICONS_MAP } from './Alert.constants';
16
16
 
17
- export interface AlertProps {
17
+ interface AlertOwnProps {
18
18
  /**
19
19
  * The alert's contents.
20
20
  */
@@ -50,12 +50,11 @@ export interface AlertProps {
50
50
  * The type/color of the alert to show.
51
51
  */
52
52
  variant?: AlertVariant;
53
- /**
54
- * Additional props to be spread to rendered element
55
- */
56
- [x: string]: any; // eslint-disable-line
57
53
  }
58
54
 
55
+ export type AlertProps = AlertOwnProps &
56
+ Omit<BoxProps<'div'>, keyof AlertOwnProps | 'as'>;
57
+
59
58
  const AlertComponent: FC<AlertProps> = ({
60
59
  children,
61
60
  className = '',
@@ -48,6 +48,18 @@ const getBadge = (text: string): HTMLElement =>
48
48
  screen.getByText(text).closest('.badge') as HTMLElement;
49
49
 
50
50
  describe('Badge', () => {
51
+ test('accepts attributes and refs for the element selected by as', () => {
52
+ const buttonRef = React.createRef<HTMLButtonElement>();
53
+
54
+ render(
55
+ <Badge as="button" type="button" disabled ref={buttonRef}>
56
+ Action
57
+ </Badge>
58
+ );
59
+
60
+ expect(buttonRef.current).toBeDisabled();
61
+ });
62
+
51
63
  test('Badge correctly renders with base props', () => {
52
64
  render(<Badge>hello</Badge>);
53
65
  const badge = getBadge('hello');
@@ -1,9 +1,14 @@
1
- import React, { Children, ReactNode, forwardRef } from 'react';
1
+ import React, {
2
+ Children,
3
+ ReactElement,
4
+ ReactNode,
5
+ forwardRef,
6
+ } from 'react';
2
7
  import classNames from 'classnames';
3
8
  import { ResponsiveProp } from '../../types';
4
9
  import { generateResponsiveClasses } from '../../lib/generateResponsiveClasses';
5
10
  import styles from './Badge.module.scss';
6
- import { Box, BoxProps } from '../Box/Box';
11
+ import { Box, BoxElement, BoxProps, BoxRef } from '../Box/Box';
7
12
 
8
13
  export type BadgeSize = 'sm' | 'md' | 'lg';
9
14
 
@@ -50,7 +55,7 @@ const renderBadgeChildren = (children: ReactNode) =>
50
55
  )
51
56
  );
52
57
 
53
- export interface BadgeProps extends Omit<BoxProps, 'color' | 'radius'> {
58
+ interface BadgeOwnProps {
54
59
  /**
55
60
  * The color of the badge. Accepts a hue, or one of the semantic names
56
61
  * (`danger`, `success`, `warning`, `info`) which map onto `red`, `green`, `yellow` and `blue`.
@@ -70,9 +75,22 @@ export interface BadgeProps extends Omit<BoxProps, 'color' | 'radius'> {
70
75
  variant?: BadgeVariant;
71
76
  }
72
77
 
73
- export const Badge = forwardRef<HTMLDivElement, BadgeProps>(
74
- (
75
- {
78
+ export type BadgeProps<T extends BoxElement = 'div'> = BadgeOwnProps &
79
+ Omit<BoxProps<T>, keyof BadgeOwnProps>;
80
+
81
+ type BadgeComponent = {
82
+ <T extends BoxElement>(
83
+ props: BadgeProps<T> & { as: T; ref?: BoxRef<T> }
84
+ ): ReactElement | null;
85
+ (props: BadgeProps<'div'> & { ref?: BoxRef<'div'> }): ReactElement | null;
86
+ displayName?: string;
87
+ };
88
+
89
+ const BadgeBaseComponent = <T extends BoxElement = 'div'>(
90
+ props: BadgeProps<T>,
91
+ ref: BoxRef<T>
92
+ ) => {
93
+ const {
76
94
  className = '',
77
95
  color = 'grey',
78
96
  radius = 'full',
@@ -80,9 +98,7 @@ export const Badge = forwardRef<HTMLDivElement, BadgeProps>(
80
98
  size = 'md',
81
99
  children,
82
100
  ...restProps
83
- },
84
- ref
85
- ) => {
101
+ } = props as BadgeOwnProps & Omit<BoxProps<T>, keyof BadgeOwnProps>;
86
102
  const responsiveClasses = generateResponsiveClasses('size', size).map(
87
103
  (c) => styles[c]
88
104
  );
@@ -98,19 +114,35 @@ export const Badge = forwardRef<HTMLDivElement, BadgeProps>(
98
114
  styles[`radius-${radius}`]
99
115
  );
100
116
 
117
+ const boxProps = {
118
+ className: badgeClasses,
119
+ display: 'inline-flex',
120
+ alignItems: 'center',
121
+ direction: 'row',
122
+ ...restProps,
123
+ } as BoxProps<T>;
124
+
125
+ const badgeChildren = renderBadgeChildren(children);
126
+
127
+ if (boxProps.as) {
128
+ const boxPropsWithAs = boxProps as BoxProps<T> & { as: T };
129
+
130
+ return (
131
+ <Box<T> ref={ref} {...boxPropsWithAs}>
132
+ {badgeChildren}
133
+ </Box>
134
+ );
135
+ }
136
+
101
137
  return (
102
- <Box
103
- ref={ref}
104
- className={badgeClasses}
105
- display="inline-flex"
106
- alignItems="center"
107
- direction="row"
108
- {...restProps}
109
- >
110
- {renderBadgeChildren(children)}
138
+ <Box ref={ref as BoxRef<'div'>} {...(boxProps as BoxProps<'div'>)}>
139
+ {badgeChildren}
111
140
  </Box>
112
141
  );
113
- }
114
- );
142
+ };
143
+
144
+ // React.forwardRef cannot preserve a generic `as` parameter, so restore the
145
+ // polymorphic call signature after wrapping the implementation.
146
+ export const Badge = forwardRef(BadgeBaseComponent as never) as BadgeComponent;
115
147
 
116
148
  Badge.displayName = 'Badge';
@@ -9,7 +9,7 @@ import * as Stories from './Box.stories';
9
9
  A `<Box>` is a layout component to build UIs with consistent padding and spacing between elements.
10
10
 
11
11
  - Its content is arranged in a specified `direction`, with 'column' being the default
12
- - Renders a `<div />` element by default, but can be changed using `as`
12
+ - Renders a `<div />` element by default, but can be changed using `as`. The element is never inferred from other props, so links must set `as="a"` alongside `href`
13
13
  - Allows you to specify the spacing between child elements using `gap`
14
14
  - Many props accept `ResponsiveProp` that take an object with keys that correspond to [breakpoint tokens](?path=/docs/foundation-design-tokens--docs#breakpoints).
15
15
 
@@ -1726,7 +1726,7 @@ export const Focus = () => (
1726
1726
  focus={{
1727
1727
  shadow: 'lg',
1728
1728
  }}
1729
- tabIndex="0"
1729
+ tabIndex={0}
1730
1730
  style={{ outline: 'none' }}
1731
1731
  >
1732
1732
  Card focus effect
@@ -4,7 +4,7 @@ import {
4
4
  CssOverflowValue,
5
5
  ResponsiveProp,
6
6
  } from '../../types';
7
- import { Box } from './Box';
7
+ import { Box, boxPropsKeys } from './Box';
8
8
  import React from 'react';
9
9
  import { fireEvent, render } from '@testing-library/react';
10
10
  import {
@@ -20,6 +20,18 @@ import {
20
20
  describe('Box', () => {
21
21
  const renderBox = (props = {}) => render(<Box {...props}>Test Box</Box>);
22
22
 
23
+ test('accepts attributes for the element selected by as', () => {
24
+ const buttonRef = React.createRef<HTMLButtonElement>();
25
+
26
+ render(
27
+ <Box as="button" type="button" disabled ref={buttonRef}>
28
+ Action
29
+ </Box>
30
+ );
31
+
32
+ expect(buttonRef.current).toBeDisabled();
33
+ });
34
+
23
35
  describe('aria attributes', () => {
24
36
  test('aria-label is applied if set', () => {
25
37
  const { getByLabelText } = renderBox({ 'aria-label': 'test label' });
@@ -54,6 +66,17 @@ describe('Box', () => {
54
66
  expect(getByText('Test Box')).toHaveClass('hover:background-color-primary');
55
67
  });
56
68
 
69
+ test('renders hover classes that are not supported by focus', () => {
70
+ const { getByText } = renderBox({ hover: { fontSize: 'lg' } });
71
+ expect(getByText('Test Box')).toHaveClass('hover:font-size-lg');
72
+ });
73
+
74
+ test('lists all text wrapping props as Box props', () => {
75
+ expect(boxPropsKeys).toEqual(
76
+ expect.arrayContaining(['whiteSpace', 'wordBreak'])
77
+ );
78
+ });
79
+
57
80
  test('renders with proper focus classes', () => {
58
81
  const { getByText } = renderBox({ focus: { borderColor: 'primary' } });
59
82
  expect(getByText('Test Box')).toHaveClass('focus:border-color-primary');
@@ -31,9 +31,11 @@ import {
31
31
  TextTransform,
32
32
  } from '../../types';
33
33
  import {
34
+ ComponentPropsWithoutRef,
35
+ ComponentPropsWithRef,
34
36
  CSSProperties,
35
37
  Children,
36
- FC,
38
+ ElementType,
37
39
  ReactElement,
38
40
  ReactNode,
39
41
  cloneElement,
@@ -41,12 +43,10 @@ import {
41
43
  forwardRef,
42
44
  } from 'react';
43
45
 
44
- import { KnownKeys } from '../../types/lib.types';
45
46
  import classNames from 'classnames';
46
47
  import { cssShorthandToClasses } from '../../lib/cssShorthandToClasses';
47
48
  import { generateResponsiveClasses } from '../../lib/generateResponsiveClasses';
48
49
  import { getDimensionCss } from '../../lib/getDimensionCss';
49
- import { getElementType } from '../../lib/getElementType';
50
50
  import styles from './Box.module.scss';
51
51
 
52
52
  const AUTO_FONT_COLOR_BACKGROUNDS: BackgroundColor[] = [
@@ -61,11 +61,7 @@ export type HoverableBoxProperties =
61
61
  | 'borderColor'
62
62
  | 'shadow'
63
63
  | 'background';
64
- export interface BoxProps {
65
- /**
66
- * The element type to be rendered.
67
- */
68
- as?: string;
64
+ export interface BoxOwnProps {
69
65
  /**
70
66
  * How to align the contents along the cross axis.
71
67
  */
@@ -146,11 +142,11 @@ export interface BoxProps {
146
142
  * `* shadow`
147
143
  */
148
144
  focus?: {
149
- background?: BoxProps['background'];
150
- borderColor?: BoxProps['borderColor'];
151
- borderWidth?: BoxProps['borderWidth'];
152
- color?: BoxProps['color'];
153
- shadow?: BoxProps['shadow'];
145
+ background?: BoxOwnProps['background'];
146
+ borderColor?: BoxOwnProps['borderColor'];
147
+ borderWidth?: BoxOwnProps['borderWidth'];
148
+ color?: BoxOwnProps['color'];
149
+ shadow?: BoxOwnProps['shadow'];
154
150
  };
155
151
  /**
156
152
  * The [font family token](/?path=/docs/foundation-design-tokens--docs#font-family) identifier for the Box's text
@@ -179,12 +175,12 @@ export interface BoxProps {
179
175
  * `* shadow`
180
176
  */
181
177
  hover?: {
182
- background?: BoxProps['background'];
183
- borderColor?: BoxProps['borderColor'];
184
- borderWidth?: BoxProps['borderWidth'];
185
- color?: BoxProps['color'];
186
- fontSize?: BoxProps['fontSize'];
187
- shadow?: BoxProps['shadow'];
178
+ background?: BoxOwnProps['background'];
179
+ borderColor?: BoxOwnProps['borderColor'];
180
+ borderWidth?: BoxOwnProps['borderWidth'];
181
+ color?: BoxOwnProps['color'];
182
+ fontSize?: BoxOwnProps['fontSize'];
183
+ shadow?: BoxOwnProps['shadow'];
188
184
  };
189
185
  /**
190
186
  * Sets the gaps (gutters) between rows and columns.
@@ -294,20 +290,41 @@ export interface BoxProps {
294
290
  * Can be responsive.
295
291
  */
296
292
  zIndex?: ZIndexSize | ResponsiveProp<ZIndexSize>;
293
+ }
294
+
295
+ export type BoxElement = ElementType;
296
+
297
+ export type BoxProps<T extends BoxElement = 'div'> = BoxOwnProps & {
297
298
  /**
298
- * Additional props to be spread to rendered element
299
+ * The element type to be rendered. Defaults to `div`.
300
+ *
301
+ * The element is no longer inferred from other props: passing `href` alone
302
+ * renders a `div` with an inert `href` attribute rather than an anchor, so
303
+ * links must set `as="a"` explicitly.
299
304
  */
300
- [x: string]: any; // eslint-disable-line
301
- }
305
+ as?: T;
306
+ } & Omit<ComponentPropsWithoutRef<T>, keyof BoxOwnProps | 'as'>;
307
+
308
+ export type BoxRef<T extends BoxElement> = ComponentPropsWithRef<T>['ref'];
309
+
310
+ type BoxComponent = {
311
+ <T extends BoxElement>(
312
+ props: BoxProps<T> & { as: T; ref?: BoxRef<T> }
313
+ ): ReactElement | null;
314
+ (props: BoxProps<'div'> & { ref?: BoxRef<'div'> }): ReactElement | null;
315
+ displayName?: string;
316
+ };
302
317
 
303
318
  /**
304
319
  * A `<Box>` is a layout component to build UIs with consistent padding and spacing between
305
320
  * elements.
306
321
  */
307
- export const Box: FC<BoxProps> = forwardRef(
308
- (
309
- {
310
- as = 'div',
322
+ const BoxBaseComponent = <T extends BoxElement = 'div'>(
323
+ props: BoxProps<T>,
324
+ ref: BoxRef<T>
325
+ ) => {
326
+ const {
327
+ as,
311
328
  alignItems = undefined,
312
329
  alignContent = undefined,
313
330
  alignSelf = undefined,
@@ -352,9 +369,7 @@ export const Box: FC<BoxProps> = forwardRef(
352
369
  wordBreak = undefined,
353
370
  zIndex = undefined,
354
371
  ...restProps
355
- },
356
- ref
357
- ) => {
372
+ } = props as BoxOwnProps & { as?: T };
358
373
  const heightCss = getDimensionCss('h', height);
359
374
  const widthCss = getDimensionCss('w', width);
360
375
  const maxHeightCss = getDimensionCss('mh', maxHeight);
@@ -369,7 +384,7 @@ export const Box: FC<BoxProps> = forwardRef(
369
384
  color ??
370
385
  (background === 'inverse'
371
386
  ? 'inverse'
372
- : AUTO_FONT_COLOR_BACKGROUNDS.includes(background)
387
+ : background && AUTO_FONT_COLOR_BACKGROUNDS.includes(background)
373
388
  ? 'grey'
374
389
  : undefined);
375
390
 
@@ -411,16 +426,13 @@ export const Box: FC<BoxProps> = forwardRef(
411
426
 
412
427
  const getStatefulClasses = (
413
428
  stateKey: 'hover' | 'focus',
414
- values: BoxProps['hover' | 'hover']
429
+ values: BoxOwnProps['hover' | 'focus']
415
430
  ) =>
416
431
  values // eslint-disable-line
417
432
  ? Object.entries(values).map(
418
433
  ([key, value]) =>
419
434
  cssPropertyMap[key].transformer(
420
- `${stateKey}:${
421
- cssPropertyMap[key as keyof BoxProps['focus' | 'hover']]
422
- .classPrefix
423
- }`,
435
+ `${stateKey}:${cssPropertyMap[key].classPrefix}`,
424
436
  value
425
437
  ) // eslint-disable-line max-len
426
438
  )
@@ -629,7 +641,7 @@ export const Box: FC<BoxProps> = forwardRef(
629
641
  );
630
642
  }
631
643
 
632
- const element = getElementType(Box, { as });
644
+ const element = as ?? 'div';
633
645
 
634
646
  return createElement(
635
647
  element,
@@ -641,52 +653,66 @@ export const Box: FC<BoxProps> = forwardRef(
641
653
  },
642
654
  children !== null && children !== undefined ? decoratedChildren : null
643
655
  );
644
- }
645
- );
656
+ };
657
+
658
+ // React.forwardRef cannot preserve a generic `as` parameter, so restore the
659
+ // polymorphic call signature after wrapping the implementation.
660
+ export const Box = forwardRef(BoxBaseComponent as never) as BoxComponent;
646
661
 
647
662
  Box.displayName = 'Box';
648
663
 
649
- export const boxPropsKeys: (keyof Pick<BoxProps, KnownKeys<BoxProps>>)[] = [
650
- 'as',
651
- 'alignItems',
652
- 'alignContent',
653
- 'alignSelf',
654
- 'background',
655
- 'borderColor',
656
- 'borderWidth',
657
- 'className',
658
- 'childGap',
659
- 'children',
660
- 'color',
661
- 'columnGap',
662
- 'cursor',
663
- 'direction',
664
- 'display',
665
- 'flex',
666
- 'focus',
667
- 'fontFamily',
668
- 'fontSize',
669
- 'fontWeight',
670
- 'gap',
671
- 'height',
672
- 'hover',
673
- 'justifyContent',
674
- 'margin',
675
- 'maxHeight',
676
- 'minHeight',
677
- 'maxWidth',
678
- 'minWidth',
679
- 'overflow',
680
- 'padding',
681
- 'position',
682
- 'radius',
683
- 'rowGap',
684
- 'shadow',
685
- 'style',
686
- 'textAlign',
687
- 'textTransform',
688
- 'textWrap',
689
- 'wrap',
690
- 'width',
691
- 'zIndex',
692
- ];
664
+ // `boxPropsKeys` drives runtime prop routing (see SelectInputNative), so a key
665
+ // missing from the list silently forwards a Box prop to the rendered element.
666
+ // `Record` requires an entry for every `BoxOwnProps` key, so omitting one here
667
+ // fails to compile.
668
+ const boxPropsKeyMap: Record<keyof BoxOwnProps | 'as', true> = {
669
+ as: true,
670
+ alignItems: true,
671
+ alignContent: true,
672
+ alignSelf: true,
673
+ background: true,
674
+ borderColor: true,
675
+ borderWidth: true,
676
+ className: true,
677
+ childGap: true,
678
+ children: true,
679
+ color: true,
680
+ columnGap: true,
681
+ cursor: true,
682
+ direction: true,
683
+ display: true,
684
+ flex: true,
685
+ focus: true,
686
+ fontFamily: true,
687
+ fontSize: true,
688
+ fontWeight: true,
689
+ gap: true,
690
+ height: true,
691
+ hover: true,
692
+ justifyContent: true,
693
+ margin: true,
694
+ maxHeight: true,
695
+ minHeight: true,
696
+ maxWidth: true,
697
+ minWidth: true,
698
+ overflow: true,
699
+ padding: true,
700
+ position: true,
701
+ radius: true,
702
+ rowGap: true,
703
+ shadow: true,
704
+ style: true,
705
+ textAlign: true,
706
+ textTransform: true,
707
+ textWrap: true,
708
+ whiteSpace: true,
709
+ wrap: true,
710
+ width: true,
711
+ wordBreak: true,
712
+ zIndex: true,
713
+ };
714
+
715
+ export const boxPropsKeys = Object.keys(boxPropsKeyMap) as (
716
+ | keyof BoxOwnProps
717
+ | 'as'
718
+ )[];
@@ -7,9 +7,13 @@ export interface CardProps extends BoxProps {
7
7
  * The Card's contents.
8
8
  */
9
9
  children?: ReactNode;
10
+ /**
11
+ * Whether the card uses its subdued background.
12
+ */
13
+ subdued?: boolean;
10
14
  }
11
15
 
12
- export const CardBaseComponent: React.FC<CardProps> = React.forwardRef(
16
+ export const CardBaseComponent = React.forwardRef<HTMLDivElement, CardProps>(
13
17
  (
14
18
  {
15
19
  children,
@@ -5,10 +5,6 @@ export interface CardFooterProps extends BoxProps {
5
5
  * Contents of the Footer.
6
6
  */
7
7
  children?: ReactNode;
8
- /**
9
- * Additional props to be spread to rendered element
10
- */
11
- [x: string]: any; // eslint-disable-line
12
8
  }
13
9
 
14
10
  export const CardFooter: FC<CardFooterProps> = ({
@@ -2,7 +2,7 @@ import React, { FC, ReactNode } from 'react';
2
2
  import { Box, BoxProps } from '../../../Box/Box';
3
3
  import { Heading } from '../../../Heading/Heading';
4
4
 
5
- export interface CardHeaderProps extends BoxProps {
5
+ interface CardHeaderOwnProps {
6
6
  /**
7
7
  * The title of the card
8
8
  */
@@ -13,6 +13,9 @@ export interface CardHeaderProps extends BoxProps {
13
13
  description?: ReactNode;
14
14
  }
15
15
 
16
+ export type CardHeaderProps = CardHeaderOwnProps &
17
+ Omit<BoxProps<'div'>, keyof CardHeaderOwnProps | 'as'>;
18
+
16
19
  export const CardHeader: FC<CardHeaderProps> = ({
17
20
  children,
18
21
  title,
@@ -4,7 +4,7 @@ import React, { FC, ReactNode } from 'react';
4
4
  import classNames from 'classnames';
5
5
  import styles from '../../Card.module.scss';
6
6
 
7
- export interface CardSectionProps extends BoxProps {
7
+ interface CardSectionOwnProps {
8
8
  /**
9
9
  * Any valid background color token, or a `url()` for an image
10
10
  */
@@ -41,12 +41,11 @@ export interface CardSectionProps extends BoxProps {
41
41
  * Title for the section.
42
42
  */
43
43
  title?: ReactNode;
44
- /**
45
- * Additional props to be spread to rendered element
46
- */
47
- [x: string]: any; // eslint-disable-line
48
44
  }
49
45
 
46
+ export type CardSectionProps = CardSectionOwnProps &
47
+ Omit<BoxProps<'div'>, keyof CardSectionOwnProps | 'as'>;
48
+
50
49
  export const CardSection: FC<CardSectionProps> = ({
51
50
  background = undefined,
52
51
  borderColor = undefined,
@@ -108,6 +108,23 @@ describe('CheckboxInput', () => {
108
108
  expect(checkbox.checked).toEqual(false);
109
109
  });
110
110
 
111
+ test.each([0, ''])('forwards the falsy value %p to the input', (value) => {
112
+ render(
113
+ <CheckboxInput
114
+ id="testCheckbox"
115
+ label="test checkbox"
116
+ isChecked={false}
117
+ onChange={() => null}
118
+ value={value}
119
+ />
120
+ );
121
+
122
+ expect(screen.getByRole('checkbox')).toHaveAttribute(
123
+ 'value',
124
+ String(value)
125
+ );
126
+ });
127
+
111
128
  test('assigns the "aria-labelledby" attribute and calls FormLabel with expected props', () => {
112
129
  const { getByLabelText } = render(
113
130
  <CheckboxInput