Skip to content

Commit 3620915

Browse files
feat(fuselage): Implement OwnerDocument context and update document references (#1947)
Co-authored-by: Tasso Evangelista <tasso.evangelista@rocket.chat>
1 parent 5190571 commit 3620915

10 files changed

Lines changed: 71 additions & 24 deletions

File tree

.changeset/khaki-pants-exist.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@rocket.chat/fuselage": minor
3+
---
4+
5+
feat(fuselage): Implement `OwnerDocument` context and update `document` references

packages/fuselage/src/components/AudioPlayer/AudioPlayer.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,33 @@ import type { TrackHTMLAttributes } from 'react';
33
import { useState, useRef, forwardRef } from 'react';
44

55
import { Box, Button, IconButton, Margins } from '../..';
6+
import { useOwnerDocument } from '../../contexts';
67
import { Slider } from '../Slider';
78

89
const getMaskTime = (durationTime: number) =>
910
new Date(durationTime * 1000)
1011
.toISOString()
1112
.slice(durationTime > 60 * 60 ? 11 : 14, 19);
1213

13-
function forceDownload(url: string, fileName?: string) {
14+
function forceDownload(
15+
ownerDocument: Document,
16+
url: string,
17+
fileName?: string,
18+
) {
1419
const xhr = new XMLHttpRequest();
1520
xhr.open('GET', url, true);
1621
xhr.responseType = 'blob';
1722
xhr.onload = function () {
1823
const urlCreator = window.URL || window.webkitURL;
1924
const imageUrl = urlCreator.createObjectURL(this.response);
20-
const tag = document.createElement('a');
25+
const tag = ownerDocument.createElement('a');
2126
tag.href = imageUrl;
2227
if (fileName) {
2328
tag.download = fileName;
2429
}
25-
document.body.appendChild(tag);
30+
ownerDocument.body.appendChild(tag);
2631
tag.click();
27-
document.body.removeChild(tag);
32+
ownerDocument.body.removeChild(tag);
2833
};
2934
xhr.send();
3035
}
@@ -126,6 +131,8 @@ const AudioPlayer = forwardRef<HTMLAudioElement, AudioPlayerProps>(
126131
handlePlaybackSpeed(1);
127132
};
128133

134+
const { document: ownerDocument } = useOwnerDocument();
135+
129136
return (
130137
<Box
131138
borderWidth='default'
@@ -188,9 +195,9 @@ const AudioPlayer = forwardRef<HTMLAudioElement, AudioPlayerProps>(
188195
medium
189196
onClick={(e) => {
190197
const { host } = new URL(src);
191-
if (host !== window.location.host) {
198+
if (host !== ownerDocument.defaultView?.location.host) {
192199
e.preventDefault();
193-
forceDownload(src);
200+
forceDownload(ownerDocument, src);
194201
}
195202
}}
196203
/>

packages/fuselage/src/components/Menu/Menu.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { createPortal } from 'react-dom';
77
import type { MenuTriggerProps } from 'react-stately';
88
import { useMenuTriggerState } from 'react-stately';
99

10+
import { useOwnerDocument } from '../../contexts';
1011
import type { BoxProps } from '../Box';
1112
import { IconButton } from '../Button';
1213
import type { IconButtonProps } from '../Button/IconButton';
@@ -65,6 +66,8 @@ const Menu = <T extends object>({
6566
const sizes = { large, medium, tiny, mini };
6667
const defaultSmall = !large && !medium && !tiny && !mini;
6768

69+
const { document: ownerDocument } = useOwnerDocument();
70+
6871
const popover = state.isOpen && (
6972
<MenuPopover
7073
state={state}
@@ -99,7 +102,9 @@ const Menu = <T extends object>({
99102
{...sizes}
100103
/>
101104
)}
102-
{detached ? createPortal(popover, document.body) : popover}
105+
{detached
106+
? createPortal(popover, ownerDocument?.body || document.body)
107+
: popover}
103108
</>
104109
);
105110
};
Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,25 @@
11
import { useMemo } from 'react';
22

3-
export const useCreateStyleContainer = (id: string) =>
4-
useMemo(() => {
3+
import { useOwnerDocument } from '../../../contexts';
4+
5+
export const useCreateStyleContainer = (id: string) => {
6+
const { document: ownerDocument } = useOwnerDocument();
7+
return useMemo(() => {
58
const refElement =
6-
document.getElementById('rcx-styles') || document.head.lastChild;
9+
ownerDocument.getElementById('rcx-styles') ||
10+
ownerDocument.head.lastChild;
711

8-
const el = document.getElementById(id);
12+
const el = ownerDocument.getElementById(id);
913

1014
if (el) {
1115
return el;
1216
}
1317

14-
const styleElement = document.createElement('style');
18+
const styleElement = ownerDocument.createElement('style');
1519
styleElement.setAttribute('id', id);
1620

17-
document.head.insertBefore(styleElement, refElement);
18-
document.head.appendChild(document.createElement('style'));
21+
ownerDocument.head.insertBefore(styleElement, refElement);
22+
ownerDocument.head.appendChild(ownerDocument.createElement('style'));
1923
return styleElement;
20-
}, [id]);
24+
}, [id, ownerDocument]);
25+
};

packages/fuselage/src/components/Popover/Popover.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,31 @@ import type { AriaPopoverProps } from 'react-aria';
44
import { usePopover, DismissButton, Overlay } from 'react-aria';
55
import type { OverlayTriggerState } from 'react-stately';
66

7+
import { useOwnerDocument } from '../../contexts';
8+
79
export interface PopoverProps extends Omit<AriaPopoverProps, 'popoverRef'> {
810
children: ReactNode;
911
state: OverlayTriggerState;
12+
portalContainer?: Element;
1013
}
1114

12-
function Popover(props: PopoverProps) {
15+
function Popover({ portalContainer, ...props }: PopoverProps) {
1316
const popoverRef = useRef<HTMLDivElement>(null);
1417
const { state, children, isNonModal } = props;
1518

19+
const { document: ownerDocument } = useOwnerDocument();
20+
1621
const { popoverProps, underlayProps } = usePopover(
1722
{
1823
...props,
1924
popoverRef,
25+
boundaryElement: ownerDocument?.body,
2026
},
2127
state,
2228
);
2329

2430
return (
25-
<Overlay>
31+
<Overlay portalContainer={ownerDocument?.body}>
2632
{!isNonModal && <div {...underlayProps} />}
2733
<div {...popoverProps} ref={popoverRef}>
2834
{!isNonModal && <DismissButton onDismiss={state.close} />}

packages/fuselage/src/components/Position/Position.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { RefObject, ReactElement } from 'react';
44
import { useRef, useMemo, cloneElement, useState, useEffect } from 'react';
55
import { createPortal } from 'react-dom';
66

7+
import { useOwnerDocument } from '../../contexts';
78
import type { BoxProps } from '../Box';
89

910
export type PositionProps = {
@@ -33,26 +34,29 @@ const Position = ({
3334
() => ({ position: 'fixed', ...positionStyle }),
3435
[positionStyle],
3536
);
37+
38+
const { document: ownerDocument } = useOwnerDocument();
39+
3640
const [portalContainer] = useState(() => {
37-
const prev = document.getElementById('position-container');
41+
const prev = ownerDocument.getElementById('position-container');
3842
if (prev) {
3943
return prev;
4044
}
41-
const element = document.createElement('div');
45+
const element = ownerDocument.createElement('div');
4246

4347
element.id = 'position-container';
4448

45-
document.body.appendChild(element);
49+
ownerDocument.body.appendChild(element);
4650
return element;
4751
});
4852

4953
useEffect(
5054
() => () => {
5155
if (portalContainer.childNodes.length === 0) {
52-
document.body.removeChild(portalContainer);
56+
ownerDocument.body.removeChild(portalContainer);
5357
}
5458
},
55-
[portalContainer],
59+
[portalContainer, ownerDocument],
5660
);
5761

5862
return createPortal(
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { createContext, useContext } from 'react';
2+
3+
export const OwnerDocument = createContext<{
4+
document: Document;
5+
}>({ document: window.document });
6+
7+
export const useOwnerDocument = () => {
8+
return useContext(OwnerDocument);
9+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './OwnerDocument';

packages/fuselage/src/hooks/useStyle.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
} from '@rocket.chat/css-in-js';
88
import { useDebugValue, useInsertionEffect, useMemo } from 'react';
99

10+
import { useOwnerDocument } from '../contexts';
11+
1012
export const useStyle = (cssFn: cssFn | undefined, arg: unknown) => {
1113
const content = useMemo(() => (cssFn ? cssFn(arg) : undefined), [arg, cssFn]);
1214

@@ -20,19 +22,21 @@ export const useStyle = (cssFn: cssFn | undefined, arg: unknown) => {
2022

2123
useDebugValue(className);
2224

25+
const { document } = useOwnerDocument();
26+
2327
useInsertionEffect(() => {
2428
if (!content || !className) {
2529
return;
2630
}
2731

2832
const escapedClassName = escapeName(className);
2933
const transpiledContent = transpile(`.${escapedClassName}`, content);
30-
const detach = attachRules(transpiledContent);
34+
const detach = attachRules(transpiledContent, { document });
3135

3236
return () => {
3337
setTimeout(detach, 1000);
3438
};
35-
}, [className, content]);
39+
}, [className, content, document]);
3640

3741
return className;
3842
};

packages/fuselage/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import './index.scss';
22

33
export * from './components';
44
export * from './styleTokens';
5+
export * from './contexts';
56

67
export { Palette, __setThrowErrorOnInvalidToken__ } from './Theme';
78
export { useArrayLikeClassNameProp } from './hooks/useArrayLikeClassNameProp';

0 commit comments

Comments
 (0)