@scality/core-ui 0.115.0 → 0.117.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 (56) hide show
  1. package/.github/workflows/dependency-review.yaml +2 -0
  2. package/.storybook/preview.js +32 -18
  3. package/README.md +1 -1
  4. package/dist/components/buttonv2/Buttonv2.component.d.ts +1 -0
  5. package/dist/components/buttonv2/Buttonv2.component.d.ts.map +1 -1
  6. package/dist/components/buttonv2/Buttonv2.component.js +9 -8
  7. package/dist/components/card/Card.component.d.ts.map +1 -1
  8. package/dist/components/card/Card.component.js +11 -11
  9. package/dist/components/checkbox/Checkbox.component.d.ts.map +1 -1
  10. package/dist/components/checkbox/Checkbox.component.js +2 -2
  11. package/dist/components/dropdown/Dropdown.component.d.ts.map +1 -1
  12. package/dist/components/dropdown/Dropdown.component.js +41 -57
  13. package/dist/components/inlineinput/InlineInput.d.ts +17 -0
  14. package/dist/components/inlineinput/InlineInput.d.ts.map +1 -0
  15. package/dist/components/inlineinput/InlineInput.js +85 -0
  16. package/dist/components/inputv2/inputv2.d.ts +8 -0
  17. package/dist/components/inputv2/inputv2.d.ts.map +1 -1
  18. package/dist/components/modal/Modal.component.d.ts.map +1 -1
  19. package/dist/components/modal/Modal.component.js +18 -1
  20. package/dist/components/navbar/Navbar.component.d.ts +1 -0
  21. package/dist/components/navbar/Navbar.component.d.ts.map +1 -1
  22. package/dist/components/navbar/Navbar.component.js +23 -6
  23. package/dist/components/sidebar/Sidebar.component.d.ts.map +1 -1
  24. package/dist/components/sidebar/Sidebar.component.js +18 -1
  25. package/dist/components/tablev2/MultiSelectableContent.d.ts.map +1 -1
  26. package/dist/components/tablev2/MultiSelectableContent.js +10 -1
  27. package/dist/components/tablev2/SingleSelectableContent.d.ts.map +1 -1
  28. package/dist/components/tablev2/SingleSelectableContent.js +20 -1
  29. package/dist/components/tablev2/Tablestyle.d.ts +1 -0
  30. package/dist/components/tablev2/Tablestyle.d.ts.map +1 -1
  31. package/dist/components/tablev2/Tablestyle.js +10 -1
  32. package/dist/components/text/Text.component.d.ts.map +1 -1
  33. package/dist/components/text/Text.component.js +2 -2
  34. package/dist/index.d.ts +1 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +1 -0
  37. package/dist/spacing.js +1 -1
  38. package/package.json +23 -51
  39. package/src/lib/components/buttonv2/Buttonv2.component.tsx +11 -8
  40. package/src/lib/components/card/Card.component.tsx +11 -11
  41. package/src/lib/components/checkbox/Checkbox.component.tsx +2 -2
  42. package/src/lib/components/dropdown/Dropdown.component.tsx +65 -81
  43. package/src/lib/components/inlineinput/InlineInput.test.tsx +211 -0
  44. package/src/lib/components/inlineinput/InlineInput.tsx +179 -0
  45. package/src/lib/components/inputv2/inputv2.tsx +2 -2
  46. package/src/lib/components/modal/Modal.component.tsx +19 -1
  47. package/src/lib/components/navbar/Navbar.component.tsx +27 -19
  48. package/src/lib/components/sidebar/Sidebar.component.tsx +21 -0
  49. package/src/lib/components/tablev2/MultiSelectableContent.tsx +17 -1
  50. package/src/lib/components/tablev2/SingleSelectableContent.tsx +29 -1
  51. package/src/lib/components/tablev2/Tablestyle.tsx +15 -2
  52. package/src/lib/components/text/Text.component.tsx +2 -2
  53. package/src/lib/index.ts +1 -0
  54. package/src/lib/spacing.tsx +1 -1
  55. package/stories/InlineInput/InlineInput.stories.tsx +46 -0
  56. package/stories/navbar.stories.tsx +18 -0
@@ -12,6 +12,8 @@ import { spacing } from '../../spacing';
12
12
  import { fontSize } from '../../style/theme';
13
13
  import { getThemePropSelector } from '../../utils';
14
14
  import { Icon } from '../icon/Icon.component';
