@openedx/paragon 23.14.9 → 23.15.0

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 (72) hide show
  1. package/dist/Card/CardBody.d.ts +9 -0
  2. package/dist/Card/CardBody.js +0 -11
  3. package/dist/Card/CardBody.js.map +1 -1
  4. package/dist/Card/CardContext.d.ts +17 -0
  5. package/dist/Card/CardContext.js +8 -21
  6. package/dist/Card/CardContext.js.map +1 -1
  7. package/dist/Card/CardDivider.d.ts +7 -0
  8. package/dist/Card/CardDivider.js +2 -10
  9. package/dist/Card/CardDivider.js.map +1 -1
  10. package/dist/Card/CardFallbackDefaultImage.d.ts +1 -0
  11. package/dist/Card/CardFallbackDefaultImage.js +1 -0
  12. package/dist/Card/CardFallbackDefaultImage.js.map +1 -0
  13. package/dist/Card/CardGrid.d.ts +22 -0
  14. package/dist/Card/CardGrid.js +6 -31
  15. package/dist/Card/CardGrid.js.map +1 -1
  16. package/dist/DataTable/DataTableContext.d.ts +3 -0
  17. package/dist/DataTable/DataTableContext.js.map +1 -1
  18. package/dist/DataTable/TableCell.d.ts +14 -0
  19. package/dist/DataTable/TableCell.js +0 -12
  20. package/dist/DataTable/TableCell.js.map +1 -1
  21. package/dist/DataTable/TableHeaderCell.d.ts +26 -0
  22. package/dist/DataTable/TableHeaderCell.js +4 -32
  23. package/dist/DataTable/TableHeaderCell.js.map +1 -1
  24. package/dist/Menu/MenuItem.d.ts +17 -0
  25. package/dist/Menu/MenuItem.js +5 -27
  26. package/dist/Menu/MenuItem.js.map +1 -1
  27. package/dist/Menu/index.d.ts +16 -0
  28. package/dist/Menu/index.js +4 -24
  29. package/dist/Menu/index.js.map +1 -1
  30. package/dist/OverflowScroll/data/constants.d.ts +1 -0
  31. package/dist/OverflowScroll/data/constants.js +1 -0
  32. package/dist/OverflowScroll/data/constants.js.map +1 -0
  33. package/dist/PageBanner/index.d.ts +27 -0
  34. package/dist/PageBanner/index.js +5 -28
  35. package/dist/PageBanner/index.js.map +1 -1
  36. package/dist/Sheet/SheetContainer.js +30 -8
  37. package/dist/Sheet/SheetContainer.js.map +1 -1
  38. package/dist/Sheet/index.js +15 -5
  39. package/dist/Sheet/index.js.map +1 -1
  40. package/dist/Stack/index.d.ts +20 -0
  41. package/dist/Stack/index.js +3 -28
  42. package/dist/Stack/index.js.map +1 -1
  43. package/dist/Tabs/Tab.d.ts +19 -0
  44. package/dist/Tabs/Tab.js +0 -23
  45. package/dist/Tabs/Tab.js.map +1 -1
  46. package/dist/index.d.ts +1 -1
  47. package/dist/index.js +1 -2
  48. package/dist/index.js.map +1 -1
  49. package/package.json +1 -1
  50. package/src/Card/CardBody.tsx +19 -0
  51. package/src/Card/{CardContext.jsx → CardContext.tsx} +24 -25
  52. package/src/Card/CardDivider.tsx +13 -0
  53. package/src/Card/{CardGrid.jsx → CardGrid.tsx} +28 -35
  54. package/src/DataTable/{TableCell.jsx → TableCell.tsx} +13 -15
  55. package/src/DataTable/{TableHeaderCell.jsx → TableHeaderCell.tsx} +32 -33
  56. package/src/Menu/MenuItem.tsx +49 -0
  57. package/src/Menu/{index.jsx → index.tsx} +18 -27
  58. package/src/PageBanner/{index.jsx → index.tsx} +27 -29
  59. package/src/Sheet/Sheet.test.jsx +63 -3
  60. package/src/Sheet/SheetContainer.jsx +34 -7
  61. package/src/Sheet/SheetContainer.test.jsx +34 -1
  62. package/src/Sheet/__snapshots__/Sheet.test.jsx.snap +15 -6
  63. package/src/Sheet/index.jsx +12 -2
  64. package/src/Stack/{index.jsx → index.tsx} +22 -35
  65. package/src/Tabs/{Tab.jsx → Tab.tsx} +10 -18
  66. package/src/index.ts +1 -2
  67. package/src/Card/CardBody.jsx +0 -23
  68. package/src/Card/CardDivider.jsx +0 -18
  69. package/src/Menu/MenuItem.jsx +0 -57
  70. /package/src/Card/{CardFallbackDefaultImage.js → CardFallbackDefaultImage.ts} +0 -0
  71. /package/src/DataTable/{DataTableContext.jsx → DataTableContext.tsx} +0 -0
  72. /package/src/OverflowScroll/data/{constants.js → constants.ts} +0 -0
