Skip to content

Commit 0415e14

Browse files
arturbienchaance
authored andcommitted
replace window with providedDocument.defaultView
1 parent cf8db99 commit 0415e14

File tree

15 files changed

+249
-146
lines changed

15 files changed

+249
-146
lines changed

apps/storybook/stories/presence.stories.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react';
2-
import { Presence } from 'radix-ui/internal';
2+
import { Presence, useDocument } from 'radix-ui/internal';
33
import styles from './presence.stories.module.css';
44

55
export default { title: 'Utilities/Presence' };
@@ -35,15 +35,17 @@ export const WithDeferredMountAnimation = () => {
3535
const timerRef = React.useRef(0);
3636
const [open, setOpen] = React.useState(false);
3737
const [animate, setAnimate] = React.useState(false);
38+
const documentWindow = useDocument()?.defaultView;
3839

3940
React.useEffect(() => {
41+
if (!documentWindow) return;
4042
if (open) {
41-
timerRef.current = window.setTimeout(() => setAnimate(true), 150);
43+
timerRef.current = documentWindow.setTimeout(() => setAnimate(true), 150);
4244
} else {
4345
setAnimate(false);
44-
window.clearTimeout(timerRef.current);
46+
documentWindow.clearTimeout(timerRef.current);
4547
}
46-
}, [open]);
48+
}, [open, documentWindow]);
4749

4850
return (
4951
<>

packages/react/avatar/src/avatar.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Primitive } from '@radix-ui/react-primitive';
66
import { useIsHydrated } from '@radix-ui/react-use-is-hydrated';
77

88
import type { Scope } from '@radix-ui/react-context';
9+
import { useDocument } from '@radix-ui/react-document-context';
910

