Skip to content

Commit 084e83f

Browse files
committed
fix(RelativeRangeDatePicker): correctly close picker popup inside dialog.
Closes #115
1 parent fcab830 commit 084e83f

File tree

6 files changed

+101
-5
lines changed

6 files changed

+101
-5
lines changed

src/components/RelativeDatePicker/RelativeDatePicker.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
Clock as ClockIcon,
88
Function as FunctionIcon,
99
} from '@gravity-ui/icons';
10-
import {Button, Icon, Popup, TextInput, useMobile} from '@gravity-ui/uikit';
10+
import {Button, Icon, Popup, TextInput, useForkRef, useMobile} from '@gravity-ui/uikit';
1111

1212
import {block} from '../../utils/cn';
1313
import {Calendar} from '../Calendar';
@@ -60,12 +60,13 @@ export function RelativeDatePicker(props: RelativeDatePickerProps) {
6060
} = useRelativeDatePickerProps(state, props);
6161

6262
const anchorRef = React.useRef<HTMLDivElement>(null);
63+
const handleRef = useForkRef(anchorRef, groupProps.ref);
6364

6465
const isMobile = useMobile();
6566
const isOnlyTime = state.datePickerState.hasTime && !state.datePickerState.hasDate;
6667

6768
return (
68-
<div ref={anchorRef} className={b(null, props.className)} {...groupProps}>
69+
<div {...groupProps} ref={handleRef} className={b(null, props.className)}>
6970
{isMobile && state.mode === 'absolute' && (
7071
<MobileCalendar
7172
state={state.datePickerState}

src/components/RelativeDatePicker/__stories__/RelativeDatePicker.stories.tsx

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react';
22

33
import {dateTimeParse} from '@gravity-ui/date-utils';
4-
import {Tabs, useControlledState} from '@gravity-ui/uikit';
4+
import {Button, Dialog, Tabs, useControlledState} from '@gravity-ui/uikit';
55
import {toaster} from '@gravity-ui/uikit/toaster-singleton-react-18';
66
import {action} from '@storybook/addon-actions';
77
import type {Meta, StoryObj} from '@storybook/react';
@@ -10,6 +10,7 @@ import {timeZoneControl} from '../../../demo/utils/zones';
1010
import {Calendar} from '../../Calendar';
1111
import {constrainValue} from '../../CalendarView/utils';
1212
import {RelativeDatePicker} from '../RelativeDatePicker';
13+
import type {RelativeDatePickerProps} from '../RelativeDatePicker';
1314
import type {Value} from '../hooks/useRelativeDatePickerState';
1415

1516
const meta: Meta<typeof RelativeDatePicker> = {
@@ -179,3 +180,39 @@ export const WithCustomCalendar = {
179180
});
180181
},
181182
} satisfies Story;
183+
184+
export const InsideDialog: StoryObj<RelativeDatePickerProps & {disableDialogFocusTrap?: boolean}> =
185+
{
186+
...Default,
187+
render: function InsideDialog(args) {
188+
const [isOpen, setOpen] = React.useState(false);
189+
return (
190+
<React.Fragment>
191+
<Button
192+
onClick={() => {
193+
setOpen(true);
194+
}}
195+
>
196+
Open dialog
197+
</Button>
198+
<Dialog
199+
open={isOpen}
200+
onClose={() => setOpen(false)}
201+
disableFocusTrap={args.disableDialogFocusTrap}
202+
>
203+
<Dialog.Header />
204+
<Dialog.Body>
205+
<div style={{paddingTop: 16}}>{Default.render(args)}</div>
206+
</Dialog.Body>
207+
</Dialog>
208+
</React.Fragment>
209+
);
210+
},
211+
argTypes: {
212+
disableDialogFocusTrap: {
213+
control: {
214+
type: 'boolean',
215+
},
216+
},
217+
},
218+
};

src/components/RelativeDatePicker/hooks/useRelativeDatePickerProps.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {i18n} from '../i18n';
1515
import type {RelativeDatePickerState} from './useRelativeDatePickerState';
1616

1717
interface InnerRelativeDatePickerProps {
18-
groupProps: React.HTMLAttributes<unknown>;
18+
groupProps: React.HTMLAttributes<unknown> & {ref: React.Ref<HTMLElement>};
1919
fieldProps: TextInputProps;
2020
modeSwitcherProps: ButtonProps;
2121
calendarButtonProps: ButtonProps;
@@ -128,9 +128,11 @@ export function useRelativeDatePickerProps(
128128
inputRef.current?.focus();
129129
});
130130
}
131+
const groupRef = React.useRef<HTMLElement>(null);
131132