15
+ import { useSelect } from 'downshift';
16
+ import { FocusVisibleStyle } from '../buttonv2/Buttonv2.component';
15
17
  export type Item = {
16
18
  label: string;
17
19
  name?: string;
@@ -41,38 +43,14 @@ const DropdownMenuStyled = styled.ul`
41
43
  position: absolute;
42
44
  margin: 0;
43
45
  padding: 0;
46
+ top: 50px;
44
47
  border: 1px solid ${getThemePropSelector('backgroundLevel1')};
45
48
  z-index: ${zIndex.dropdown};
46
49
  max-height: 200px;
47
50
  min-width: 100%;
48
51
  overflow: auto;
49
52
  border-bottom: 0.3px solid ${getThemePropSelector('border')};
50
- ${(props) => {
51
- if (
52
- props.size &&
53
- props.triggerSize &&
54
- props.triggerSize.x + props.size.width > window.innerWidth
55
- ) {
56
- return css`
57
- right: 0;
58
- top: 100%;
59
- `;
60
- } else if (
61
- props.size &&
62
- props.triggerSize &&
63
- props.triggerSize.y + props.size.height > window.innerHeight
64
- ) {
65
- return css`
66
- left: 0;
67
- bottom: ${props.triggerSize.height + 'px'};
68
- `;
69
- } else {
70
- return css`
71
- left: 0;
72
- top: 100%;
73
- `;
74
- }
75
- }};
53
+ display: ${(props) => (props.isOpen ? 'auto' : 'none')};
76
54
  `;
77
55
  const DropdownMenuItemStyled = styled.li`
78
56
  display: flex;
@@ -81,25 +59,36 @@ const DropdownMenuItemStyled = styled.li`
81
59
  white-space: nowrap;
82
60
  cursor: pointer;
83
61
  font-size: ${fontSize.base};
62
+ ${(props) => {
63
+ console.log(props.isSelected);
64
+ return props.isSelected
65
+ ? `background-color: ${props.theme.highlight};`
66
+ : `background-color: ${props.theme.backgroundLevel1};`;
67
+ }}
68
+
69
+ color: ${getThemePropSelector('textPrimary')};
70
+ border-top: 0.3px solid ${getThemePropSelector('border')};
71
+ border-left: 0.3px solid ${getThemePropSelector('border')};
72
+ border-right: 0.3px solid ${getThemePropSelector('border')};
84
73
 
85
- ${css`
86
- background-color: ${getThemePropSelector('backgroundLevel1')};
87
- color: ${getThemePropSelector('textPrimary')};
88
- border-top: 0.3px solid ${getThemePropSelector('border')};
89
- border-left: 0.3px solid ${getThemePropSelector('border')};
90
- border-right: 0.3px solid ${getThemePropSelector('border')};
91
- &:hover {
92
- background-color: ${getThemePropSelector('highlight')};
93
- }
94
- &:active {
95
- background-color: ${getThemePropSelector('highlight')};
96
- }
97
- `};
74
+ &:hover {
75
+ background-color: ${getThemePropSelector('highlight')};
76
+ }
77
+ &:active {
78
+ background-color: ${getThemePropSelector('highlight')};
79
+ }
98
80
  `;
99
81
  const Caret = styled.span`
100
82
  margin-left: ${spacing.r16};
101
83
  `;
102
- const TriggerStyled = ButtonStyled.withComponent('div');
84
+ const Trigger = ButtonStyled.withComponent('div');
85
+ const TriggerStyled = styled(Trigger)`
86
+ // :focus-visible is the keyboard-only version of :focus
87
+ &:focus-visible {
88
+ ${FocusVisibleStyle}
89
+ color: ${(props) => props.theme.textPrimary};
90
+ }
91
+ `;
103
92
 
104
93
  function Dropdown({
105
94
  items,
@@ -111,19 +100,16 @@ function Dropdown({
111
100
  caret = true,
112
101
  ...rest
113
102
  }: Props) {
114
- const [open, setOpen] = useState(false);
115
- const [menuSize, setMenuSize] = useState();
116
- const [triggerSize, setTriggerSize] = useState();
117
- const refMenuCallback = useCallback((node) => {
118
- if (node !== null) {
119
- setMenuSize(node.getBoundingClientRect());
120
- }
121
- }, []);
122
- const refTriggerCallback = useCallback((node) => {
123
- if (node !== null) {
124
- setTriggerSize(node.getBoundingClientRect());
125
- }
126
- }, []);
103
+ const {
104
+ isOpen,
105
+ getToggleButtonProps,
106
+ getMenuProps,
107
+ getItemProps,
108
+ highlightedIndex,
109
+ } = useSelect({
110
+ items,
111
+ itemToString: (item) => item?.label || '',
112
+ });
127
113
  return (
128
114
  <DropdownStyled
129
115
  active={open}
@@ -135,12 +121,8 @@ function Dropdown({
135
121
  variant={variant}
136
122
  size={size}
137
123
  className="trigger"
138
- onBlur={() => setOpen(!open)}
139
- onFocus={() => setOpen(!open)}
140
- onClick={(event) => event.stopPropagation()}
141
- tabIndex="0"
142
124
  title={title}
143
- ref={refTriggerCallback}
125
+ {...getToggleButtonProps()}
144
126
  >
145
127
  {icon && (
146
128
  <ButtonIcon text={text} size={size}>
@@ -153,29 +135,31 @@ function Dropdown({
153
135
  <Icon name="Dropdown-down" />
154
136
  </Caret>
155
137
  )}
156
- {open && (
157
- <DropdownMenuStyled
158
- className="menu-item"
159
- postion={'right'}
160
- ref={refMenuCallback}
161
- size={menuSize}
162
- triggerSize={triggerSize}
163
- >
164
- {items.map(({ label, onClick, ...itemRest }) => {
165
- return (
166
- <DropdownMenuItemStyled
167
- className="menu-item-label"
168
- key={label}
169
- onClick={onClick}
170
- variant={variant}
171
- {...itemRest}
172
- >
173
- {label}
174
- </DropdownMenuItemStyled>
175
- );
176
- })}
177
- </DropdownMenuStyled>
178
- )}
138
+
139
+ <DropdownMenuStyled
140
+ className="menu-item"
141
+ isOpen={isOpen}
142
+ {...getMenuProps()}
143
+ >
144
+ {items.map((item, index) => {
145
+ return (
146
+ <DropdownMenuItemStyled
147
+ className="menu-item-label"
148
+ key={item.label}
149
+ variant={item.variant}
150
+ {...item}
151
+ {...getItemProps({
152
+ item,
153
+ index,
154
+ onClick: item.onClick,
155
+ })}
156
+ isSelected={index === highlightedIndex}
157
+ >
158
+ {item.label}
159
+ </DropdownMenuItemStyled>
160
+ );
161
+ })}
162
+ </DropdownMenuStyled>
179
163
  </TriggerStyled>
180
164
  </DropdownStyled>
181
165
  );
@@ -0,0 +1,211 @@
1
+ import React, { PropsWithChildren } from 'react';
2
+ import {
3
+ QueryClient,
4
+ QueryClientProvider,
5
+ useMutation,
6
+ UseMutationResult,
7
+ } from 'react-query';
8
+ import { ToastProvider } from '../toast/ToastProvider';
9
+ import {
10
+ render,
11
+ screen,
12
+ waitFor,
13
+ waitForElementToBeRemoved,
14
+ within,
15
+ } from '@testing-library/react';
16
+ import userEvent from '@testing-library/user-event';
17
+ import { InlineInput } from './InlineInput';
18
+
19
+ const queryClient = new QueryClient();
20
+ const Wrapper = ({ children }: PropsWithChildren<{}>) => {
21
+ return (
22
+ <ToastProvider>
23
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
24
+ </ToastProvider>
25
+ );
26
+ };
27
+
28
+ const ChangeMutationProvider = ({
29
+ onChange,
30
+ children,
31
+ }: {
32
+ onChange: (value: string) => void;
33
+ children: ({
34
+ changeMutation,
35
+ }: {
36
+ changeMutation: UseMutationResult<unknown, unknown, { value: string }>;
37
+ }) => JSX.Element;
38
+ }) => {
39
+ const changeMutation = useMutation<unknown, unknown, { value: string }>({
40
+ mutationFn: ({ value }) => {
41
+ return new Promise((resolve) => {
42
+ onChange(value);
43
+ resolve(null);
44
+ });
45
+ },
46
+ });
47
+ return <>{children({ changeMutation })}</>;
48
+ };
49
+
50
+ const selectors = {
51
+ confirmationModal: () => screen.getByRole('dialog', { name: /Confirm/i }),
52
+ };
53
+
54
+ describe('InlineInput', () => {
55
+ describe('when the user clicks accepts the edit', () => {
56
+ test('without confirmation modal', async () => {
57
+ //S
58
+ const mock = jest.fn();
59
+ render(
60
+ <ChangeMutationProvider onChange={mock}>
61
+ {({ changeMutation }) => (
62
+ <InlineInput
63
+ id="test"
64
+ defaultValue={'test'}
65
+ changeMutation={changeMutation}
66
+ />
67
+ )}
68
+ </ChangeMutationProvider>,
69
+ { wrapper: Wrapper },
70
+ );
71
+
72
+ //E
73
+ /// First focus the edit button
74
+ await userEvent.tab();
75
+ /// Then press enter to edit the input
76
+ await userEvent.keyboard('{enter}');
77
+ /// Then type a new value
78
+ await userEvent.type(document.activeElement, 'new value');
79
+ /// Then press enter to confirm the new value
80
+ await userEvent.keyboard('{enter}');
81
+ await waitForElementToBeRemoved(() => screen.getByRole('textbox'));
82
+
83
+ //V
84
+ expect(mock).toHaveBeenCalledWith('testnew value');
85
+ expect(mock).toHaveBeenCalledTimes(1);
86
+ expect(screen.getByText('testnew value')).toBeInTheDocument();
87
+ });
88
+
89
+ test('with confirmation modal', async () => {
90
+ //S
91
+ const mock = jest.fn();
92
+ render(
93
+ <ChangeMutationProvider onChange={mock}>
94
+ {({ changeMutation }) => (
95
+ <InlineInput
96
+ id="test"
97
+ defaultValue={'test'}
98
+ changeMutation={changeMutation}
99
+ confirmationModal={{
100
+ title: <div>Confirm</div>,
101
+ body: <div>Are you sure?</div>,
102
+ }}
103
+ />
104
+ )}
105
+ </ChangeMutationProvider>,
106
+ { wrapper: Wrapper },
107
+ );
108
+
109
+ //E
110
+ /// First focus the edit button
111
+ await userEvent.tab();
112
+ /// Then press enter to edit the input
113
+ await userEvent.keyboard('{enter}');
114
+ /// Then type a new value
115
+ await userEvent.type(document.activeElement, 'new value');
116
+ /// Then press enter to confirm the new value
117
+ await userEvent.keyboard('{enter}');
118
+ /// Expect the confirmation modal to be opened
119
+ await waitFor(() =>
120
+ expect(selectors.confirmationModal()).toBeInTheDocument(),
121
+ );
122
+ /// Click the confirm button
123
+ await userEvent.click(screen.getByRole('button', { name: /confirm/i }));
124
+ /// Wait for modal to be closed
125
+ await waitForElementToBeRemoved(() => selectors.confirmationModal());
126
+
127
+ //V
128
+ expect(mock).toHaveBeenCalledWith('testnew value');
129
+ expect(mock).toHaveBeenCalledTimes(1);
130
+ expect(screen.getByText('testnew value')).toBeInTheDocument();
131
+ });
132
+ });
133
+
134
+ describe('when the user clicks reset the edit', () => {
135
+ test('without confirmation modal', async () => {
136
+ //S
137
+ const mock = jest.fn();
138
+ render(
139
+ <ChangeMutationProvider onChange={mock}>
140
+ {({ changeMutation }) => (
141
+ <InlineInput
142
+ id="test"
143
+ defaultValue={'test'}
144
+ changeMutation={changeMutation}
145
+ />
146
+ )}
147
+ </ChangeMutationProvider>,
148
+ { wrapper: Wrapper },
149
+ );
150
+
151
+ //E
152
+ /// First focus the edit button
153
+ await userEvent.tab();
154
+ /// Then press enter to edit the input
155
+ await userEvent.keyboard('{enter}');
156
+ /// Then type a new value
157
+ await userEvent.type(document.activeElement, 'new value');
158
+ /// Then press escape to cancel the new value
159
+ await userEvent.keyboard('{esc}');
160
+
161
+ //V
162
+ expect(mock).not.toHaveBeenCalled();
163
+ expect(screen.getByText('test')).toBeInTheDocument();
164
+ });
165
+ test('with confirmation modal', async () => {
166
+ //S
167
+ const mock = jest.fn();
168
+ render(
169
+ <ChangeMutationProvider onChange={mock}>
170
+ {({ changeMutation }) => (
171
+ <InlineInput
172
+ id="test"
173
+ defaultValue={'test'}
174
+ changeMutation={changeMutation}
175
+ confirmationModal={{
176
+ title: <div>Confirm</div>,
177
+ body: <div>Are you sure?</div>,
178
+ }}
179
+ />
180
+ )}
181
+ </ChangeMutationProvider>,
182
+ { wrapper: Wrapper },
183
+ );
184
+
185
+ //E
186
+ /// First focus the edit button
187
+ await userEvent.tab();
188
+ /// Then press enter to edit the input
189
+ await userEvent.keyboard('{enter}');
190
+ /// Then type a new value
191
+ await userEvent.type(document.activeElement, 'new value');
192
+ /// Then press enter to confirm the new value
193
+ await userEvent.keyboard('{enter}');
194
+ /// Expect the confirmation modal to be opened
195
+ await waitFor(() =>
196
+ expect(selectors.confirmationModal()).toBeInTheDocument(),
197
+ );
198
+ /// Click the cancel button
199
+ await userEvent.click(
200
+ within(selectors.confirmationModal()).getByRole('button', {
201
+ name: /Cancel/i,
202
+ }),
203
+ );
204
+
205
+ //V
206
+ expect(mock).not.toHaveBeenCalled();
207
+ expect(screen.getByRole('textbox')).toBeInTheDocument();
208
+ expect(screen.getByRole('textbox').value).toBe('testnew value');
209
+ });
210
+ });
211
+ });
@@ -0,0 +1,179 @@
1
+ import styled from 'styled-components';
2
+ import { Button } from '../buttonv2/Buttonv2.component';
3
+ import { Icon } from '../icon/Icon.component';
4
+ import { Input, InputProps } from '../inputv2/inputv2';
5
+ import { Modal } from '../modal/Modal.component';
6
+ import { useToast } from '../toast/ToastProvider';
7
+ import { useForm } from 'react-hook-form';
8
+ import { UseMutationResult } from 'react-query';
9
+ import { Text } from '../text/Text.component';
10
+ import { useState } from 'react';
11
+ import { Stack, Wrap } from '../../spacing';
12
+
13
+ const UnderlinedText = styled(Text)`
14
+ text-decoration-line: underline;
15
+ text-decoration-style: dashed;
16
+ cursor: text;
17
+ `;
18
+
19
+ type InlineInputForm = {
20
+ value: string;
21
+ };
22
+ type InlineInputProps = {
23
+ defaultValue?: string;
24
+ confirmationModal?: {
25
+ title: JSX.Element;
26
+ body: JSX.Element;
27
+ };
28
+ changeMutation: UseMutationResult<unknown, unknown, InlineInputForm, unknown>;
29
+ } & InputProps;
30
+
31
+ export const InlineInput = ({
32
+ defaultValue,
33
+ confirmationModal,
34
+ changeMutation,
35
+ ...props
36
+ }: InlineInputProps) => {
37
+ const { register, handleSubmit, watch, reset } = useForm<InlineInputForm>({
38
+ defaultValues: {
39
+ value: defaultValue,
40
+ },
41
+ });
42
+ const [isConfirmationModalOpened, setIsConfirmationModalOpened] =
43
+ useState(false);
44
+ const handleSuccess = () => {
45
+ setIsConfirmationModalOpened(false);
46
+ setIsEditing(false);
47
+ setIsHover(false);
48
+ };
49
+ const onSubmit = (data: InlineInputForm) => {
50
+ if (confirmationModal) {
51
+ setIsConfirmationModalOpened(true);
52
+ } else {
53
+ changeMutation.mutate(data, {
54
+ onSuccess: () => {
55
+ handleSuccess();
56
+ },
57
+ onError: () => {
58
+ showToast({
59
+ open: true,
60
+ status: 'error',
61
+ message: 'An error occurred while updating the value',
62
+ });
63
+ },
64
+ });
65
+ }
66
+ };
67
+ const { showToast } = useToast();
68
+ const [isHover, setIsHover] = useState(false);
69
+ const [isEditing, setIsEditing] = useState(false);
70
+
71
+ const handleReset = () => {
72
+ reset();
73
+ setIsEditing(false);
74
+ setIsHover(false);
75
+ };
76
+
77
+ //handle esc key to cancel editing
78
+ const handleKeyDown = (event: React.KeyboardEvent) => {
79
+ if (event.key === 'Escape') {
80
+ handleReset();
81
+ }
82
+ };
83
+
84
+ if (isEditing) {
85
+ return (
86
+ <>
87
+ <form onSubmit={handleSubmit(onSubmit)}>
88
+ <Stack>
89
+ <Input
90
+ {...register('value')}
91
+ size="1/3"
92
+ autoFocus
93
+ onKeyDown={handleKeyDown}
94
+ {...props}
95
+ />
96
+ <Button
97
+ icon={<Icon name="Close" />}
98
+ tooltip={{
99
+ overlay: 'Cancel',
100
+ }}
101
+ type="reset"
102
+ variant="outline"
103
+ onClick={handleReset}
104
+ />
105
+ <Button
106
+ icon={<Icon name="Check" />}
107
+ tooltip={{
108
+ overlay: 'Save',
109
+ }}
110
+ variant="primary"
111
+ type="submit"
112
+ isLoading={changeMutation.isLoading}
113
+ />
114
+ </Stack>
115
+ </form>
116
+ {confirmationModal && (
117
+ <Modal
118
+ isOpen={isConfirmationModalOpened}
119
+ title={confirmationModal.title}
120
+ footer={
121
+ <Wrap>
122
+ <p></p>
123
+ <Stack>
124
+ <Button
125
+ label="Cancel"
126
+ variant="outline"
127
+ onClick={() => setIsConfirmationModalOpened(false)}
128
+ />
129
+ <Button
130
+ label="Confirm"
131
+ variant="primary"
132
+ isLoading={changeMutation.isLoading}
133
+ onClick={() => {
134
+ changeMutation.mutate(watch(), {
135
+ onSuccess: () => {
136
+ handleSuccess();
137
+ },
138
+ onError: () => {
139
+ showToast({
140
+ open: true,
141
+ status: 'error',
142
+ message:
143
+ 'An error occurred while updating the value',
144
+ });
145
+ },
146
+ });
147
+ }}
148
+ />
149
+ </Stack>
150
+ </Wrap>
151
+ }
152
+ >
153
+ {confirmationModal.body}
154
+ </Modal>
155
+ )}
156
+ </>
157
+ );
158
+ }
159
+
160
+ return (
161
+ <Stack
162
+ onMouseEnter={() => setIsHover(true)}
163
+ onMouseLeave={() => setIsHover(false)}
164
+ onFocus={() => setIsHover(true)}
165
+ onBlur={() => setIsHover(false)}
166
+ >
167
+ <UnderlinedText>{watch('value')}</UnderlinedText>
168
+ <Button
169
+ icon={<Icon name="Pencil" />}
170
+ tooltip={{
171
+ overlay: 'Edit',
172
+ }}
173
+ variant="primary"
174
+ onClick={() => setIsEditing(true)}
175
+ style={{ opacity: !isHover ? '0' : '1' }}
176
+ />
177
+ </Stack>
178
+ );
179
+ };
@@ -89,7 +89,7 @@ const SelfCenterredIcon = styled(Icon)`
89
89
 
90
90
  type InputSize = '1' | '2/3' | '1/2' | '1/3';
91
91
 
92
- type Props = {
92
+ export type InputProps = {
93
93
  error?: string;
94
94
  id: string;
95
95
  leftIcon?: IconName;
@@ -97,7 +97,7 @@ type Props = {
97
97
  size?: InputSize;
98
98
  } & Omit<InputHTMLAttributes<HTMLInputElement>, 'size'>;
99
99
 
100
- export const Input = forwardRef<HTMLInputElement, Props>(
100
+ export const Input = forwardRef<HTMLInputElement, InputProps>(
101
101
  (
102
102
  {
103
103
  error,
@@ -1,4 +1,4 @@
1
- import { ReactNode, useLayoutEffect, useRef } from 'react';
1
+ import { ReactNode, useEffect, useLayoutEffect, useRef } from 'react';
2
2
  import ReactDom from 'react-dom';
3
3
  import styled from 'styled-components';
4
4
  import { Wrap, spacing } from '../../spacing';
@@ -76,6 +76,24 @@ const Modal = ({
76
76
  document.body && document.body.removeChild(modalContainer.current);
77
77
  };
78
78
  }, [modalContainer]);
79
+
80
+ useEffect(() => {
81
+ if (isOpen) {
82
+ //Auto focus the modal when it opens
83
+ modalContainer.current.setAttribute('tabindex', '0');
84
+ modalContainer.current.focus();
85
+ //Listen to esc key to close the modal
86
+ const handleEsc = (event: KeyboardEvent) => {
87
+ if (event.key === 'Escape') {
88
+ close && close();
89
+ }
90
+ };
91
+ document.addEventListener('keydown', handleEsc);
92
+ return () => {
93
+ document.removeEventListener('keydown', handleEsc);
94
+ };
95
+ }
96
+ }, [isOpen]);
79
97
  return isOpen
80
98
  ? ReactDom.createPortal(
81
99
  <ModalContainer