@hyphen/hyphen-components 2.18.0 → 2.19.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyphen/hyphen-components",
3
- "version": "2.18.0",
3
+ "version": "2.19.0",
4
4
  "license": "MIT",
5
5
  "author": {
6
6
  "name": "@hyphen"
@@ -66,11 +66,13 @@
66
66
  "@semantic-release/commit-analyzer": "^11.1.0",
67
67
  "@size-limit/preset-small-lib": "^11.1.4",
68
68
  "@storybook/addon-essentials": "^8.2.9",
69
+ "@storybook/addon-interactions": "^8.3.5",
69
70
  "@storybook/addon-themes": "^8.2.9",
70
71
  "@storybook/blocks": "^8.2.9",
71
72
  "@storybook/manager-api": "^8.2.9",
72
73
  "@storybook/react": "^8.2.9",
73
74
  "@storybook/react-vite": "^8.2.9",
75
+ "@storybook/test": "^8.3.5",
74
76
  "@storybook/theming": "^8.2.9",
75
77
  "@testing-library/jest-dom": "^6.5.0",
76
78
  "@testing-library/react": "^14.1.2",
@@ -1,8 +1,9 @@
1
1
  import React, { useRef } from 'react';
2
2
  import { Modal } from './Modal';
3
- import type { Meta } from '@storybook/react';
3
+ import type { Meta, StoryObj } from '@storybook/react';
4
4
  import { Button } from '../Button/Button';
5
5
  import { useOpenClose } from '../../hooks/useOpenClose/useOpenClose';
6
+ import { userEvent, within, expect } from '@storybook/test';
6
7
 
7
8
  const meta: Meta<typeof Modal> = {
8
9
  title: 'Components/Modal',
@@ -10,6 +11,7 @@ const meta: Meta<typeof Modal> = {
10
11
  };
11
12
 
12
13
  export default meta;
14
+ type Story = StoryObj<typeof Modal>;
13
15
 
14
16
  export const BasicUsage = () => {
15
17
  const {
@@ -17,8 +19,11 @@ export const BasicUsage = () => {
17
19
  handleOpen: openModal,
18
20
  handleClose: closeModal,
19
21
  } = useOpenClose();
22
+
23
+ const ref = useRef(null);
24
+
20
25
  return (
21
- <div>
26
+ <div id="modalContainer" ref={ref}>
22
27
  <Button variant="primary" onClick={openModal}>
23
28
  Show Modal
24
29
  </Button>
@@ -26,6 +31,7 @@ export const BasicUsage = () => {
26
31
  ariaLabelledBy="titleBasic"
27
32
  isOpen={isModalOpen}
28
33
  onDismiss={closeModal}
34
+ containerRef={ref}
29
35
  >
30
36
  <Modal.Header
31
37
  id="titleBasic"
@@ -34,16 +40,29 @@ export const BasicUsage = () => {
34
40
  />
35
41
  <Modal.Body>Modal content</Modal.Body>
36
42
  <Modal.Footer>
37
- <Button variant="secondary" onClick={closeModal}>
43
+ <Button variant="secondary" onClick={closeModal} shadow="sm">
38
44
  Cancel
39
45
  </Button>
40
- <Button variant="primary">Primary Action</Button>
46
+ <Button variant="primary" shadow="sm">
47
+ Primary Action
48
+ </Button>
41
49
  </Modal.Footer>
42
50
  </Modal>
43
51
  </div>
44
52
  );
45
53
  };
46
54
 
55
+ export const OpenModal: Story = {
56
+ play: async ({ canvasElement, mount }) => {
57
+ await mount(<BasicUsage />);
58
+ const canvas = within(canvasElement);
59
+
60
+ await userEvent.click(canvas.getByText('Show Modal'));
61
+
62
+ await expect(canvas.getByText('Modal content')).toBeInTheDocument();
63
+ },
64
+ };
65
+
47
66
  export const BodyAndFooter = () => {
48
67
  const {
49
68
  isOpen: isModalOpen,
@@ -113,10 +132,12 @@ export const WithoutHeader = () => {
113
132
  >
114
133
  <Modal.Body>Modal content</Modal.Body>
115
134
  <Modal.Footer>
116
- <Button variant="secondary" onClick={closeModal}>
135
+ <Button variant="secondary" onClick={closeModal} shadow="sm">
117
136
  Cancel
118
137
  </Button>
119
- <Button variant="primary">Primary Action</Button>
138
+ <Button variant="primary" shadow="sm">
139
+ Primary Action
140
+ </Button>
120
141
  </Modal.Footer>
121
142
  </Modal>
122
143
  </div>
@@ -147,10 +168,12 @@ export const FullscreenMobile = () => {
147
168
  />
148
169
  <Modal.Body>Modal content</Modal.Body>
149
170
  <Modal.Footer>
150
- <Button variant="secondary" onClick={closeModal}>
171
+ <Button variant="secondary" onClick={closeModal} shadow="sm">
151
172
  Cancel
152
173
  </Button>
153
- <Button variant="primary">Primary Action</Button>
174
+ <Button variant="primary" shadow="sm">
175
+ Primary Action
176
+ </Button>
154
177
  </Modal.Footer>
155
178
  </Modal>
156
179
  </div>
@@ -184,7 +207,12 @@ export const MaxWidth = () => {
184
207
  />
185
208
  <Modal.Body>Modal body content</Modal.Body>
186
209
  <Modal.Footer>
187
- <Button variant="secondary" ref={ref} onClick={closeModal}>
210
+ <Button
211
+ variant="secondary"
212
+ ref={ref}
213
+ onClick={closeModal}
214
+ shadow="sm"
215
+ >
188
216
  Cancel
189
217
  </Button>
190
218
  </Modal.Footer>
@@ -1,11 +1,10 @@
1
1
  import React from 'react';
2
- import { render, fireEvent } from '@testing-library/react';
2
+ import { render } from '@testing-library/react';
3
3
  import { Modal } from './Modal';
4
4
 
5
5
  describe('Modal', () => {
6
6
  test('renders its children', () => {
7
7
  const { getByText } = render(
8
- // eslint-disable-next-line @typescript-eslint/no-empty-function
9
8
  <Modal isOpen onDismiss={() => {}} ariaLabel="testDefault">
10
9
  test modal
11
10
  </Modal>
@@ -15,7 +14,6 @@ describe('Modal', () => {
15
14
 
16
15
  test('it open and closes based on isOpen prop', () => {
17
16
  const { queryByText, getByText, rerender } = render(
18
- // eslint-disable-next-line @typescript-eslint/no-empty-function
19
17
  <Modal isOpen={false} onDismiss={() => {}} ariaLabel="testIsOpen">
20
18
  test modal
21
19
  </Modal>
@@ -24,7 +22,6 @@ describe('Modal', () => {
24
22
  expect(queryByText('test modal')).toBe(null);
25
23
 
26
24
  rerender(
27
- // eslint-disable-next-line @typescript-eslint/no-empty-function
28
25
  <Modal isOpen onDismiss={() => {}} ariaLabel="testIsOpen">
29
26
  test modal
30
27
  </Modal>
@@ -35,9 +32,7 @@ describe('Modal', () => {
35
32
 
36
33
  test('Subcomponents', () => {
37
34
  const { getByText } = render(
38
- // eslint-disable-next-line @typescript-eslint/no-empty-function
39
35
  <Modal isOpen onDismiss={() => {}} ariaLabel="testSubcomponents">
40
- {/* eslint-disable-next-line @typescript-eslint/no-empty-function */}
41
36
  <Modal.Header
42
37
  id="titleFooterBody"
43
38
  title="The Modal Title"
@@ -55,27 +50,39 @@ describe('Modal', () => {
55
50
  ).toBeInTheDocument();
56
51
  });
57
52
 
58
- test('onDismiss', async () => {
59
- const mockOnDismiss = jest.fn();
60
- const { getByTestId } = render(
61
- <Modal isOpen onDismiss={mockOnDismiss} ariaLabel="testSubcomponents">
62
- <Modal.Header
63
- id="titleFooterBody"
64
- title="The Modal Title"
65
- onDismiss={mockOnDismiss}
66
- />
67
- <Modal.Body>Modal body content</Modal.Body>
68
- <Modal.Footer>This is content in the modal footer</Modal.Footer>
53
+ test('applies maxWidth styles', () => {
54
+ const { getByLabelText } = render(
55
+ <Modal
56
+ isOpen
57
+ onDismiss={() => {}}
58
+ ariaLabel="testMaxWidth"
59
+ maxWidth="500px"
60
+ >
61
+ test modal
69
62
  </Modal>
70
63
  );
71
64
 
72
- const closeButton = getByTestId('icon-testid--remove').closest('button');
73
- expect(closeButton).toBeInTheDocument();
65
+ expect(getByLabelText('testMaxWidth').parentElement).toHaveStyle(
66
+ 'max-width: 500px'
67
+ );
68
+ });
74
69
 
75
- if (closeButton) {
76
- await fireEvent.click(closeButton);
77
- }
70
+ test('applies aria-labelledby attribute', () => {
71
+ const { getByLabelText } = render(
72
+ <Modal
73
+ isOpen
74
+ onDismiss={() => {}}
75
+ ariaLabelledBy="modalTitle"
76
+ ariaLabel="testAriaLabelledBy"
77
+ >
78
+ <h1 id="modalTitle">Modal Title</h1>
79
+ test modal
80
+ </Modal>
81
+ );
78
82
 
79
- expect(mockOnDismiss).toHaveBeenCalledTimes(1);
83
+ expect(getByLabelText('testAriaLabelledBy')).toHaveAttribute(
84
+ 'aria-labelledby',
85
+ 'modalTitle'
86
+ );
80
87
  });
81
88
  });
@@ -117,6 +117,8 @@ export const ModalBaseComponent: React.FC<ModalProps> = forwardRef<
117
117
  }
118
118
  );
119
119
 
120
+ if (!isOpen) return null;
121
+
120
122
  const parentElement = containerRef?.current
121
123
  ? (containerRef.current as HTMLElement)
122
124
  : undefined;
@@ -137,13 +139,15 @@ export const ModalBaseComponent: React.FC<ModalProps> = forwardRef<
137
139
  onRequestClose={onDismiss}
138
140
  ariaHideApp={false}
139
141
  parentSelector={parentElement ? () => parentElement : undefined}
142
+ style={{ content: { ...maxWidthCss.styles } }}
140
143
  {...restProps}
141
144
  >
142
145
  <Box
143
146
  aria-label={ariaLabel}
144
147
  aria-labelledby={ariaLabelledBy}
145
- style={{ ...maxWidthCss.styles }}
146
148
  height="100"
149
+ padding={{ base: '2xl', tablet: '4xl' }}
150
+ gap="3xl"
147
151
  >
148
152
  {children}
149
153
  </Box>
@@ -8,13 +8,31 @@ describe('ModalBody', () => {
8
8
  expect(getByText('test modal')).toBeInTheDocument();
9
9
  });
10
10
 
11
- test('xl padding class is applied by default', () => {
12
- const { container } = render(<ModalBody>test modal</ModalBody>);
13
- expect(container.children[0].classList).toContain('p-xl');
14
- });
15
-
16
11
  test('flex-auto class is applied by default', () => {
17
12
  const { container } = render(<ModalBody>test modal</ModalBody>);
18
13
  expect(container.children[0].classList).toContain('flex-auto');
19
14
  });
15
+
16
+ test('applies custom overflow value', () => {
17
+ const { container } = render(
18
+ <ModalBody overflow="hidden">test modal</ModalBody>
19
+ );
20
+ expect(container.children[0].classList).toContain('overflow-hidden');
21
+ });
22
+
23
+ test('applies custom height value', () => {
24
+ const { container } = render(
25
+ <ModalBody height="200px">test modal</ModalBody>
26
+ );
27
+ expect((container.children[0] as HTMLElement).style.height).toBe('200px');
28
+ });
29
+
30
+ test('applies additional props to Box component', () => {
31
+ const { container } = render(
32
+ <ModalBody data-testid="modal-body">test modal</ModalBody>
33
+ );
34
+ expect(container.children[0].getAttribute('data-testid')).toBe(
35
+ 'modal-body'
36
+ );
37
+ });
20
38
  });
@@ -6,13 +6,11 @@ export type ModalBodyProps = BoxProps;
6
6
  export const ModalBody: FC<ModalBodyProps> = ({
7
7
  children,
8
8
  flex = 'auto',
9
- padding = 'xl',
10
9
  overflow = 'auto',
11
10
  height = '100',
12
11
  ...restProps
13
12
  }) => (
14
13
  <Box
15
- padding={padding}
16
14
  flex={flex}
17
15
  overflow={overflow}
18
16
  height={height}
@@ -8,25 +8,61 @@ describe('ModalFooter', () => {
8
8
  expect(getByText('test modal')).toBeInTheDocument();
9
9
  });
10
10
 
11
- test('xl padding class is applied by default', () => {
12
- const { container } = render(<ModalFooter>test modal</ModalFooter>);
13
- expect(container.children[0].classList).toContain('p-xl');
14
- });
15
-
16
11
  test('row direction class is applied by default', () => {
17
12
  const { container } = render(<ModalFooter>test modal</ModalFooter>);
18
- expect(container.children[0].classList).toContain('flex-direction-row');
13
+ expect(container.firstChild).toHaveClass('flex-direction-row');
19
14
  });
20
15
 
21
16
  test('align-items center class is applied by default', () => {
22
17
  const { container } = render(<ModalFooter>test modal</ModalFooter>);
23
- expect(container.children[0].classList).toContain('align-items-center');
18
+ expect(container.firstChild).toHaveClass('align-items-center');
24
19
  });
25
20
 
26
21
  test('justify content flex-end class is applied by default', () => {
27
22
  const { container } = render(<ModalFooter>test modal</ModalFooter>);
28
- expect(container.children[0].classList).toContain(
29
- 'justify-content-flex-end'
23
+ expect(container.firstChild).toHaveClass('justify-content-flex-end');
24
+ });
25
+
26
+ test('applies custom padding', () => {
27
+ const { container } = render(
28
+ <ModalFooter padding="lg">test modal</ModalFooter>
29
+ );
30
+ expect(container.firstChild).toHaveStyle('padding: lg');
31
+ });
32
+
33
+ test('applies custom direction', () => {
34
+ const { container } = render(
35
+ <ModalFooter direction="column">test modal</ModalFooter>
36
+ );
37
+ expect(container.firstChild).toHaveClass('flex-direction-column');
38
+ });
39
+
40
+ test('applies custom alignItems', () => {
41
+ const { container } = render(
42
+ <ModalFooter alignItems="flex-start">test modal</ModalFooter>
43
+ );
44
+ expect(container.firstChild).toHaveClass('align-items-flex-start');
45
+ });
46
+
47
+ test('applies custom justifyContent', () => {
48
+ const { container } = render(
49
+ <ModalFooter justifyContent="center">test modal</ModalFooter>
50
+ );
51
+ expect(container.firstChild).toHaveClass('justify-content-center');
52
+ });
53
+
54
+ test('applies custom gap', () => {
55
+ const { container } = render(
56
+ <ModalFooter gap="lg">test modal</ModalFooter>
57
+ );
58
+ expect(container.firstChild).toHaveClass('g-lg');
59
+ });
60
+
61
+ test('applies custom styles', () => {
62
+ const customStyle = { backgroundColor: 'red' };
63
+ const { container } = render(
64
+ <ModalFooter style={customStyle}>test modal</ModalFooter>
30
65
  );
66
+ expect(container.firstChild).toHaveStyle('background-color: red');
31
67
  });
32
68
  });
@@ -1,30 +1,23 @@
1
1
  import React, { FC } from 'react';
2
2
  import { Box, BoxProps } from '../../../Box/Box';
3
3
 
4
- export type ModalFooterProps = Omit<
5
- BoxProps,
6
- 'as' | 'background' | 'borderColor' | 'borderWidth' | 'radius'
7
- >;
4
+ export type ModalFooterProps = Omit<BoxProps, 'as' | 'radius'>;
8
5
 
9
6
  export const ModalFooter: FC<ModalFooterProps> = ({
10
7
  children,
11
- padding = 'xl',
8
+ padding,
12
9
  direction = 'row',
13
10
  alignItems = 'center',
14
11
  justifyContent = 'flex-end',
15
- background = 'secondary',
16
- gap = 'sm',
12
+ gap = 'md',
17
13
  style,
18
14
  ...restProps
19
15
  }) => (
20
16
  <Box
21
- background={background}
22
17
  padding={padding}
23
18
  direction={direction}
24
19
  alignItems={alignItems}
25
20
  justifyContent={justifyContent}
26
- borderWidth="sm 0 0 0"
27
- borderColor="default"
28
21
  gap={gap}
29
22
  style={{
30
23
  flexShrink: 0,
@@ -19,11 +19,30 @@ describe('ModalHeader', () => {
19
19
  const mockOnDismiss = jest.fn();
20
20
  render(<ModalHeader id="modal" onDismiss={mockOnDismiss} />);
21
21
  fireEvent.click(screen.getByLabelText('close'));
22
- expect(mockOnDismiss).toBeCalledTimes(1);
22
+ expect(mockOnDismiss).toHaveBeenCalledTimes(1);
23
23
  });
24
24
 
25
- test('xl padding class is applied by default', () => {
26
- const { container } = render(<ModalHeader id="modal" />);
27
- expect(container.children[0].classList).toContain('p-xl');
25
+ test('does not render title if not provided', () => {
26
+ const { queryByText } = render(<ModalHeader id="modal" />);
27
+ expect(queryByText('modal title')).toBeNull();
28
+ });
29
+
30
+ test('does not render close button if onDismiss is not set', () => {
31
+ render(<ModalHeader id="modal" />);
32
+ expect(screen.queryByLabelText('close')).toBeNull();
33
+ });
34
+
35
+ test('renders with correct id for title', () => {
36
+ const { getByText } = render(
37
+ <ModalHeader id="modal-title" title="modal title" />
38
+ );
39
+ expect(getByText('modal title').id).toBe('modal-title');
40
+ });
41
+
42
+ test('renders with correct styles', () => {
43
+ const { container } = render(
44
+ <ModalHeader id="modal" title="modal title" />
45
+ );
46
+ expect(container.firstChild).toHaveStyle('flex-shrink: 0');
28
47
  });
29
48
  });
@@ -1,7 +1,6 @@
1
1
  import React, { FC } from 'react';
2
2
  import { Box } from '../../../Box/Box';
3
- import { Icon } from '../../../Icon/Icon';
4
- import styles from '../../Modal.module.scss';
3
+ import { Button } from '../../../Button/Button';
5
4
 
6
5
  export type ModalHeaderProps = {
7
6
  /**
@@ -28,12 +27,10 @@ export const ModalHeader: FC<ModalHeaderProps> = ({
28
27
 
29
28
  return (
30
29
  <Box
31
- padding="xl"
32
30
  direction="row"
33
31
  alignItems="center"
34
32
  justifyContent={justifyContentValue}
35
- borderWidth="0 0 sm 0"
36
- borderColor="default"
33
+ gap="3xl"
37
34
  style={{
38
35
  flexShrink: 0,
39
36
  }}
@@ -44,14 +41,13 @@ export const ModalHeader: FC<ModalHeaderProps> = ({
44
41
  </Box>
45
42
  )}
46
43
  {onDismiss && (
47
- <button
44
+ <Button
48
45
  aria-label="close"
49
- type="button"
50
- className={styles['modal-close']}
46
+ variant="tertiary"
51
47
  onClick={onDismiss}
52
- >
53
- <Icon name="remove" />
54
- </button>
48
+ iconPrefix="remove"
49
+ size="sm"
50
+ />
55
51
  )}
56
52
  </Box>
57
53
  );
@@ -10,7 +10,8 @@
10
10
  left: 0;
11
11
  align-items: center;
12
12
  justify-content: center;
13
- background: rgb(255 255 255 / 50%);
13
+ background: var(--color-background-primary);
14
+ opacity: 0.5;
14
15
  }
15
16
 
16
17
  .scroll-container {
@@ -7,7 +7,7 @@
7
7
  border-bottom-style: solid;
8
8
  border-width: 0 0 var(--size-border-width-sm) 0;
9
9
  background-color: var(--color-background-primary);
10
- color: var(--color-font-primary);
10
+ color: var(--color-font-secondary);
11
11
  font-size: var(--size-font-size-sm);
12
12
  font-weight: var(--size-font-weight-bold);
13
13
  padding: var(--size-spacing-md)