132133
return {
133134
groupProps: {
135+
ref: groupRef,
134136
tabIndex: -1,
135137
role: 'group',
136138
...focusWithinProps,
@@ -199,6 +201,11 @@ export function useRelativeDatePickerProps(
199201
setOpen(false);
200202
focusInput();
201203
},
204+
onOutsideClick: (e) => {
205+
if (e.target && !groupRef.current?.contains(e.target as Node)) {
206+
setOpen(false);
207+
}
208+
},
202209
onTransitionExited: () => {
203210
setFocusedDate(
204211
mode === 'relative'

src/components/RelativeRangeDatePicker/RelativeRangeDatePicker.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,11 @@ export function RelativeRangeDatePicker(props: RelativeRangeDatePickerProps) {
241241
onClose={() => {
242242
setOpen(false);
243243
}}
244+
focusInput={() => {
245+
setTimeout(() => {
246+
inputRef.current?.focus();
247+
});
248+
}}
244249
anchorRef={anchorRef}
245250
isMobile={isMobile}
246251
className={props.popupClassName}

src/components/RelativeRangeDatePicker/__stories__/RelativeRangeDatePiker.stories.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1+
import React from 'react';
2+
13
import {dateTimeParse} from '@gravity-ui/date-utils';
4+
import {Button, Dialog} from '@gravity-ui/uikit';
25
import {toaster} from '@gravity-ui/uikit/toaster-singleton-react-18';
36
import {action} from '@storybook/addon-actions';
47
import type {Meta, StoryObj} from '@storybook/react';
58

69
import {timeZoneControl} from '../../../demo/utils/zones';
710
import type {Value} from '../../RelativeDatePicker';
811
import {RelativeRangeDatePicker} from '../RelativeRangeDatePicker';
12+
import type {RelativeRangeDatePickerProps} from '../RelativeRangeDatePicker';
913

1014
const meta: Meta<typeof RelativeRangeDatePicker> = {
1115
title: 'Components/RelativeRangeDatePicker',
@@ -81,3 +85,40 @@ export const Default = {
8185
timeZone: timeZoneControl,
8286
},
8387
} satisfies Story;
88+
89+
export const InsideDialog: StoryObj<
90+
RelativeRangeDatePickerProps & {disableDialogFocusTrap?: boolean}
91+
> = {
92+
...Default,
93+
render: function InsideDialog(args) {
94+
const [isOpen, setOpen] = React.useState(false);
95+
return (
96+
<React.Fragment>
97+
<Button
98+
onClick={() => {
99+
setOpen(true);
100+
}}
101+
>
102+
Open dialog
103+
</Button>
104+
<Dialog
105+
open={isOpen}
106+
onClose={() => setOpen(false)}
107+
disableFocusTrap={args.disableDialogFocusTrap}
108+
>
109+
<Dialog.Header />
110+
<Dialog.Body>
111+
<div style={{paddingTop: 16}}>{Default.render(args)}</div>
112+
</Dialog.Body>
113+
</Dialog>
114+
</React.Fragment>
115+
);
116+
},
117+
argTypes: {
118+
disableDialogFocusTrap: {
119+
control: {
120+
type: 'boolean',
121+
},
122+
},
123+
},
124+
};

src/components/RelativeRangeDatePicker/components/PickerDialog/PickerDialog.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,15 @@ export interface PickerDialogProps {
2626
isMobile?: boolean;
2727
anchorRef?: React.RefObject<HTMLElement>;
2828
onClose: () => void;
29+
focusInput: () => void;
2930
}
3031

3132
export function PickerDialog({
3233
props,
3334
state,
3435
open,
3536
onClose,
37+
focusInput,
3638
isMobile,
3739
anchorRef,
3840
className,
@@ -52,13 +54,16 @@ export function PickerDialog({
5254
return (
5355
<Popup
5456
open={open}
57+
onEscapeKeyDown={() => {
58+
onClose();
59+
focusInput();
60+
}}
5561
onClose={onClose}
5662
role="dialog"
5763
anchorRef={anchorRef}
5864
contentClassName={b('content', {size: props.size}, className)}
5965
autoFocus
6066
focusTrap
61-
restoreFocus
6267
>
6368
<DialogContent {...props} state={state} onApply={onClose} />
6469
</Popup>

0 commit comments

Comments
 (0)