1011
/* -------------------------------------------------------------------------------------------------
1112
* Avatar
@@ -99,13 +100,16 @@ const AvatarFallback = React.forwardRef<AvatarFallbackElement, AvatarFallbackPro
99100
const { __scopeAvatar, delayMs, ...fallbackProps } = props;
100101
const context = useAvatarContext(FALLBACK_NAME, __scopeAvatar);
101102
const [canRender, setCanRender] = React.useState(delayMs === undefined);
103+
const providedDocument = useDocument();
104+
const documentWindow = providedDocument?.defaultView;
102105

103106
React.useEffect(() => {
107+
if (!documentWindow) return;
104108
if (delayMs !== undefined) {
105-
const timerId = window.setTimeout(() => setCanRender(true), delayMs);
106-
return () => window.clearTimeout(timerId);
109+
const timerId = documentWindow.setTimeout(() => setCanRender(true), delayMs);
110+
return () => documentWindow.clearTimeout(timerId);
107111
}
108-
}, [delayMs]);
112+
}, [delayMs, documentWindow]);
109113

110114
return canRender && context.imageLoadingStatus !== 'loaded' ? (
111115
<Primitive.span {...fallbackProps} ref={forwardedRef} />
@@ -136,10 +140,12 @@ function useImageLoadingStatus(
136140
) {
137141
const isHydrated = useIsHydrated();
138142
const imageRef = React.useRef<HTMLImageElement | null>(null);
143+
const providedDocument = useDocument();
144+
const documentWindow = providedDocument?.defaultView;
139145
const image = (() => {
140146
if (!isHydrated) return null;
141147
if (!imageRef.current) {
142-
imageRef.current = new window.Image();
148+
imageRef.current = new (documentWindow ?? globalThis.window).Image();
143149
}
144150
return imageRef.current;
145151
})();

packages/react/context-menu/src/context-menu.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { useCallbackRef } from '@radix-ui/react-use-callback-ref';
88
import { useControllableState } from '@radix-ui/react-use-controllable-state';
99

1010
import type { Scope } from '@radix-ui/react-context';
11+
import { useDocument } from '@radix-ui/react-document-context';
1112

1213
type Direction = 'ltr' | 'rtl';
1314
type Point = { x: number; y: number };
@@ -97,10 +98,12 @@ const ContextMenuTrigger = React.forwardRef<ContextMenuTriggerElement, ContextMe
9798
const virtualRef = React.useRef({
9899
getBoundingClientRect: () => DOMRect.fromRect({ width: 0, height: 0, ...pointRef.current }),
99100
});
101+
const documentWindow = useDocument()?.defaultView;
102+
100103
const longPressTimerRef = React.useRef(0);
101104
const clearLongPress = React.useCallback(
102-
() => window.clearTimeout(longPressTimerRef.current),
103-
[]
105+
() => documentWindow?.clearTimeout(longPressTimerRef.current),
106+
[documentWindow]
104107
);
105108
const handleOpen = (event: React.MouseEvent | React.PointerEvent) => {
106109
pointRef.current = { x: event.clientX, y: event.clientY };
@@ -140,7 +143,12 @@ const ContextMenuTrigger = React.forwardRef<ContextMenuTriggerElement, ContextMe
140143
whenTouchOrPen((event) => {
141144
// clear the long press here in case there's multiple touch points
142145
clearLongPress();
143-
longPressTimerRef.current = window.setTimeout(() => handleOpen(event), 700);
146+
if (documentWindow) {
147+
longPressTimerRef.current = documentWindow?.setTimeout(
148+
() => handleOpen(event),
149+
700
150+
);
151+
}
144152
})
145153
)
146154
}

packages/react/dismissable-layer/src/dismissable-layer.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,8 @@ function usePointerDownOutside(onPointerDownOutside?: (event: PointerDownOutside
228228

229229
React.useEffect(() => {
230230
// Only add listeners if document exists
231-
if (!providedDocument) return;
231+
const documentWindow = providedDocument?.defaultView;
232+
if (!documentWindow) return;
232233

233234
const handlePointerDown = (event: PointerEvent) => {
234235
if (event.target && !isPointerInsideReactTreeRef.current) {
@@ -282,12 +283,12 @@ function usePointerDownOutside(onPointerDownOutside?: (event: PointerDownOutside
282283
* })
283284
* });
284285
*/
285-
const timerId = window.setTimeout(() => {
286+
const timerId = documentWindow.setTimeout(() => {
286287
providedDocument.addEventListener('pointerdown', handlePointerDown);
287288
}, 0);
288289

289290
return () => {
290-
window.clearTimeout(timerId);
291+
documentWindow.clearTimeout(timerId);
291292
providedDocument?.removeEventListener('pointerdown', handlePointerDown);
292293
providedDocument?.removeEventListener('click', handleClickRef.current);
293294
};

packages/react/hover-card/src/hover-card.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ const HoverCard: React.FC<HoverCardProps> = (props: ScopedProps<HoverCardProps>)
6464
const closeTimerRef = React.useRef(0);
6565
const hasSelectionRef = React.useRef(false);
6666
const isPointerDownOnContentRef = React.useRef(false);
67+
const documentWindow = useDocument()?.defaultView;
6768

6869
const [open, setOpen] = useControllableState({
6970
prop: openProp,
@@ -74,15 +75,17 @@ const HoverCard: React.FC<HoverCardProps> = (props: ScopedProps<HoverCardProps>)
7475

7576
const handleOpen = React.useCallback(() => {
7677
clearTimeout(closeTimerRef.current);
77-
openTimerRef.current = window.setTimeout(() => setOpen(true), openDelay);
78-
}, [openDelay, setOpen]);
78+
if (!documentWindow) return;
79+
openTimerRef.current = documentWindow.setTimeout(() => setOpen(true), openDelay);
80+
}, [openDelay, setOpen, documentWindow]);
7981

8082
const handleClose = React.useCallback(() => {
8183
clearTimeout(openTimerRef.current);
84+
if (!documentWindow) return;
8285
if (!hasSelectionRef.current && !isPointerDownOnContentRef.current) {
83-
closeTimerRef.current = window.setTimeout(() => setOpen(false), closeDelay);
86+
closeTimerRef.current = documentWindow.setTimeout(() => setOpen(false), closeDelay);
8487
}
85-
}, [closeDelay, setOpen]);
88+
}, [closeDelay, setOpen, documentWindow]);
8689

