Skip to content

Commit 52c664f

Browse files
committed
feat(Modal): add triggerRef prop to restore focus after close
This accepts a ref to the element that triggers the modal. This allows to restore the focus on the trigger element after closing the modal
1 parent 45ce1e6 commit 52c664f

File tree

4 files changed

+39
-16
lines changed

4 files changed

+39
-16
lines changed

packages/orbit-components/src/Modal/Modal.stories.tsx

+29-16
Original file line numberDiff line numberDiff line change
@@ -212,26 +212,31 @@ export default meta;
212212

213213
function useModal() {
214214
const [open, toggle] = React.useState(true);
215+
const ref = React.useRef(null);
216+
215217
return {
216218
Container: ({ children }) => (
217219
<>
218220
{open && children}
219-
<Button onClick={() => toggle(true)}>Open</Button>
221+
<Button ref={ref} onClick={() => toggle(true)}>
222+
Open
223+
</Button>
220224
</>
221225
),
222226
onClose: () => {
223227
toggle(false);
224228
action("onClose")();
225229
},
230+
triggerRef: ref,
226231
};
227232
}
228233

229234
export const RemovableSections: Story = {
230235
render: ({ showSection, children }) => {
231-
const { Container, onClose } = useModal();
236+
const { Container, onClose, triggerRef } = useModal();
232237
return (
233238
<Container>
234-
<Modal onClose={onClose}>
239+
<Modal triggerRef={triggerRef} onClose={onClose}>
235240
<ModalHeader
236241
title="Enjoy something to eat while you fly"
237242
illustration={<Illustration name="Meal" size="small" />}
@@ -277,17 +282,18 @@ export const RemovableSections: Story = {
277282
"labelClose",
278283
"lockScrolling",
279284
"fixedFooter",
285+
"triggerRef",
280286
],
281287
},
282288
},
283289
};
284290

