Skip to content

Commit f122950

Browse files
authored
fix(RelativeRangeDatePicker): correctly close picker popup inside dialog (#117)
1 parent 8270119 commit f122950

File tree

7 files changed

+103
-7
lines changed

7 files changed

+103
-7
lines changed

src/components/DatePicker/hooks/useDatePickerProps.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export function useDatePickerProps<T extends DateTime | RangeValue<DateTime>>(
7878

7979
function focusInput() {
8080
setTimeout(() => {
81-
inputRef.current?.focus();
81+
inputRef.current?.focus({preventScroll: true});
8282
});
8383
}
8484

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';
@@ -62,12 +62,13 @@ export function RelativeDatePicker(props: RelativeDatePickerProps) {
6262
} = useRelativeDatePickerProps(state, props);
6363

6464
const anchorRef = React.useRef<HTMLDivElement>(null);
65+
const handleRef = useForkRef(anchorRef, groupProps.ref);
6566

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

6970
return (
70-
<div ref={anchorRef} className={b(null, props.className)} {...groupProps}>
71+
<div {...groupProps} ref={handleRef} className={b(null, props.className)}>
7172
{isMobile && state.mode === 'absolute' && (
7273
<MobileCalendar
7374
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> = {
@@ -180,3 +181,39 @@ export const WithCustomCalendar = {
180181
});
181182
},
182183
} satisfies Story;
184+
185+
export const InsideDialog: StoryObj<RelativeDatePickerProps & {disableDialogFocusTrap?: boolean}> =
186+
{
187+
...Default,
188+
render: function InsideDialog(args) {
189+
const [isOpen, setOpen] = React.useState(false);
190+
return (
191+
<React.Fragment>
192+
<Button
193+
onClick={() => {
194+
setOpen(true);
195+
}}
196+
>
197+
Open dialog
198+
</Button>
199+
<Dialog
200+
open={isOpen}
201+
onClose={() => setOpen(false)}
202+
disableFocusTrap={args.disableDialogFocusTrap}
203+
>
204+
<Dialog.Header />
205+
<Dialog.Body>
206+
<div style={{paddingTop: 16}}>{Default.render(args)}</div>
207+
</Dialog.Body>
208+
</Dialog>
209+
</React.Fragment>
210+
);
211+
},
212+
argTypes: {
213+
disableDialogFocusTrap: {
214+
control: {
215+
type: 'boolean',
216+
},
217+
},
218+
},
219+
};

src/components/RelativeDatePicker/hooks/useRelativeDatePickerProps.ts

Lines changed: 9 additions & 2 deletions
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;
@@ -125,12 +125,14 @@ export function useRelativeDatePickerProps(
125125

126126
function focusInput() {
127127
setTimeout(() => {
128-
inputRef.current?.focus();
128+
inputRef.current?.focus({preventScroll: true});
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
@@ -250,6 +250,11 @@ export function RelativeRangeDatePicker(props: RelativeRangeDatePickerProps) {
250250
onClose={() => {
251251
setOpen(false);
252252
}}
253+
focusInput={() => {
254+
setTimeout(() => {
255+
inputRef.current?.focus({preventScroll: true});
256+
});
257+
}}
253258
anchorRef={anchorRef}
254259
isMobile={isMobile}
255260
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',
@@ -82,3 +86,40 @@ export const Default = {
8286
timeZone: timeZoneControl,
8387
},
8488
} satisfies Story;
89+
90+
export const InsideDialog: StoryObj<
91+
RelativeRangeDatePickerProps & {disableDialogFocusTrap?: boolean}
92+
> = {
93+
...Default,
94+
render: function InsideDialog(args) {
95+
const [isOpen, setOpen] = React.useState(false);
96+
return (
97+
<React.Fragment>
98+
<Button
99+
onClick={() => {
100+
setOpen(true);
101+
}}
102+
>
103+
Open dialog
104+
</Button>
105+
<Dialog
106+
open={isOpen}
107+
onClose={() => setOpen(false)}
108+
disableFocusTrap={args.disableDialogFocusTrap}
109+
>
110+
<Dialog.Header />
111+
<Dialog.Body>
112+
<div style={{paddingTop: 16}}>{Default.render(args)}</div>
113+
</Dialog.Body>
114+
</Dialog>
115+
</React.Fragment>
116+
);
117+
},
118+
argTypes: {
119+
disableDialogFocusTrap: {
120+
control: {
121+
type: 'boolean',
122+
},
123+
},
124+
},
125+
};

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)