8790
const handleDismiss = React.useCallback(() => setOpen(false), [setOpen]);
8891

packages/react/menu/src/menu.tsx

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,8 @@ const MenuContentImpl = React.forwardRef<MenuContentImplElement, MenuContentImpl
403403
? { as: Slot, allowPinchZoom: true }
404404
: undefined;
405405

406+
const documentWindow = providedDocument?.defaultView;
407+
406408
const handleTypeaheadSearch = (key: string) => {
407409
const search = searchRef.current + key;
408410
const items = getItems().filter((item) => !item.disabled);
@@ -415,8 +417,10 @@ const MenuContentImpl = React.forwardRef<MenuContentImplElement, MenuContentImpl
415417
// Reset `searchRef` 1 second after it was last updated
416418
(function updateSearch(value: string) {
417419
searchRef.current = value;
418-
window.clearTimeout(timerRef.current);
419-
if (value !== '') timerRef.current = window.setTimeout(() => updateSearch(''), 1000);
420+
if (!documentWindow) return;
421+
documentWindow.clearTimeout(timerRef.current);
422+
if (value !== '')
423+
timerRef.current = documentWindow.setTimeout(() => updateSearch(''), 1000);
420424
})(search);
421425

422426
if (newItem) {
@@ -429,8 +433,8 @@ const MenuContentImpl = React.forwardRef<MenuContentImplElement, MenuContentImpl
429433
};
430434

431435
React.useEffect(() => {
432-
return () => window.clearTimeout(timerRef.current);
433-
}, []);
436+
return () => documentWindow?.clearTimeout(timerRef.current);
437+
}, [documentWindow]);
434438

435439
// Make sure the whole tree has focus guards as our `MenuContent` may be
436440
// the last element in the DOM (because of the `Portal`)
@@ -542,7 +546,7 @@ const MenuContentImpl = React.forwardRef<MenuContentImplElement, MenuContentImpl
542546
onBlur={composeEventHandlers(props.onBlur, (event) => {
543547
// clear search buffer when leaving the menu
544548
if (!event.currentTarget.contains(event.target)) {
545-
window.clearTimeout(timerRef.current);
549+
documentWindow?.clearTimeout(timerRef.current);
546550
searchRef.current = '';
547551
}
548552
})}
@@ -1038,21 +1042,21 @@ const MenuSubTrigger = React.forwardRef<MenuSubTriggerElement, MenuSubTriggerPro
10381042
const openTimerRef = React.useRef<number | null>(null);
10391043
const { pointerGraceTimerRef, onPointerGraceIntentChange } = contentContext;
10401044
const scope = { __scopeMenu: props.__scopeMenu };
1041-
1045+
const documentWindow = useDocument()?.defaultView;
10421046
const clearOpenTimer = React.useCallback(() => {
1043-
if (openTimerRef.current) window.clearTimeout(openTimerRef.current);
1047+
if (openTimerRef.current) documentWindow?.clearTimeout(openTimerRef.current);
10441048
openTimerRef.current = null;
1045-
}, []);
1049+
}, [documentWindow]);
10461050

10471051
React.useEffect(() => clearOpenTimer, [clearOpenTimer]);
10481052

10491053
React.useEffect(() => {
10501054
const pointerGraceTimer = pointerGraceTimerRef.current;
10511055
return () => {
1052-
window.clearTimeout(pointerGraceTimer);
1056+
documentWindow?.clearTimeout(pointerGraceTimer);
10531057
onPointerGraceIntentChange(null);
10541058
};
1055-
}, [pointerGraceTimerRef, onPointerGraceIntentChange]);
1059+
}, [pointerGraceTimerRef, onPointerGraceIntentChange, documentWindow]);
10561060