@@ -0,0 +1,19 @@
1
+ import React, { ForwardedRef } from 'react';
2
+ import classNames from 'classnames';
3
+
4
+ interface CardBodyProps extends React.HTMLAttributes<HTMLDivElement> {
5
+ /** Specifies the content of the component. */
6
+ children?: React.ReactNode;
7
+ /** The class to append to the base element. */
8
+ className?: string;
9
+ }
10
+
11
+ const CardBody = React.forwardRef(({
12
+ className, children, ...rest
13
+ }: CardBodyProps, ref: ForwardedRef<HTMLDivElement>) => (
14
+ <div className={classNames('pgn__card-body', className)} ref={ref} {...rest}>
15
+ {children}
16
+ </div>
17
+ ));
18
+
19
+ export default CardBody;
@@ -1,14 +1,31 @@
1
- import React, { createContext } from 'react';
2
- import PropTypes from 'prop-types';
1
+ import React, { createContext, ReactNode } from 'react';
3
2
 
4
- const CardContext = createContext({});
3
+ interface CardContextData {
4
+ /** Specifies which orientation to use. */
5
+ orientation: 'horizontal' | 'vertical';
6
+ /** Specifies loading state. */
7
+ isLoading: boolean;
8
+ /** Specifies `Card` style variant */
9
+ variant: 'light' | 'dark' | 'muted';
10
+ }
11
+
12
+ const CardContext = createContext<CardContextData>({
13
+ orientation: 'vertical',
14
+ isLoading: false,
15
+ variant: 'light',
16
+ });
17
+
18
+ interface CardContextProviderProps extends Partial<CardContextData> {
19
+ /** Specifies content of the component. */
20
+ children?: ReactNode;
21
+ }
5
22
 