285291
export const WithFixedFooter: Story = {
286292
render: args => {
287-
const { Container, onClose } = useModal();
293+
const { Container, onClose, triggerRef } = useModal();
288294
return (
289295
<Container>
290-
<Modal onClose={onClose} {...args}>
296+
<Modal triggerRef={triggerRef} onClose={onClose} {...args}>
291297
<ModalHeader
292298
title="Enjoy something to eat while you fly"
293299
illustration={<Illustration name="BaggageDrop" size="small" />}
@@ -323,6 +329,7 @@ export const WithFixedFooter: Story = {
323329
"hasCloseButton",
324330
"disableAnimation",
325331
"labelClose",
332+
"triggerRef",
326333
"lockScrolling",
327334
],
328335
},
@@ -331,11 +338,11 @@ export const WithFixedFooter: Story = {
331338

332339
export const WithForm: Story = {
333340
render: ({ showSection }) => {
334-
const { Container, onClose } = useModal();
341+
const { Container, onClose, triggerRef } = useModal();
335342

336343
return (
337344
<Container>
338-
<Modal onClose={onClose} fixedFooter>
345+
<Modal triggerRef={triggerRef} onClose={onClose} fixedFooter>
339346
<ModalHeader title="Refund" description="Reservation number: 123456789" />
340347
<ModalSection>
341348
<Stack>
@@ -402,6 +409,7 @@ export const WithForm: Story = {
402409
"hasCloseButton",
403410
"disableAnimation",
404411
"labelClose",
412+
"triggerRef",
405413
"lockScrolling",
406414
"fixedFooter",
407415
],
@@ -411,11 +419,15 @@ export const WithForm: Story = {
411419

412420
export const WithItinerary: Story = {
413421
render: () => {
414-
const { Container, onClose } = useModal();
422+
const { Container, onClose, triggerRef } = useModal();
415423

416424
return (
417425
<Container>
418-
<Modal ariaLabel="Itinerary from Prague to Frankfurt" onClose={onClose}>
426+
<Modal
427+
triggerRef={triggerRef}
428+
ariaLabel="Itinerary from Prague to Frankfurt"
429+
onClose={onClose}
430+
>
419431
<ModalSection>
420432
<Itinerary>
421433
<ItineraryStatus type="success" label="This part is new">
@@ -457,10 +469,10 @@ export const WithItinerary: Story = {
457469

458470
export const WithModalHeaderOnly: Story = {
459471
render: args => {
460-
const { Container, onClose } = useModal();
472+
const { Container, onClose, triggerRef } = useModal();
461473
return (
462474
<Container>
463-
<Modal onClose={onClose} {...args}>
475+
<Modal triggerRef={triggerRef} onClose={onClose} {...args}>
464476
<ModalHeader
465477
title="Enjoy something to eat while you fly"
466478
illustration={<Illustration name="BaggageDrop" size="small" />}
@@ -488,6 +500,7 @@ export const WithModalHeaderOnly: Story = {
488500
"hasCloseButton",
489501
"disableAnimation",
490502
"labelClose",
503+
"triggerRef",
491504
"lockScrolling",
492505
],
493506
},
@@ -508,11 +521,11 @@ export const Playground: StoryObj<PlaygroundStoryProps> = {
508521
showSection,
509522
...args
510523
}) => {
511-
const { Container, onClose } = useModal();
524+
const { Container, onClose, triggerRef } = useModal();
512525

513526
return (
514527
<Container>
515-
<Modal onClose={onClose} {...args}>
528+
<Modal triggerRef={triggerRef} onClose={onClose} {...args}>
516529
{header && (
517530
<ModalHeader
518531
title={title}
@@ -588,18 +601,18 @@ export const Playground: StoryObj<PlaygroundStoryProps> = {
588601
parameters: {
589602
info: "Playground of Modal component. Check Orbit.Kiwi for more detailed design guidelines.",
590603
controls: {
591-
exclude: ["children"],
604+
exclude: ["children", "triggerRef"],
592605
},
593606
},
594607
};
595608

596609
export const Rtl: Story = {
597610
render: () => {
598-
const { Container, onClose } = useModal();
611+
const { Container, onClose, triggerRef } = useModal();
599612
return (
600613
<Container>
601614
<RenderInRtl>
602-
<Modal onClose={onClose}>
615+
<Modal triggerRef={triggerRef} onClose={onClose}>
603616
<ModalHeader
604617
title="The title of the ModalHeader"
605618
illustration={<Illustration name="Accommodation" size="small" />}

packages/orbit-components/src/Modal/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Table below contains all types of the props available in the Modal component.
2525
| Name | Type | Default | Description |
2626
| :------------------ | :------------------------- | :--------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2727
| children | `React.Node` | | The content of the Modal. [See Subcomponents](#subcomponents) |
28+
| triggerRef | `React.RefObject` | | The ref to the element which triggers the Modal. |
2829
| lockScrolling | `boolean` | `true` | Whether to prevent scrolling of the rest of the page while Modal is open. This is on by default to provide a better user experience. |
2930
| scrollingElementRef | ref (object or function) | | The scrolling element, which depends on the viewport. |
3031
| dataTest | `string` | | Optional prop for testing purposes. |

packages/orbit-components/src/Modal/index.tsx

+8
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ const Modal = React.forwardRef<Instance, Props>(
4848
scrollingElementRef,
4949
children,
5050
onClose,
51+
triggerRef,
5152
autoFocus = true,
5253
fixedFooter = false,
5354
isMobileFullPage = false,
@@ -354,6 +355,13 @@ const Modal = React.forwardRef<Instance, Props>(
354355
}
355356
}, [children, prevChildren]);
356357

358+
React.useEffect(() => {
359+
return () => {
360+
// eslint-disable-next-line react-hooks/exhaustive-deps
361+
triggerRef?.current?.focus();
362+
};
363+
}, [triggerRef]);
364+
357365
const hasCloseContainer = mobileHeader && (hasModalTitle || (onClose && hasCloseButton));
358366

359367
const value = React.useMemo(

packages/orbit-components/src/Modal/types.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ type Size = "extraSmall" | "small" | "normal" | "large" | "extraLarge";
1010
export interface Props extends Common.Globals {
1111
readonly size?: Size;
1212
readonly children: React.ReactNode;
13+
readonly triggerRef?: React.RefObject<HTMLElement>;
1314
readonly lockScrolling?: boolean;
1415
readonly scrollingElementRef?: React.Ref<HTMLElement>;
1516
readonly autoFocus?: boolean;

0 commit comments

Comments
 (0)