10571061
return (
10581062
<MenuAnchor asChild {...scope}>
@@ -1084,7 +1088,8 @@ const MenuSubTrigger = React.forwardRef<MenuSubTriggerElement, MenuSubTriggerPro
10841088
if (event.defaultPrevented) return;
10851089
if (!props.disabled && !context.open && !openTimerRef.current) {
10861090
contentContext.onPointerGraceIntentChange(null);
1087-
openTimerRef.current = window.setTimeout(() => {
1091+
if (!documentWindow) return;
1092+
openTimerRef.current = documentWindow.setTimeout(() => {
10881093
context.onOpenChange(true);
10891094
clearOpenTimer();
10901095
}, 100);
@@ -1118,8 +1123,9 @@ const MenuSubTrigger = React.forwardRef<MenuSubTriggerElement, MenuSubTriggerPro
11181123
side,
11191124
});
11201125

1121-
window.clearTimeout(pointerGraceTimerRef.current);
1122-
pointerGraceTimerRef.current = window.setTimeout(
1126+
if (!documentWindow) return;
1127+
documentWindow.clearTimeout(pointerGraceTimerRef.current);
1128+
pointerGraceTimerRef.current = documentWindow.setTimeout(
11231129
() => contentContext.onPointerGraceIntentChange(null),
11241130
300
11251131
);

packages/react/navigation-menu/src/navigation-menu.tsx

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -116,18 +116,20 @@ const NavigationMenu = React.forwardRef<NavigationMenuElement, NavigationMenuPro
116116
const closeTimerRef = React.useRef(0);
117117
const skipDelayTimerRef = React.useRef(0);
118118
const [isOpenDelayed, setIsOpenDelayed] = React.useState(true);
119+
const documentWindow = useDocument()?.defaultView;
119120
const [value, setValue] = useControllableState({
120121
prop: valueProp,
121122
onChange: (value) => {
122123
const isOpen = value !== '';
123124
const hasSkipDelayDuration = skipDelayDuration > 0;
124125

125126
if (isOpen) {
126-
window.clearTimeout(skipDelayTimerRef.current);
127+
documentWindow?.clearTimeout(skipDelayTimerRef.current);
127128
if (hasSkipDelayDuration) setIsOpenDelayed(false);
128129
} else {
129-
window.clearTimeout(skipDelayTimerRef.current);
130-
skipDelayTimerRef.current = window.setTimeout(
130+
documentWindow?.clearTimeout(skipDelayTimerRef.current);
131+
if (!documentWindow) return;
132+
skipDelayTimerRef.current = documentWindow.setTimeout(
131133
() => setIsOpenDelayed(true),
132134
skipDelayDuration
133135
);
@@ -140,16 +142,17 @@ const NavigationMenu = React.forwardRef<NavigationMenuElement, NavigationMenuPro
140142
});
141143

142144
const startCloseTimer = React.useCallback(() => {
143-
window.clearTimeout(closeTimerRef.current);
144-
closeTimerRef.current = window.setTimeout(() => setValue(''), 150);
145-
}, [setValue]);
145+
if (!documentWindow) return;
146+
documentWindow.clearTimeout(closeTimerRef.current);
147+
closeTimerRef.current = documentWindow.setTimeout(() => setValue(''), 150);
148+
}, [setValue, documentWindow]);
146149

147150
const handleOpen = React.useCallback(
148151
(itemValue: string) => {
149-
window.clearTimeout(closeTimerRef.current);
152+
documentWindow?.clearTimeout(closeTimerRef.current);
150153
setValue(itemValue);
151154
},
152-
[setValue]
155+
[setValue, documentWindow]
153156
);
154157

155158
const handleDelayedOpen = React.useCallback(
@@ -158,24 +161,24 @@ const NavigationMenu = React.forwardRef<NavigationMenuElement, NavigationMenuPro
158161
if (isOpenItem) {
159162
// If the item is already open (e.g. we're transitioning from the content to the trigger)
160163
// then we want to clear the close timer immediately.
161-
window.clearTimeout(closeTimerRef.current);
162-
} else {
163-
openTimerRef.current = window.setTimeout(() => {
164-
window.clearTimeout(closeTimerRef.current);
164+
documentWindow?.clearTimeout(closeTimerRef.current);
165+
} else if (documentWindow) {
166+
openTimerRef.current = documentWindow.setTimeout(() => {
167+
documentWindow.clearTimeout(closeTimerRef.current);
165168
setValue(itemValue);
166169
}, delayDuration);
167170
}
168171
},
169-
[value, setValue, delayDuration]
172+
[value, setValue, delayDuration, documentWindow]
170173
);
171174

172175
React.useEffect(() => {
173176
return () => {
174-
window.clearTimeout(openTimerRef.current);
175-
window.clearTimeout(closeTimerRef.current);
176-
window.clearTimeout(skipDelayTimerRef.current);
177+
documentWindow?.clearTimeout(openTimerRef.current);
178+
documentWindow?.clearTimeout(closeTimerRef.current);
179+
documentWindow?.clearTimeout(skipDelayTimerRef.current);
177180
};
178-
}, []);
181+
}, [documentWindow]);
179182

180183
return (
181184
<NavigationMenuProvider
@@ -186,15 +189,15 @@ const NavigationMenu = React.forwardRef<NavigationMenuElement, NavigationMenuPro
186189
orientation={orientation}
187190
rootNavigationMenu={navigationMenu}
188191
onTriggerEnter={(itemValue) => {
189-
window.clearTimeout(openTimerRef.current);
192+
documentWindow?.clearTimeout(openTimerRef.current);
190193
if (isOpenDelayed) handleDelayedOpen(itemValue);
191194
else handleOpen(itemValue);
192195
}}
193196
onTriggerLeave={() => {
194-
window.clearTimeout(openTimerRef.current);
197+
documentWindow?.clearTimeout(openTimerRef.current);
195198
startCloseTimer();
196199
}}
197-
onContentEnter={() => window.clearTimeout(closeTimerRef.current)}
200+
onContentEnter={() => documentWindow?.clearTimeout(closeTimerRef.current)}
198201
onContentLeave={startCloseTimer}
199202
onItemSelect={(itemValue) => {
200203
setValue((prevValue) => (prevValue === itemValue ? '' : itemValue));
@@ -1220,10 +1223,11 @@ function removeFromTabOrder(candidates: HTMLElement[]) {
12201223
}
12211224

12221225
function useResizeObserver(element: HTMLElement | null, onResize: () => void) {
1226+
const documentWindow = useDocument()?.defaultView;
12231227
const handleResize = useCallbackRef(onResize);
12241228
useLayoutEffect(() => {
12251229
let rAF = 0;
1226-
if (element) {
1230+
if (element && documentWindow) {
12271231
/**
12281232
* Resize Observer will throw an often benign error that says `ResizeObserver loop
12291233
* completed with undelivered notifications`. This means that ResizeObserver was not
@@ -1233,15 +1237,15 @@ function useResizeObserver(element: HTMLElement | null, onResize: () => void) {
12331237
*/
12341238
const resizeObserver = new ResizeObserver(() => {
12351239
cancelAnimationFrame(rAF);
1236-
rAF = window.requestAnimationFrame(handleResize);
1240+
rAF = documentWindow.requestAnimationFrame(handleResize);
12371241
});
12381242
resizeObserver.observe(element);
12391243
return () => {
1240-
window.cancelAnimationFrame(rAF);
1244+
documentWindow.cancelAnimationFrame(rAF);
12411245
resizeObserver.unobserve(element);
12421246
};
12431247
}
1244-
}, [element, handleResize]);
1248+
}, [element, handleResize, documentWindow]);
12451249
}
12461250

12471251
function getOpenState(open: boolean) {

0 commit comments

Comments
 (0)