Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/many-monkeys-deliver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@rocket.chat/fuselage-toastbar': patch
---

Enable compatibility with React 18

React 18's Strict Mode fires effects twice, which breaks Fuselage's toast bar portal.
92 changes: 29 additions & 63 deletions packages/fuselage-toastbar/src/ToastBar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,85 +53,51 @@ export const Default: StoryFn = () => {
);
};

export const TopStart: StoryFn = () => {
const Template: StoryFn<{
position: 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end';
}> = ({ position }) => {
const dispatchToastMessage = useToastBarDispatch();

const handleDispatch = () =>
dispatchToastMessage({
type: 'success',
message: DEFAULT_MESSAGE,
position: 'top-start',
});

useEffect(() => {
handleDispatch();
}, []);

return (
<Button primary onClick={handleDispatch}>
Dispatch ToastBar
</Button>
);
};

export const TopEnd: StoryFn = () => {
const dispatchToastMessage = useToastBarDispatch();

const handleDispatch = () =>
dispatchToastMessage({
type: 'success',
message: DEFAULT_MESSAGE,
position,
});

useEffect(() => {
handleDispatch();
}, []);
}, [dispatchToastMessage, position]);

return (
<Button primary onClick={handleDispatch}>
<Button
primary
onClick={() =>
dispatchToastMessage({
type: 'success',
message: DEFAULT_MESSAGE,
position,
})
}
>
Dispatch ToastBar
</Button>
);
};

export const BottomStart: StoryFn = () => {
const dispatchToastMessage = useToastBarDispatch();

const handleDispatch = () =>
dispatchToastMessage({
type: 'success',
message: DEFAULT_MESSAGE,
position: 'bottom-start',
});

useEffect(() => {
handleDispatch();
}, []);

return (
<Button primary onClick={handleDispatch}>
Dispatch ToastBar
</Button>
);
export const TopStart = Template.bind({});
TopStart.args = {
position: 'top-start',
};

export const BottomEnd: StoryFn = () => {
const dispatchToastMessage = useToastBarDispatch();

const handleDispatch = () =>
dispatchToastMessage({
type: 'success',
message: DEFAULT_MESSAGE,
position: 'bottom-end',
});
export const TopEnd = Template.bind({});
TopEnd.args = {
position: 'top-end',
};

useEffect(() => {
handleDispatch();
}, []);
export const BottomStart = Template.bind({});
BottomStart.args = {
position: 'bottom-start',
};

return (
<Button primary onClick={handleDispatch}>
Dispatch ToastBar
</Button>
);
export const BottomEnd = Template.bind({});
BottomEnd.args = {
position: 'bottom-end',
};
55 changes: 50 additions & 5 deletions packages/fuselage-toastbar/src/ToastBarPortal.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,62 @@
import type { ReactElement, ReactNode } from 'react';
import { memo, useEffect, useState } from 'react';
import { memo, useLayoutEffect } from 'react';
import { createPortal } from 'react-dom';

import { createAnchor } from './lib/utils/createAnchor';
import { deleteAnchor } from './lib/utils/deleteAnchor';
const ensureAnchorElement = (id: string): HTMLElement => {
const existingAnchor = document.getElementById(id);
if (existingAnchor) return existingAnchor;

const newAnchor = document.createElement('div');
newAnchor.id = id;
document.body.appendChild(newAnchor);
return newAnchor;
};

const getAnchorRefCount = (anchorElement: HTMLElement): number => {
const { refCount } = anchorElement.dataset;
if (refCount) return parseInt(refCount, 10);
return 0;
};

const setAnchorRefCount = (
anchorElement: HTMLElement,
refCount: number,
): void => {
anchorElement.dataset.refCount = String(refCount);
};

const refAnchorElement = (anchorElement: HTMLElement): void => {
setAnchorRefCount(anchorElement, getAnchorRefCount(anchorElement) + 1);

if (anchorElement.parentElement !== document.body) {
document.body.appendChild(anchorElement);
}
};

const unrefAnchorElement = (anchorElement: HTMLElement): void => {
const refCount = getAnchorRefCount(anchorElement) - 1;
setAnchorRefCount(anchorElement, refCount);

if (refCount <= 0) {
document.body.removeChild(anchorElement);
}
};

type ToastBarPortalProps = {
children?: ReactNode;
};

const ToastBarPortal = ({ children }: ToastBarPortalProps): ReactElement => {
const [toastBarRoot] = useState(() => createAnchor('toastBarRoot'));
useEffect(() => (): void => deleteAnchor(toastBarRoot), [toastBarRoot]);
const toastBarRoot = ensureAnchorElement('toastBarRoot');

useLayoutEffect(() => {
refAnchorElement(toastBarRoot);

return () => {
unrefAnchorElement(toastBarRoot);
};
}, [toastBarRoot]);

return createPortal(children, toastBarRoot);
};

Expand Down
23 changes: 0 additions & 23 deletions packages/fuselage-toastbar/src/lib/utils/createAnchor.ts

This file was deleted.

11 changes: 0 additions & 11 deletions packages/fuselage-toastbar/src/lib/utils/deleteAnchor.ts

This file was deleted.

Loading