Skip to content

Commit 60edacc

Browse files
annezclaude
andcommitted
fix(core): prevent timezone dialog from closing parent dialogs
Stop mousedown events from propagating outside the DialogTimeZone component. This dialog renders via portal outside the DateTimeInput's popover DOM tree, so clicks inside it were being detected as "outside" the date picker by useClickOutsideEvent, closing the entire dialog stack. Fixes the issue where clicking anywhere in the timezone selector dialog (opened from Schedule Publishing) would close all parent dialogs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent b0cec6f commit 60edacc

File tree

1 file changed

+77
-66
lines changed

1 file changed

+77
-66
lines changed

packages/sanity/src/core/components/timeZone/DialogTimeZone.tsx

Lines changed: 77 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {SearchIcon} from '@sanity/icons'
22
import {Autocomplete, Card, Flex, Inline, Stack, Text, type Theme} from '@sanity/ui'
3-
import {useCallback, useMemo, useState} from 'react'
3+
import {type MouseEvent, useCallback, useMemo, useState} from 'react'
44
import {css, styled} from 'styled-components'
55

66
import {Dialog} from '../../../ui-components'
@@ -96,76 +96,87 @@ const DialogTimeZone = (props: DialogTimeZoneProps) => {
9696
return `${option.alternativeName} (${option.namePretty})`
9797
}, [])
9898

99+
// Stop mousedown events from propagating to parent click-outside handlers.
100+
// This dialog renders via portal outside the DateTimeInput's popover DOM tree,
101+
// so clicks inside it would otherwise be detected as "outside" the date picker
102+
// and close the entire dialog stack.
103+
const handleMouseDown = useCallback((e: MouseEvent) => {
104+
e.stopPropagation()
105+
}, [])
106+
99107
return (
100-
<Dialog
101-
footer={{
102-
confirmButton: {
103-
text: 'Update time zone',
104-
disabled: !isDirty || !selectedTz,
105-
onClick: handleTimeZoneUpdate,
106-
tone: 'primary',
107-
},
108-
}}
109-
header="Select time zone"
110-
id="time-zone"
111-
onClose={onClose}
112-
width={1}
113-
>
114-
<Stack padding={4} space={5}>
115-
<Text size={1}>{timeZoneScopeTypeToLabel[timeZoneScope.type]}</Text>
116-
<Stack space={3}>
117-
<Flex align="center" justify="space-between">
118-
<Inline space={2}>
119-
<Text size={1} weight="semibold">
120-
{t('time-zone.time-zone')}
121-
</Text>
122-
{isLocalTzSelected && (
123-
<Text muted size={1}>
124-
{t('time-zone.local-time')}
108+
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
109+
<div onMouseDown={handleMouseDown}>
110+
<Dialog
111+
footer={{
112+
confirmButton: {
113+
text: 'Update time zone',
114+
disabled: !isDirty || !selectedTz,
115+
onClick: handleTimeZoneUpdate,
116+
tone: 'primary',
117+
},
118+
}}
119+
header="Select time zone"
120+
id="time-zone"
121+
onClose={onClose}
122+
width={1}
123+
>
124+
<Stack padding={4} space={5}>
125+
<Text size={1}>{timeZoneScopeTypeToLabel[timeZoneScope.type]}</Text>
126+
<Stack space={3}>
127+
<Flex align="center" justify="space-between">
128+
<Inline space={2}>
129+
<Text size={1} weight="semibold">
130+
{t('time-zone.time-zone')}
131+
</Text>
132+
{isLocalTzSelected && (
133+
<Text muted size={1}>
134+
{t('time-zone.local-time')}
135+
</Text>
136+
)}
137+
</Inline>
138+
{!isLocalTzSelected && (
139+
<Text size={1} weight="medium">
140+
<a onClick={handleTimeZoneSelectLocal} style={{cursor: 'pointer'}}>
141+
{t('time-zone.action.select-local-time-zone')}
142+
</a>
125143
</Text>
126144
)}
127-
</Inline>
128-
{!isLocalTzSelected && (
129-
<Text size={1} weight="medium">
130-
<a onClick={handleTimeZoneSelectLocal} style={{cursor: 'pointer'}}>
131-
{t('time-zone.action.select-local-time-zone')}
132-
</a>
133-
</Text>
134-
)}
135-
</Flex>
136-
137-
<Autocomplete
138-
fontSize={2}
139-
icon={SearchIcon}
140-
id="timezone"
141-
onChange={handleTimeZoneChange}
142-
openButton
143-
options={allTimeZones}
144-
padding={4}
145-
filterOption={(query: string, option: NormalizedTimeZone) => {
146-
if (query === '') return true
147-
return `${option.city} (GMT
145+
</Flex>
146+
147+
<Autocomplete
148+
fontSize={2}
149+
icon={SearchIcon}
150+
id="timezone"
151+
onChange={handleTimeZoneChange}
152+
openButton
153+
options={allTimeZones}
154+
padding={4}
155+
filterOption={(query: string, option: NormalizedTimeZone) => {
156+
if (query === '') return true
157+
return `${option.city} (GMT
148158
${option.offset}) ${option.alternativeName}`
149-
?.toLowerCase()
150-
?.includes(query?.toLowerCase())
151-
}}
152-
placeholder={t('time-zone.action.search-for-timezone-placeholder')}
153-
popover={{
154-
boundaryElement:
155-
timeZoneScope.type === 'input'
156-
? (document.querySelector('#document-panel-scroller') as HTMLElement)
157-
: (document.querySelector('body') as HTMLElement),
158-
constrainSize: true,
159-
placement: 'bottom-start',
160-
}}
161-
renderOption={renderOption}
162-
renderValue={renderValue}
163-
tabIndex={-1}
164-
value={selectedTz?.value}
165-
/>
159+
?.toLowerCase()
160+
?.includes(query?.toLowerCase())
161+
}}
162+
placeholder={t('time-zone.action.search-for-timezone-placeholder')}
163+
popover={{
164+
boundaryElement:
165+
timeZoneScope.type === 'input'
166+
? (document.querySelector('#document-panel-scroller') as HTMLElement)
167+
: (document.querySelector('body') as HTMLElement),
168+
constrainSize: true,
169+
placement: 'bottom-start',
170+
}}
171+
renderOption={renderOption}
172+
renderValue={renderValue}
173+
tabIndex={-1}
174+
value={selectedTz?.value}
175+
/>
176+
</Stack>
166177
</Stack>
167-
</Stack>
168-
</Dialog>
178+
</Dialog>
179+
</div>
169180
)
170181
}
171182

0 commit comments

Comments
 (0)