mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-22 17:33:23 +01:00
Adding ModalFocus component (#18979)
This commit is contained in:
parent
62ffd8022a
commit
32688c2e3c
@ -3910,6 +3910,64 @@
|
||||
"setTimeout": true
|
||||
}
|
||||
},
|
||||
"react-focus-lock": {
|
||||
"globals": {
|
||||
"addEventListener": true,
|
||||
"console.error": true,
|
||||
"console.warn": true,
|
||||
"document": true,
|
||||
"removeEventListener": true,
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"@babel/runtime": true,
|
||||
"prop-types": true,
|
||||
"react": true,
|
||||
"react-focus-lock>focus-lock": true,
|
||||
"react-focus-lock>react-clientside-effect": true,
|
||||
"react-focus-lock>use-callback-ref": true,
|
||||
"react-focus-lock>use-sidecar": true
|
||||
}
|
||||
},
|
||||
"react-focus-lock>focus-lock": {
|
||||
"globals": {
|
||||
"HTMLIFrameElement": true,
|
||||
"Node.DOCUMENT_FRAGMENT_NODE": true,
|
||||
"Node.DOCUMENT_NODE": true,
|
||||
"Node.DOCUMENT_POSITION_CONTAINED_BY": true,
|
||||
"Node.DOCUMENT_POSITION_CONTAINS": true,
|
||||
"Node.ELEMENT_NODE": true,
|
||||
"console.error": true,
|
||||
"console.warn": true,
|
||||
"document": true,
|
||||
"getComputedStyle": true,
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"wait-on>rxjs>tslib": true
|
||||
}
|
||||
},
|
||||
"react-focus-lock>react-clientside-effect": {
|
||||
"packages": {
|
||||
"react": true,
|
||||
"react-focus-lock>react-clientside-effect>@babel/runtime": true
|
||||
}
|
||||
},
|
||||
"react-focus-lock>use-callback-ref": {
|
||||
"packages": {
|
||||
"react": true
|
||||
}
|
||||
},
|
||||
"react-focus-lock>use-sidecar": {
|
||||
"globals": {
|
||||
"console.error": true
|
||||
},
|
||||
"packages": {
|
||||
"react": true,
|
||||
"react-focus-lock>use-sidecar>detect-node-es": true,
|
||||
"wait-on>rxjs>tslib": true
|
||||
}
|
||||
},
|
||||
"react-idle-timer": {
|
||||
"globals": {
|
||||
"clearTimeout": true,
|
||||
|
@ -4362,6 +4362,64 @@
|
||||
"setTimeout": true
|
||||
}
|
||||
},
|
||||
"react-focus-lock": {
|
||||
"globals": {
|
||||
"addEventListener": true,
|
||||
"console.error": true,
|
||||
"console.warn": true,
|
||||
"document": true,
|
||||
"removeEventListener": true,
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"@babel/runtime": true,
|
||||
"prop-types": true,
|
||||
"react": true,
|
||||
"react-focus-lock>focus-lock": true,
|
||||
"react-focus-lock>react-clientside-effect": true,
|
||||
"react-focus-lock>use-callback-ref": true,
|
||||
"react-focus-lock>use-sidecar": true
|
||||
}
|
||||
},
|
||||
"react-focus-lock>focus-lock": {
|
||||
"globals": {
|
||||
"HTMLIFrameElement": true,
|
||||
"Node.DOCUMENT_FRAGMENT_NODE": true,
|
||||
"Node.DOCUMENT_NODE": true,
|
||||
"Node.DOCUMENT_POSITION_CONTAINED_BY": true,
|
||||
"Node.DOCUMENT_POSITION_CONTAINS": true,
|
||||
"Node.ELEMENT_NODE": true,
|
||||
"console.error": true,
|
||||
"console.warn": true,
|
||||
"document": true,
|
||||
"getComputedStyle": true,
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"wait-on>rxjs>tslib": true
|
||||
}
|
||||
},
|
||||
"react-focus-lock>react-clientside-effect": {
|
||||
"packages": {
|
||||
"react": true,
|
||||
"react-focus-lock>react-clientside-effect>@babel/runtime": true
|
||||
}
|
||||
},
|
||||
"react-focus-lock>use-callback-ref": {
|
||||
"packages": {
|
||||
"react": true
|
||||
}
|
||||
},
|
||||
"react-focus-lock>use-sidecar": {
|
||||
"globals": {
|
||||
"console.error": true
|
||||
},
|
||||
"packages": {
|
||||
"react": true,
|
||||
"react-focus-lock>use-sidecar>detect-node-es": true,
|
||||
"wait-on>rxjs>tslib": true
|
||||
}
|
||||
},
|
||||
"react-idle-timer": {
|
||||
"globals": {
|
||||
"clearTimeout": true,
|
||||
|
@ -4362,6 +4362,64 @@
|
||||
"setTimeout": true
|
||||
}
|
||||
},
|
||||
"react-focus-lock": {
|
||||
"globals": {
|
||||
"addEventListener": true,
|
||||
"console.error": true,
|
||||
"console.warn": true,
|
||||
"document": true,
|
||||
"removeEventListener": true,
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"@babel/runtime": true,
|
||||
"prop-types": true,
|
||||
"react": true,
|
||||
"react-focus-lock>focus-lock": true,
|
||||
"react-focus-lock>react-clientside-effect": true,
|
||||
"react-focus-lock>use-callback-ref": true,
|
||||
"react-focus-lock>use-sidecar": true
|
||||
}
|
||||
},
|
||||
"react-focus-lock>focus-lock": {
|
||||
"globals": {
|
||||
"HTMLIFrameElement": true,
|
||||
"Node.DOCUMENT_FRAGMENT_NODE": true,
|
||||
"Node.DOCUMENT_NODE": true,
|
||||
"Node.DOCUMENT_POSITION_CONTAINED_BY": true,
|
||||
"Node.DOCUMENT_POSITION_CONTAINS": true,
|
||||
"Node.ELEMENT_NODE": true,
|
||||
"console.error": true,
|
||||
"console.warn": true,
|
||||
"document": true,
|
||||
"getComputedStyle": true,
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"wait-on>rxjs>tslib": true
|
||||
}
|
||||
},
|
||||
"react-focus-lock>react-clientside-effect": {
|
||||
"packages": {
|
||||
"react": true,
|
||||
"react-focus-lock>react-clientside-effect>@babel/runtime": true
|
||||
}
|
||||
},
|
||||
"react-focus-lock>use-callback-ref": {
|
||||
"packages": {
|
||||
"react": true
|
||||
}
|
||||
},
|
||||
"react-focus-lock>use-sidecar": {
|
||||
"globals": {
|
||||
"console.error": true
|
||||
},
|
||||
"packages": {
|
||||
"react": true,
|
||||
"react-focus-lock>use-sidecar>detect-node-es": true,
|
||||
"wait-on>rxjs>tslib": true
|
||||
}
|
||||
},
|
||||
"react-idle-timer": {
|
||||
"globals": {
|
||||
"clearTimeout": true,
|
||||
|
@ -3910,6 +3910,64 @@
|
||||
"setTimeout": true
|
||||
}
|
||||
},
|
||||
"react-focus-lock": {
|
||||
"globals": {
|
||||
"addEventListener": true,
|
||||
"console.error": true,
|
||||
"console.warn": true,
|
||||
"document": true,
|
||||
"removeEventListener": true,
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"@babel/runtime": true,
|
||||
"prop-types": true,
|
||||
"react": true,
|
||||
"react-focus-lock>focus-lock": true,
|
||||
"react-focus-lock>react-clientside-effect": true,
|
||||
"react-focus-lock>use-callback-ref": true,
|
||||
"react-focus-lock>use-sidecar": true
|
||||
}
|
||||
},
|
||||
"react-focus-lock>focus-lock": {
|
||||
"globals": {
|
||||
"HTMLIFrameElement": true,
|
||||
"Node.DOCUMENT_FRAGMENT_NODE": true,
|
||||
"Node.DOCUMENT_NODE": true,
|
||||
"Node.DOCUMENT_POSITION_CONTAINED_BY": true,
|
||||
"Node.DOCUMENT_POSITION_CONTAINS": true,
|
||||
"Node.ELEMENT_NODE": true,
|
||||
"console.error": true,
|
||||
"console.warn": true,
|
||||
"document": true,
|
||||
"getComputedStyle": true,
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"wait-on>rxjs>tslib": true
|
||||
}
|
||||
},
|
||||
"react-focus-lock>react-clientside-effect": {
|
||||
"packages": {
|
||||
"react": true,
|
||||
"react-focus-lock>react-clientside-effect>@babel/runtime": true
|
||||
}
|
||||
},
|
||||
"react-focus-lock>use-callback-ref": {
|
||||
"packages": {
|
||||
"react": true
|
||||
}
|
||||
},
|
||||
"react-focus-lock>use-sidecar": {
|
||||
"globals": {
|
||||
"console.error": true
|
||||
},
|
||||
"packages": {
|
||||
"react": true,
|
||||
"react-focus-lock>use-sidecar>detect-node-es": true,
|
||||
"wait-on>rxjs>tslib": true
|
||||
}
|
||||
},
|
||||
"react-idle-timer": {
|
||||
"globals": {
|
||||
"clearTimeout": true,
|
||||
|
@ -7362,7 +7362,7 @@
|
||||
},
|
||||
"stylelint>postcss-html>htmlparser2>domutils>dom-serializer": {
|
||||
"packages": {
|
||||
"stylelint>postcss-html>htmlparser2>domutils>dom-serializer>domelementtype": true,
|
||||
"stylelint>postcss-html>htmlparser2>domelementtype": true,
|
||||
"stylelint>postcss-html>htmlparser2>entities": true
|
||||
}
|
||||
},
|
||||
|
@ -345,6 +345,7 @@
|
||||
"qrcode.react": "^1.0.1",
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-focus-lock": "^2.9.4",
|
||||
"react-idle-timer": "^4.2.5",
|
||||
"react-inspector": "^2.3.0",
|
||||
"react-markdown": "^6.0.3",
|
||||
|
@ -34,6 +34,7 @@ export { TextField, TEXT_FIELD_TYPES, TEXT_FIELD_SIZES } from './text-field';
|
||||
export { TextFieldSearch } from './text-field-search';
|
||||
export { ModalContent, ModalContentSize } from './modal-content';
|
||||
export { ModalOverlay } from './modal-overlay';
|
||||
export { ModalFocus } from './modal-focus';
|
||||
|
||||
// Molecules
|
||||
export { BannerBase } from './banner-base';
|
||||
|
248
ui/components/component-library/modal-focus/README.mdx
Normal file
248
ui/components/component-library/modal-focus/README.mdx
Normal file
@ -0,0 +1,248 @@
|
||||
import { Story, Canvas, ArgsTable } from '@storybook/addon-docs';
|
||||
|
||||
import { ModalFocus } from './modal-focus';
|
||||
|
||||
# ModalFocus
|
||||
|
||||
`ModalFocus` traps the focus within the modal. This greatly improves the experience for screen readers and keyboard only users.
|
||||
|
||||
<Canvas>
|
||||
<Story id="components-componentlibrary-modalfocus--default-story" />
|
||||
</Canvas>
|
||||
|
||||
## Props
|
||||
|
||||
The `ModalFocus` is built with [react-focus-lock](https://github.com/theKashey/react-focus-lock) and accepts all of the props from that library.
|
||||
|
||||
<ArgsTable of={ModalFocus} />
|
||||
|
||||
### Children
|
||||
|
||||
Use the `children` prop to render the component to lock focus to
|
||||
|
||||
<Canvas>
|
||||
<Story id="components-componentlibrary-modalfocus--children" />
|
||||
</Canvas>
|
||||
|
||||
```jsx
|
||||
import React from 'react';
|
||||
import {
|
||||
BorderColor,
|
||||
DISPLAY,
|
||||
FLEX_DIRECTION,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
|
||||
import Box from '../../ui/box';
|
||||
|
||||
import { ModalFocus } from '../../component-library';
|
||||
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
<button onClick={() => setOpen(true)} marginBottom={4}>
|
||||
Open
|
||||
</button>;
|
||||
|
||||
{
|
||||
open && (
|
||||
<ModalFocus>
|
||||
<Box
|
||||
padding={4}
|
||||
borderColor={BorderColor.borderDefault}
|
||||
display={DISPLAY.FLEX}
|
||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||
gap={4}
|
||||
>
|
||||
<p>Modal focus children</p>
|
||||
<input />
|
||||
<p>
|
||||
Use the keyboard to try tabbing around you will notice that the focus
|
||||
is locked to the content within modal focus
|
||||
</p>
|
||||
<button onClick={() => setOpen(false)}>Close</button>
|
||||
</Box>
|
||||
</ModalFocus>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Initial Focus Ref
|
||||
|
||||
Use the `initialFocusRef` to pass the `ref` of the element to receive focus initially
|
||||
|
||||
<Canvas>
|
||||
<Story id="components-componentlibrary-modalfocus--initial-focus-ref" />
|
||||
</Canvas>
|
||||
|
||||
```jsx
|
||||
import React from 'react';
|
||||
import {
|
||||
BorderColor,
|
||||
DISPLAY,
|
||||
FLEX_DIRECTION,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
|
||||
import Box from '../../ui/box';
|
||||
|
||||
import { ModalFocus } from '../../component-library';
|
||||
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const ref = React.useRef < HTMLButtonElement > null;
|
||||
|
||||
<button onClick={() => setOpen(true)} marginBottom={4}>
|
||||
Open
|
||||
</button>;
|
||||
|
||||
{
|
||||
open && (
|
||||
<ModalFocus initialFocusRef={ref}>
|
||||
<Box
|
||||
padding={4}
|
||||
borderColor={BorderColor.borderDefault}
|
||||
display={DISPLAY.FLEX}
|
||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||
gap={4}
|
||||
>
|
||||
<input />
|
||||
<p>Initial focus is on the close button</p>
|
||||
<button ref={ref} onClick={() => setOpen(false)}>
|
||||
Close
|
||||
</button>
|
||||
</Box>
|
||||
</ModalFocus>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Final Focus Ref
|
||||
|
||||
Use the `finalFocusRef` to pass the `ref` of the element to return focus to when `ModalFocus` unmounts
|
||||
|
||||
<Canvas>
|
||||
<Story id="components-componentlibrary-modalfocus--final-focus-ref" />
|
||||
</Canvas>
|
||||
|
||||
```jsx
|
||||
import React from 'react';
|
||||
import {
|
||||
BorderColor,
|
||||
DISPLAY,
|
||||
FLEX_DIRECTION,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
|
||||
import Box from '../../ui/box';
|
||||
|
||||
import { ModalFocus } from '../../component-library';
|
||||
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const ref = React.useRef < HTMLInputElement > null;
|
||||
|
||||
<button onClick={() => setOpen(true)} marginBottom={4}>
|
||||
Open
|
||||
</button>;
|
||||
<input placeholder="Focus will return here" ref={ref} />;
|
||||
|
||||
{
|
||||
open && (
|
||||
<ModalFocus finalFocusRef={ref}>
|
||||
<Box
|
||||
padding={4}
|
||||
borderColor={BorderColor.borderDefault}
|
||||
display={DISPLAY.FLEX}
|
||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||
gap={4}
|
||||
>
|
||||
<p>Focus will be returned to the input once closed</p>
|
||||
<button onClick={() => setOpen(false)}>Close</button>
|
||||
</Box>
|
||||
</ModalFocus>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Restore Focus
|
||||
|
||||
Use the `restoreFocus` to restore focus to the element that triggered the `ModalFocus` once it unmounts
|
||||
|
||||
<Canvas>
|
||||
<Story id="components-componentlibrary-modalfocus--restore-focus" />
|
||||
</Canvas>
|
||||
|
||||
```jsx
|
||||
import React from 'react';
|
||||
import {
|
||||
BorderColor,
|
||||
DISPLAY,
|
||||
FLEX_DIRECTION,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
|
||||
import Box from '../../ui/box';
|
||||
|
||||
import { ModalFocus } from '../../component-library';
|
||||
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
<button onClick={() => setOpen(true)} marginBottom={4}>
|
||||
Open
|
||||
</button>;
|
||||
{
|
||||
open && (
|
||||
<ModalFocus restoreFocus>
|
||||
<Box
|
||||
padding={4}
|
||||
borderColor={BorderColor.borderDefault}
|
||||
display={DISPLAY.FLEX}
|
||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||
gap={4}
|
||||
>
|
||||
<p>Focus will be restored to the open button once closed</p>
|
||||
<button onClick={() => setOpen(false)}>Close</button>
|
||||
</Box>
|
||||
</ModalFocus>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Auto Focus
|
||||
|
||||
Use the `autoFocus` to auto focus to the first focusable element within the `ModalFocus` once it mounts
|
||||
|
||||
Defaults to `true`
|
||||
|
||||
<Canvas>
|
||||
<Story id="components-componentlibrary-modalfocus--auto-focus" />
|
||||
</Canvas>
|
||||
|
||||
```jsx
|
||||
import React from 'react';
|
||||
import {
|
||||
BorderColor,
|
||||
DISPLAY,
|
||||
FLEX_DIRECTION,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
|
||||
import Box from '../../ui/box';
|
||||
|
||||
import { ModalFocus } from '../../component-library';
|
||||
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
<button onClick={() => setOpen(true)} marginBottom={4}>
|
||||
Open
|
||||
</button>;
|
||||
{
|
||||
open && (
|
||||
<ModalFocus autoFocus={false}>
|
||||
<Box
|
||||
padding={4}
|
||||
borderColor={BorderColor.borderDefault}
|
||||
display={DISPLAY.FLEX}
|
||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||
gap={4}
|
||||
>
|
||||
<p>auto focus set to false</p>
|
||||
<button onClick={() => setOpen(false)}>Close</button>
|
||||
</Box>
|
||||
</ModalFocus>
|
||||
);
|
||||
}
|
||||
```
|
2
ui/components/component-library/modal-focus/index.ts
Normal file
2
ui/components/component-library/modal-focus/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { ModalFocus } from './modal-focus';
|
||||
export type { ModalFocusProps } from './modal-focus.types';
|
@ -0,0 +1,158 @@
|
||||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
|
||||
import Box from '../../ui/box';
|
||||
|
||||
import {
|
||||
BorderColor,
|
||||
DISPLAY,
|
||||
FLEX_DIRECTION,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
|
||||
import { ModalFocus } from './modal-focus';
|
||||
import README from './README.mdx';
|
||||
|
||||
export default {
|
||||
title: 'Components/ComponentLibrary/ModalFocus',
|
||||
component: ModalFocus,
|
||||
parameters: {
|
||||
docs: {
|
||||
page: README,
|
||||
},
|
||||
},
|
||||
args: {
|
||||
children: (
|
||||
<>
|
||||
<p>Modal focus children</p>
|
||||
<input />
|
||||
<p>
|
||||
Use the keyboard to try tabbing around you will notice that the focus
|
||||
is locked to the content within modal focus
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
} as ComponentMeta<typeof ModalFocus>;
|
||||
|
||||
const Template: ComponentStory<typeof ModalFocus> = (args) => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => setOpen(true)}>Open</button>
|
||||
{open && (
|
||||
<ModalFocus {...args}>
|
||||
<Box
|
||||
padding={4}
|
||||
borderColor={BorderColor.borderDefault}
|
||||
display={DISPLAY.FLEX}
|
||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||
gap={4}
|
||||
>
|
||||
{args.children}
|
||||
<button onClick={() => setOpen(false)}>Close</button>
|
||||
</Box>
|
||||
</ModalFocus>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const DefaultStory = Template.bind({});
|
||||
DefaultStory.storyName = 'Default';
|
||||
|
||||
export const Children = Template.bind({});
|
||||
Children.args = {
|
||||
children: (
|
||||
<>
|
||||
<p>Modal focus children</p>
|
||||
</>
|
||||
),
|
||||
};
|
||||
export const InitialFocusRef = (args) => {
|
||||
const ref = React.useRef<HTMLButtonElement>(null);
|
||||
const [open, setOpen] = React.useState(false);
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => setOpen(true)}>Open</button>
|
||||
{open && (
|
||||
<ModalFocus {...args} initialFocusRef={ref}>
|
||||
<Box
|
||||
padding={4}
|
||||
borderColor={BorderColor.borderDefault}
|
||||
display={DISPLAY.FLEX}
|
||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||
gap={4}
|
||||
>
|
||||
<input />
|
||||
{args.children}
|
||||
<button ref={ref} onClick={() => setOpen(false)}>
|
||||
Close
|
||||
</button>
|
||||
</Box>
|
||||
</ModalFocus>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
InitialFocusRef.args = {
|
||||
children: <p>Initial focus is on the close button</p>,
|
||||
};
|
||||
|
||||
export const FinalFocusRef = (args) => {
|
||||
const ref = React.useRef<HTMLInputElement>(null);
|
||||
const [open, setOpen] = React.useState(false);
|
||||
return (
|
||||
<>
|
||||
<Box display={DISPLAY.FLEX} gap={4}>
|
||||
<button onClick={() => setOpen(true)}>Open</button>
|
||||
<input placeholder="Focus will return here" ref={ref} />
|
||||
</Box>
|
||||
{open && (
|
||||
<ModalFocus {...args} finalFocusRef={ref}>
|
||||
<Box
|
||||
padding={4}
|
||||
borderColor={BorderColor.borderDefault}
|
||||
display={DISPLAY.FLEX}
|
||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||
gap={4}
|
||||
>
|
||||
<p>Focus will be returned to the input once closed</p>
|
||||
<button onClick={() => setOpen(false)}>Close</button>
|
||||
</Box>
|
||||
</ModalFocus>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const RestoreFocus = (args) => {
|
||||
const ref = React.useRef<HTMLButtonElement>(null);
|
||||
const [open, setOpen] = React.useState(false);
|
||||
return (
|
||||
<>
|
||||
<button ref={ref} onClick={() => setOpen(!open)}>
|
||||
Open
|
||||
</button>
|
||||
{open && (
|
||||
<ModalFocus {...args} restoreFocus>
|
||||
<Box
|
||||
padding={4}
|
||||
borderColor={BorderColor.borderDefault}
|
||||
display={DISPLAY.FLEX}
|
||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||
gap={4}
|
||||
>
|
||||
<p>Focus will be restored to the open button once closed</p>
|
||||
<button onClick={() => setOpen(false)}>Close</button>
|
||||
</Box>
|
||||
</ModalFocus>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const AutoFocus = Template.bind({});
|
||||
AutoFocus.args = {
|
||||
autoFocus: false,
|
||||
children: <p>auto focus set to false</p>,
|
||||
};
|
@ -0,0 +1,61 @@
|
||||
/* eslint-disable jest/require-top-level-describe */
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { ModalFocus } from './modal-focus';
|
||||
|
||||
describe('ModalFocus', () => {
|
||||
it('should render with children inside the ModalFocus', () => {
|
||||
const { getByText } = render(
|
||||
<ModalFocus>
|
||||
<div>modal focus</div>
|
||||
</ModalFocus>,
|
||||
);
|
||||
expect(getByText('modal focus')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should render with the initial element focused', () => {
|
||||
const { getByTestId } = render(
|
||||
<ModalFocus>
|
||||
<input data-testid="input" />
|
||||
</ModalFocus>,
|
||||
);
|
||||
expect(getByTestId('input')).toHaveFocus();
|
||||
});
|
||||
|
||||
it('should render with focused with autoFocus is set to false', () => {
|
||||
const { getByTestId } = render(
|
||||
<ModalFocus autoFocus={false}>
|
||||
<input data-testid="input" />
|
||||
</ModalFocus>,
|
||||
);
|
||||
expect(getByTestId('input')).not.toHaveFocus();
|
||||
});
|
||||
|
||||
it('should focus initialFocusRef on render', () => {
|
||||
const ref: React.RefObject<HTMLInputElement> = React.createRef();
|
||||
const { getByTestId } = render(
|
||||
<ModalFocus initialFocusRef={ref}>
|
||||
<input />
|
||||
<input />
|
||||
<input data-testid="input" ref={ref} />
|
||||
</ModalFocus>,
|
||||
);
|
||||
expect(getByTestId('input')).toHaveFocus();
|
||||
});
|
||||
|
||||
it('should focus final focus ref when closed', () => {
|
||||
const finalRef: React.RefObject<HTMLButtonElement> = React.createRef();
|
||||
const { rerender, getByRole } = render(
|
||||
<>
|
||||
<button ref={finalRef}>button</button>
|
||||
<ModalFocus finalFocusRef={finalRef}>
|
||||
<div>modal focus</div>
|
||||
</ModalFocus>
|
||||
</>,
|
||||
);
|
||||
expect(finalRef.current).not.toHaveFocus();
|
||||
rerender(<button ref={finalRef}>button</button>);
|
||||
expect(getByRole('button')).toHaveFocus();
|
||||
});
|
||||
});
|
46
ui/components/component-library/modal-focus/modal-focus.tsx
Normal file
46
ui/components/component-library/modal-focus/modal-focus.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import ReactFocusLock from 'react-focus-lock';
|
||||
import type { ModalFocusProps } from './modal-focus.types';
|
||||
|
||||
/**
|
||||
* Based on the ModalFocusScope component from chakra-ui
|
||||
* https://github.com/chakra-ui/chakra-ui/blob/main/packages/components/modal/src/modal-focus.tsx
|
||||
*/
|
||||
|
||||
const FocusTrap: typeof ReactFocusLock =
|
||||
(ReactFocusLock as any).default ?? ReactFocusLock;
|
||||
|
||||
export const ModalFocus: React.FC<ModalFocusProps> = ({
|
||||
initialFocusRef,
|
||||
finalFocusRef,
|
||||
restoreFocus,
|
||||
children,
|
||||
autoFocus,
|
||||
...props
|
||||
}) => {
|
||||
const onActivation = useCallback(() => {
|
||||
if (initialFocusRef?.current) {
|
||||
initialFocusRef.current.focus();
|
||||
}
|
||||
}, [initialFocusRef]);
|
||||
|
||||
const onDeactivation = useCallback(() => {
|
||||
finalFocusRef?.current?.focus();
|
||||
}, [finalFocusRef]);
|
||||
|
||||
const returnFocus = restoreFocus && !finalFocusRef;
|
||||
|
||||
return (
|
||||
<FocusTrap
|
||||
autoFocus={autoFocus}
|
||||
onActivation={onActivation}
|
||||
onDeactivation={onDeactivation}
|
||||
returnFocus={returnFocus}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</FocusTrap>
|
||||
);
|
||||
};
|
||||
|
||||
ModalFocus.displayName = 'ModalFocus';
|
@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
|
||||
export interface FocusableElement {
|
||||
focus(options?: FocusOptions): void;
|
||||
}
|
||||
|
||||
export interface ModalFocusProps {
|
||||
/**
|
||||
* The `ref` of the element to receive focus initially
|
||||
*/
|
||||
initialFocusRef?: React.RefObject<FocusableElement>;
|
||||
/**
|
||||
* The `ref` of the element to return focus to when `ModalFocus`
|
||||
* unmounts
|
||||
*/
|
||||
finalFocusRef?: React.RefObject<FocusableElement>;
|
||||
/**
|
||||
* If `true`, focus will be restored to the element that
|
||||
* triggered the `ModalFocus` once it unmounts
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
restoreFocus?: boolean;
|
||||
/**
|
||||
* The node to lock focus to
|
||||
*/
|
||||
children: React.ReactNode;
|
||||
/**
|
||||
* If `true`, the first focusable element within the `children`
|
||||
* will auto-focused once `ModalFocus` mounts
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
autoFocus?: boolean;
|
||||
}
|
90
yarn.lock
90
yarn.lock
@ -1627,6 +1627,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/runtime@npm:^7.12.13":
|
||||
version: 7.21.0
|
||||
resolution: "@babel/runtime@npm:7.21.0"
|
||||
dependencies:
|
||||
regenerator-runtime: ^0.13.11
|
||||
checksum: 7b33e25bfa9e0e1b9e8828bb61b2d32bdd46b41b07ba7cb43319ad08efc6fda8eb89445193e67d6541814627df0ca59122c0ea795e412b99c5183a0540d338ab
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/runtime@patch:@babel/runtime@npm%3A7.18.9#./.yarn/patches/@babel-runtime-npm-7.18.9-28ca6b5f61.patch::locator=metamask-crx%40workspace%3A.":
|
||||
version: 7.18.9
|
||||
resolution: "@babel/runtime@patch:@babel/runtime@npm%3A7.18.9#./.yarn/patches/@babel-runtime-npm-7.18.9-28ca6b5f61.patch::version=7.18.9&hash=918bda&locator=metamask-crx%40workspace%3A."
|
||||
@ -13947,6 +13956,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"detect-node-es@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "detect-node-es@npm:1.1.0"
|
||||
checksum: e46307d7264644975b71c104b9f028ed1d3d34b83a15b8a22373640ce5ea630e5640b1078b8ea15f202b54641da71e4aa7597093bd4b91f113db520a26a37449
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"detect-node@npm:^2.0.4":
|
||||
version: 2.0.4
|
||||
resolution: "detect-node@npm:2.0.4"
|
||||
@ -17201,6 +17217,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"focus-lock@npm:^0.11.6":
|
||||
version: 0.11.6
|
||||
resolution: "focus-lock@npm:0.11.6"
|
||||
dependencies:
|
||||
tslib: ^2.0.3
|
||||
checksum: 6a407c4c45f05f8258f92565541fc5f8043f576643a7603eb999e1a790173e08712056766ed034ccd31c6d6deed259dea558002712fa5ef2432fc6930b9c7a05
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"follow-redirects@npm:^1.14.0, follow-redirects@npm:^1.14.9, follow-redirects@npm:^1.15.0":
|
||||
version: 1.15.2
|
||||
resolution: "follow-redirects@npm:1.15.2"
|
||||
@ -24282,6 +24307,7 @@ __metadata:
|
||||
react: ^16.12.0
|
||||
react-devtools: ^4.11.0
|
||||
react-dom: ^16.12.0
|
||||
react-focus-lock: ^2.9.4
|
||||
react-idle-timer: ^4.2.5
|
||||
react-inspector: ^2.3.0
|
||||
react-markdown: ^6.0.3
|
||||
@ -28298,6 +28324,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-clientside-effect@npm:^1.2.6":
|
||||
version: 1.2.6
|
||||
resolution: "react-clientside-effect@npm:1.2.6"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.12.13
|
||||
peerDependencies:
|
||||
react: ^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||
checksum: 7db6110027a51458b1a46109d2b63dd822825f483c71afef7c0c0a671f3b1aa155049dbd8651c9d536ffac83601f8823b7c3f8916b4f4ee5c3cb7647a85cce4e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-colorful@npm:^5.1.2":
|
||||
version: 5.3.0
|
||||
resolution: "react-colorful@npm:5.3.0"
|
||||
@ -28416,6 +28453,26 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-focus-lock@npm:^2.9.4":
|
||||
version: 2.9.4
|
||||
resolution: "react-focus-lock@npm:2.9.4"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.0.0
|
||||
focus-lock: ^0.11.6
|
||||
prop-types: ^15.6.2
|
||||
react-clientside-effect: ^1.2.6
|
||||
use-callback-ref: ^1.3.0
|
||||
use-sidecar: ^1.1.2
|
||||
peerDependencies:
|
||||
"@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
checksum: f4c696bdbde5008560388622b994c00502d1faeeabff32b02964770c8c020208872f5f6b914b249a8bf3e97cc12e58bb0d227cd33460093654156b7b7f4c8d76
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-idle-timer@npm:^4.2.5":
|
||||
version: 4.2.5
|
||||
resolution: "react-idle-timer@npm:4.2.5"
|
||||
@ -29058,7 +29115,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"regenerator-runtime@npm:^0.13.9":
|
||||
"regenerator-runtime@npm:^0.13.11, regenerator-runtime@npm:^0.13.9":
|
||||
version: 0.13.11
|
||||
resolution: "regenerator-runtime@npm:0.13.11"
|
||||
checksum: 27481628d22a1c4e3ff551096a683b424242a216fee44685467307f14d58020af1e19660bf2e26064de946bad7eff28950eae9f8209d55723e2d9351e632bbb4
|
||||
@ -33779,6 +33836,37 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"use-callback-ref@npm:^1.3.0":
|
||||
version: 1.3.0
|
||||
resolution: "use-callback-ref@npm:1.3.0"
|
||||
dependencies:
|
||||
tslib: ^2.0.0
|
||||
peerDependencies:
|
||||
"@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
checksum: 7913df383a5a6fcb399212eedefaac2e0c6f843555202d4e3010bac3848afe38ecaa3d0d6500ad1d936fbeffd637e6c517e68edb024af5e6beca7f27f3ce7b21
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"use-sidecar@npm:^1.1.2":
|
||||
version: 1.1.2
|
||||
resolution: "use-sidecar@npm:1.1.2"
|
||||
dependencies:
|
||||
detect-node-es: ^1.1.0
|
||||
tslib: ^2.0.0
|
||||
peerDependencies:
|
||||
"@types/react": ^16.9.0 || ^17.0.0 || ^18.0.0
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
checksum: 925d1922f9853e516eaad526b6fed1be38008073067274f0ecc3f56b17bb8ab63480140dd7c271f94150027c996cea4efe83d3e3525e8f3eda22055f6a39220b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"use@npm:^3.1.0":
|
||||
version: 3.1.0
|
||||
resolution: "use@npm:3.1.0"
|
||||
|
Loading…
Reference in New Issue
Block a user