6
23
  function CardContextProvider({
7
- orientation,
24
+ orientation = 'vertical',
8
25
  children,
9
- isLoading,
10
- variant,
11
- }) {
26
+ isLoading = false,
27
+ variant = 'light',
28
+ }: CardContextProviderProps) {
12
29
  return (
13
30
  <CardContext.Provider value={{ orientation, isLoading, variant }}>
14
31
  {children}
@@ -16,23 +33,5 @@ function CardContextProvider({
16
33
  );
17
34
  }
18
35
 
19
- CardContextProvider.propTypes = {
20
- /** Specifies which orientation to use. */
21
- orientation: PropTypes.oneOf(['horizontal', 'vertical']),
22
- /** Specifies loading state. */
23
- isLoading: PropTypes.bool,
24
- /** Specifies content of the component. */
25
- children: PropTypes.node,
26
- /** Specifies `Card` style variant */
27
- variant: PropTypes.oneOf(['light', 'dark', 'muted']),
28
- };
29
-
30
- CardContextProvider.defaultProps = {
31
- orientation: 'vertical',
32
- isLoading: false,
33
- children: null,
34
- variant: 'light',
35
- };
36
-
37
36
  export { CardContextProvider };
38
37
  export default CardContext;
@@ -0,0 +1,13 @@
1
+ import React, { forwardRef, ForwardedRef } from 'react';
2
+ import classNames from 'classnames';
3
+
4
+ interface CardDividerProps extends React.HTMLAttributes<HTMLDivElement> {
5
+ /** Specifies class name to append to the base element. */
6
+ className?: string;
7
+ }
8
+
9
+ const CardDivider = forwardRef(({ className, ...props }: CardDividerProps, ref: ForwardedRef<HTMLDivElement>) => (
10
+ <div className={classNames('pgn__card-divider', className)} ref={ref} {...props} />
11
+ ));
12
+
13
+ export default CardDivider;
@@ -1,15 +1,38 @@
1
- import React, { useMemo } from 'react';
1
+ import React, { useMemo, ReactNode } from 'react';
2
2
  import classNames from 'classnames';
3
- import PropTypes from 'prop-types';
4
3
  import Row from 'react-bootstrap/Row';
5
4
  import Col from 'react-bootstrap/Col';
6
5
 
6
+ interface CardGridProps {
7
+ /** The class name for the CardGrid component */
8
+ className?: string;
9
+ /** The Card components to organize into a responsive grid */
10
+ children: ReactNode;
11
+ /**
12
+ * An object containing the desired column size at each breakpoint, following a similar
13
+ * props API as ``react-bootstrap/Col``
14
+ */
15
+ columnSizes?: {
16
+ xs?: number;
17
+ sm?: number;
18
+ md?: number;
19
+ lg?: number;
20
+ xl?: number;
21
+ };
22
+ /** Whether to disable the default equal height cards across rows in the card grid */
23
+ hasEqualColumnHeights?: boolean;
24
+ }
25
+
7
26
  function CardGrid({
8
27
  className,
9
28
  children,
10
- columnSizes,
11
- hasEqualColumnHeights,
12
- }) {
29
+ columnSizes = {
30
+ sm: 12,
31
+ lg: 6,
32
+ xl: 4,
33
+ },
34
+ hasEqualColumnHeights = true,
35
+ }: CardGridProps) {
13
36
  const cards = useMemo(
14
37
  () => React.Children.map(children, (card) => (
15
38
  <Col
@@ -36,34 +59,4 @@ function CardGrid({
36
59
  );
37
60
  }
38
61
 
39
- CardGrid.propTypes = {
40
- /** The class name for the CardGrid component */
41
- className: PropTypes.string,
42
- /** The Card components to organize into a responsive grid */
43
- children: PropTypes.node.isRequired,
44
- /**
45
- * An object containing the desired column size at each breakpoint, following a similar
46
- * props API as ``react-bootstrap/Col``
47
- */
48
- columnSizes: PropTypes.shape({
49
- xs: PropTypes.number,
50
- sm: PropTypes.number,
51
- md: PropTypes.number,
52
- lg: PropTypes.number,
53
- xl: PropTypes.number,
54
- }),
55
- /** Whether to disable the default equal height cards across rows in the card grid */
56
- hasEqualColumnHeights: PropTypes.bool,
57
- };
58
-
59
- CardGrid.defaultProps = {
60
- className: undefined,
61
- columnSizes: {
62
- sm: 12,
63
- lg: 6,
64
- xl: 4,
65
- },
66
- hasEqualColumnHeights: true,
67
- };
68
-
69
62
  export default CardGrid;
@@ -1,8 +1,18 @@
1
- import React from 'react';
2
- import PropTypes from 'prop-types';
1
+ import React, { ReactNode, TdHTMLAttributes } from 'react';
3
2
  import classNames from 'classnames';
4
3
 
5
- function TableCell({ getCellProps, render, column }) {
4
+ interface TableCellProps {
5
+ /** Props for the td element */
6
+ getCellProps: () => TdHTMLAttributes<HTMLTableCellElement>;
7
+ /** Function that renders the cell contents. Will be called with the string 'Cell' */
8
+ render: (type: 'Cell') => ReactNode;
9
+ /** Table column */
10
+ column: {
11
+ /** Class(es) to be applied to the cells in the given column */
12
+ cellClassName?: string;
13
+ };
14
+ }
15
+ function TableCell({ getCellProps, render, column }: TableCellProps) {
6
16
  const { className, ...rest } = getCellProps();
7
17
  return (
8
18
  <td {...rest} className={classNames('pgn__data-table-cell-wrap', className, column.cellClassName)}>
@@ -11,16 +21,4 @@ function TableCell({ getCellProps, render, column }) {
11
21
  );
12
22
  }
13
23
 
14
- TableCell.propTypes = {
15
- /** Props for the td element */
16
- getCellProps: PropTypes.func.isRequired,
17
- /** Function that renders the cell contents. Will be called with the string 'Cell' */
18
- render: PropTypes.func.isRequired,
19
- /** Table column */
20
- column: PropTypes.shape({
21
- /** Class(es) to be applied to the cells in the given column */
22
- cellClassName: PropTypes.string,
23
- }).isRequired,
24
- };
25
-
26
24
  export default TableCell;
@@ -1,10 +1,16 @@
1
1
  import React from 'react';
2
- import PropTypes from 'prop-types';
3
2
  import classNames from 'classnames';
4
3
  import Icon from '../Icon';
5
4
  import { ArrowDropDown, ArrowDropUp, ArrowDropUpDown } from '../../icons';
6
5
 
7
- export function SortIndicator({ isSorted, isSortedDesc }) {
6
+ interface SortIndicatorProps {
7
+ /** Indicates whether or not a column is sorted */
8
+ isSorted: boolean;
9
+ /** Indicates whether the column is sorted in descending order */
10
+ isSortedDesc: boolean;
11
+ }
12
+
13
+ export function SortIndicator({ isSorted, isSortedDesc }: SortIndicatorProps) {
8
14
  if (!isSorted) {
9
15
  return <Icon style={{ opacity: 0.5 }} src={ArrowDropUpDown} data-testid="arrow-drop-up-down" />;
10
16
  }
@@ -16,14 +22,32 @@ export function SortIndicator({ isSorted, isSortedDesc }) {
16
22
  return <Icon src={ArrowDropUp} data-testid="arrow-drop-up" />;
17
23
  }
18
24
 
19
- SortIndicator.propTypes = {
20
- isSorted: PropTypes.bool.isRequired,
21
- isSortedDesc: PropTypes.bool.isRequired,
22
- };
25
+ interface TableHeaderCellProps {
26
+ /** Returns props for the th element */
27
+ getHeaderProps: (...args: any[]) => Record<string, any>;
28
+ /** Indicates whether or not a column is sorted */
29
+ isSorted?: boolean;
30
+ /** Renders the header content. Passed the string 'Header' */
31
+ render: (type: 'Header') => React.ReactNode;
32
+ /** Indicates whether the column is sorted in descending order */
33
+ isSortedDesc?: boolean;
34
+ /** Gets props related to sorting that will be passed to th */
35
+ getSortByToggleProps?: (...args: any[]) => Record<string, any>;
36
+ /** Indicates whether a column is sortable */
37
+ canSort?: boolean;
38
+ /** Class(es) to be applied to header cells */
39
+ headerClassName?: string;
40
+ }
23
41
 
24
42
  function TableHeaderCell({
25
- getHeaderProps, render, canSort, getSortByToggleProps, isSorted, isSortedDesc, headerClassName,
26
- }) {
43
+ getHeaderProps,
44
+ render,
45
+ canSort = false,
46
+ getSortByToggleProps = () => ({}),
47
+ isSorted = false,
48
+ isSortedDesc = false,
49
+ headerClassName,
50
+ }: TableHeaderCellProps) {
27
51
  const toggleProps = canSort && getSortByToggleProps ? getSortByToggleProps() : {};
28
52
 
29
53
  return (
@@ -36,29 +60,4 @@ function TableHeaderCell({
36
60
  );
37
61
  }
38
62
 
39
- TableHeaderCell.defaultProps = {
40
- headerClassName: null,
41
- isSorted: false,
42
- isSortedDesc: false,
43
- canSort: false,
44
- getSortByToggleProps: () => {},
45
- };
46
-
47
- TableHeaderCell.propTypes = {
48
- /** Returns props for the th element */
49
- getHeaderProps: PropTypes.func.isRequired,
50
- /** Indicates whether or not a column is sorted */
51
- isSorted: PropTypes.bool,
52
- /** Renders the header content. Passed the string 'Header' */
53
- render: PropTypes.func.isRequired,
54
- /** Indicates whether the column is sorted in descending order */
55
- isSortedDesc: PropTypes.bool,
56
- /** Gets props related to sorting that will be passed to th */
57
- getSortByToggleProps: PropTypes.func,
58
- /** Indicates whether a column is sortable */
59
- canSort: PropTypes.bool,
60
- /** Class(es) to be applied to header cells */
61
- headerClassName: PropTypes.string,
62
- };
63
-
64
63
  export default TableHeaderCell;
@@ -0,0 +1,49 @@
1
+ import React, {
2
+ ReactElement, ReactNode, ElementType, createElement, ComponentType,
3
+ } from 'react';
4
+ import classNames from 'classnames';
5
+ import Icon from '../Icon';
6
+
7
+ interface MenuItemProps {
8
+ /** Specifies that this `MenuItem` is selected inside the `SelectMenu` */
9
+ defaultSelected?: boolean;
10
+ /** Specifies class name to append to the base element */
11
+ className?: string;
12
+ /** Specifies the content of the `MenuItem` */
13
+ children: ReactNode;
14
+ /** Specifies the base element */
15
+ as?: ElementType;
16
+ /** Specifies the jsx before the content of the `MenuItem` */
17
+ iconBefore?: ReactElement | ElementType;
18
+ /** Specifies the jsx after the content of the `MenuItem` */
19
+ iconAfter?: ReactElement | ElementType;
20
+ }
21
+ function MenuItem({
22
+ as = 'button',
23
+ children,
24
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
25
+ defaultSelected = false,
26
+ iconAfter,
27
+ iconBefore,
28
+ ...props
29
+ }: MenuItemProps) {
30
+ const className = classNames(props.className, 'pgn__menu-item');
31
+
32
+ return createElement(
33
+ as,
34
+ {
35
+ ...props,
36
+ className,
37
+ },
38
+ (
39
+ <>
40
+ {iconBefore && <Icon className="btn-icon-before" src={iconBefore as ComponentType} />}
41
+ <span className="pgn__menu-item-text">{children}</span>
42
+ <span className="pgn__menu-item-content-spacer" />
43
+ {iconAfter && <Icon className="btn-icon-after" src={iconAfter as ComponentType} />}
44
+ </>
45
+ ),
46
+ );
47
+ }
48
+
49
+ export default MenuItem;
@@ -1,18 +1,30 @@
1
- import React from 'react';
2
- import PropTypes from 'prop-types';
1
+ import React, { ElementType, ReactNode, createElement } from 'react';
3
2
  import classNames from 'classnames';
4
3
  import useArrowKeyNavigation from '../hooks/useArrowKeyNavigationHook';
5
4
 
5
+ interface MenuProps {
6
+ /** Specifies class name to append to the base element */
7
+ className?: string;
8
+ /**
9
+ * Specifies the CSS selector string that indicates to which elements
10
+ * the user can navigate using the arrow keys
11
+ */
12
+ arrowKeyNavigationSelector?: string;
13
+ /** Specifies the base element */
14
+ as?: ElementType;
15
+ /** Specifies the content of the menu */
16
+ children?: ReactNode;
17
+ }
6
18
  function Menu({
7
- as,
8
- arrowKeyNavigationSelector,
19
+ as = 'div',
20
+ arrowKeyNavigationSelector = 'a:not(:disabled),button:not(:disabled),input:not(:disabled)',
9
21
  children,
10
22
  ...props
11
- }) {
23
+ }: MenuProps) {
12
24
  const parentRef = useArrowKeyNavigation({ selectors: arrowKeyNavigationSelector });
13
25
  const className = classNames(props.className, 'pgn__menu');
14
26
 
15
- return React.createElement(
27
+ return createElement(
16
28
  as,
17
29
  {
18
30
  ...props,
@@ -28,25 +40,4 @@ function Menu({
28
40
  );
29
41
  }
30
42
 
31
- Menu.propTypes = {
32
- /** Specifies class name to append to the base element */
33
- className: PropTypes.string,
34
- /**
35
- * Specifies the CSS selector string that indicates to which elements
36
- * the user can navigate using the arrow keys
37
- */
38
- arrowKeyNavigationSelector: PropTypes.string,
39
- /** Specifies the base element */
40
- as: PropTypes.elementType,
41
- /** Specifies the content of the menu */
42
- children: PropTypes.node,
43
- };
44
-
45
- Menu.defaultProps = {
46
- className: undefined,
47
- arrowKeyNavigationSelector: 'a:not(:disabled),button:not(:disabled),input:not(:disabled)',
48
- as: 'div',
49
- children: null,
50
- };
51
-
52
43
  export default Menu;
@@ -1,5 +1,4 @@
1
- import React from 'react';
2
- import PropTypes from 'prop-types';
1
+ import React, { ReactNode } from 'react';
3
2
  import classNames from 'classnames';
4
3
  import { Close } from '../../icons';
5
4
  import Icon from '../Icon';
@@ -13,11 +12,34 @@ export const VARIANTS = {
13
12
  warning: 'warning',
14
13
  accentA: 'accentA',
15
14
  accentB: 'accentB',
16
- };
15
+ } as const;
16
+
17
+ interface PageBannerProps {
18
+ /** An element rendered inside the `Page Banner`. */
19
+ children: ReactNode;
20
+ /** Boolean used to control whether `Page Banner` is dismissible. */
21
+ dismissible: boolean;
22
+ /** An element to be set as the dismiss button's alt text (preferably a translated string). */
23
+ dismissAltText: string;
24
+ /** A function to be called on dismiss of the `Page Banner`. */
25
+ onDismiss: () => void;
26
+ /** Boolean used to control whether the Page Banner shows. */
27
+ show: boolean;
28
+ /** A string designating which color variant of the `Page Banner` to display.
29
+ * The full list of variants can be seen [here.](https://github.com/openedx/paragon/blob/release-23.x/src/PageBanner/index.tsx)
30
+ */
31
+ variant: keyof typeof VARIANTS;
32
+ }
17
33
 
18
34
  function PageBanner({
19
- children, dismissible, dismissAltText, onDismiss, show, variant, ...rest
20
- }) {
35
+ children,
36
+ dismissible = false,
37
+ dismissAltText = PAGE_BANNER_DISMISS_ALT_TEXT,
38
+ onDismiss = () => {},
39
+ show = true,
40
+ variant = VARIANTS.accentA,
41
+ ...rest
42
+ }: PageBannerProps) {
21
43
  if (!show) {
22
44
  return null;
23
45
  }
@@ -52,28 +74,4 @@ function PageBanner({
52
74
  );
53
75
  }
54
76
 
55
- PageBanner.propTypes = {
56
- /** An element rendered inside the `Page Banner`. */
57
- children: PropTypes.node,
58
- /** Boolean used to control whether `Page Banner` is dismissible. */
59
- dismissible: PropTypes.bool,
60
- /** An element to be set as the dismiss button's alt text (preferably a translated string). */
61
- dismissAltText: PropTypes.node,
62
- /** A function to be called on dismiss of the `Page Banner`. */
63
- onDismiss: PropTypes.func,
64
- /** Boolean used to control whether the Page Banner shows. */
65
- show: PropTypes.bool,
66
- /** A string designating which color variant of the `Page Banner` to display */
67
- variant: PropTypes.oneOf([VARIANTS.light, VARIANTS.dark, VARIANTS.warning, VARIANTS.accentA, VARIANTS.accentB]),
68
- };
69
-
70
- PageBanner.defaultProps = {
71
- children: undefined,
72
- dismissible: false,
73
- dismissAltText: PAGE_BANNER_DISMISS_ALT_TEXT,
74
- onDismiss: () => {},
75
- show: true,
76
- variant: VARIANTS.accentA,
77
- };
78
-
79
77
  export default PageBanner;
@@ -6,11 +6,12 @@ import Sheet, { POSITIONS, VARIANTS } from '.';
6
6
 
7
7
  /* eslint-disable react/prop-types */
8
8
  jest.mock('./SheetContainer', () => function SheetContainerMock(props) {
9
- const { children, ...otherProps } = props;
9
+ const { children, className, ...otherProps } = props;
10
+ const allClasses = ['sheet-container', className].filter(Boolean).join(' ');
10
11
  return (
11
- <sheet-container {...otherProps}>
12
+ <div data-testid="sheet-container" className={allClasses} {...otherProps}>
12
13
  {children}
13
- </sheet-container>
14
+ </div>
14
15
  );
15
16
  });
16
17
 
@@ -53,5 +54,64 @@ describe('<Sheet />', () => {
53
54
  const { container: container2 } = render(<Sheet />);
54
55
  expect(container2.firstChild).not.toBeNull();
55
56
  });
57
+
58
+ it('renders with custom className', () => {
59
+ const customClassName = 'custom-class';
60
+ const { container } = render(<Sheet className={customClassName} />);
61
+ const sheetElement = container.querySelector('.pgn__sheet-component');
62
+
63
+ expect(sheetElement).toBeInTheDocument();
64
+ expect(sheetElement).toHaveClass('pgn__sheet-component');
65
+ expect(sheetElement).toHaveClass(customClassName);
66
+ });
67
+
68
+ it('handles multiple custom className', () => {
69
+ const customClasses = 'class-one class-two';
70
+ const { container } = render(<Sheet className={customClasses} />);
71
+ const sheetElement = container.querySelector('.pgn__sheet-component');
72
+
73
+ expect(sheetElement).toHaveClass('pgn__sheet-component');
74
+ expect(sheetElement).toHaveClass('class-one');
75
+ expect(sheetElement).toHaveClass('class-two');
76
+ });
77
+
78
+ it('renders with custom className on SheetContainer', () => {
79
+ const customClassName = 'custom-container-class';
80
+ const { getByTestId } = render(<Sheet containerClassName={customClassName} />);
81
+ const sheetContainer = getByTestId('sheet-container');
82
+
83
+ expect(sheetContainer).toBeInTheDocument();
84
+ expect(sheetContainer).toHaveClass('sheet-container');
85
+ expect(sheetContainer).toHaveClass(customClassName);
86
+ });
87
+
88
+ it('handles multiple custom className values on SheetContainer', () => {
89
+ const customClasses = 'container-one container-two';
90
+ const { getByTestId } = render(<Sheet containerClassName={customClasses} />);
91
+ const sheetContainer = getByTestId('sheet-container');
92
+
93
+ expect(sheetContainer).toBeInTheDocument();
94
+ expect(sheetContainer).toHaveClass('sheet-container');
95
+ expect(sheetContainer).toHaveClass('container-one');
96
+ expect(sheetContainer).toHaveClass('container-two');
97
+ });
98
+
99
+ it('handles className and containerClassName simultaneously', () => {
100
+ const containerClass = 'container-class';
101
+ const sheetClass = 'sheet-class';
102
+ const { getByTestId, container } = render(
103
+ <Sheet containerClassName={containerClass} className={sheetClass} />,
104
+ );
105
+ const sheetContainer = getByTestId('sheet-container');
106
+ const sheetElement = container.querySelector('.pgn__sheet-component');
107
+
108
+ expect(sheetContainer).toBeInTheDocument();
109
+ expect(sheetContainer).toHaveClass('sheet-container');
110
+ expect(sheetContainer).toHaveClass(containerClass);
111
+
112
+ expect(sheetElement).toBeInTheDocument();
113
+ expect(sheetElement).toHaveClass('pgn__sheet-component');
114
+ expect(sheetElement).toHaveClass(sheetClass);
115
+ });
56
116
  });
57
117
  });
@@ -1,23 +1,44 @@
1
1
  import React from 'react';
2
2
  import ReactDOM from 'react-dom';
3
3
  import PropTypes from 'prop-types';
4
+ import classNames from 'classnames';
4
5
 
5
6
  class SheetContainer extends React.Component {
6
7
  constructor(props) {
7
8
  super(props);
8
9
  this.sheetRootName = 'sheet-root';
10
+ this.updateRootElement();
11
+ }
12
+
13
+ componentDidUpdate(prevProps) {
14
+ if (prevProps.className !== this.props.className) {
15
+ this.updateRootElement();
16
+ }
17
+ }
18
+
19
+ updateRootElement = () => {
20
+ const { className } = this.props;
21
+
22
+ /* istanbul ignore next */
9
23
  if (typeof document === 'undefined') {
10
24
  this.rootElement = null;
11
- } else if (document.getElementById(this.sheetRootName)) {
12
- this.rootElement = document.getElementById(this.sheetRootName);
13
- } else {
14
- const rootElement = document.createElement('div');
25
+ return;
26
+ }
27
+
28
+ let rootElement = document.getElementById(this.sheetRootName);
29
+
30
+ if (!rootElement) {
31
+ rootElement = document.createElement('div');
15
32
  rootElement.setAttribute('id', this.sheetRootName);
16
- rootElement.setAttribute('class', 'sheet-container');
17
33
  rootElement.setAttribute('data-testid', 'sheet-container');
18
- this.rootElement = document.body.appendChild(rootElement);
34
+ document.body.appendChild(rootElement);
19
35
  }
20
- }
36
+
37
+ const classes = classNames('sheet-container', className);
38
+ rootElement.setAttribute('class', classes);
39
+
40
+ this.rootElement = rootElement;
41
+ };
21
42
 
22
43
  render() {
23
44
  if (this.rootElement) {
@@ -32,6 +53,12 @@ class SheetContainer extends React.Component {
32
53
 
33
54
  SheetContainer.propTypes = {
34
55
  children: PropTypes.node.isRequired,
56
+ /** Additional CSS classes to apply to the sheet container */
57
+ className: PropTypes.string,
58
+ };
59
+
60
+ SheetContainer.defaultProps = {
61
+ className: undefined,
35
62
  };
36
63
 
37
64
  export default SheetContainer;
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { render, screen } from '@testing-library/react';
2
+ import { render, screen, act } from '@testing-library/react';
3
3
  import SheetContainer from './SheetContainer';
4
4
 
5
5
  const childId1 = 'sheet-container-TEST-child1';
@@ -31,4 +31,37 @@ describe('<SheetContainer />', () => {
31
31
  const childEl2 = screen.getByText(childContent2);
32
32
  expect(childEl2).toBeTruthy();
33
33
  });
34
+
35
+ it('applies custom className if provided', () => {
36
+ const customClass = 'custom-class';
37
+ render(<SheetContainer className={customClass}>{child1}</SheetContainer>);
38
+ const rootEl = screen.getByTestId('sheet-container');
39
+ expect(rootEl).toBeTruthy();
40
+ expect(rootEl.className).toContain('sheet-container');
41
+ expect(rootEl.className).toContain(customClass);
42
+ });
43
+
44
+ it('calls updateRootElement when className changes (componentDidUpdate)', () => {
45
+ const { rerender } = render(
46
+ <SheetContainer className="first-class">
47
+ <div>Content</div>
48
+ </SheetContainer>,
49
+ );
50
+
51
+ const sheetRoot = document.getElementById('sheet-root');
52
+ expect(sheetRoot).toHaveClass('sheet-container');
53
+ expect(sheetRoot).toHaveClass('first-class');
54
+
55
+ act(() => {
56
+ rerender(
57
+ <SheetContainer className="second-class">
58
+ <div>Content</div>
59
+ </SheetContainer>,
60
+ );
61
+ });
62
+
63
+ expect(sheetRoot).toHaveClass('sheet-container');
64
+ expect(sheetRoot).toHaveClass('second-class');
65
+ expect(sheetRoot).not.toHaveClass('first-class');
66
+ });
34
67
  });