From b13e71e454e978fef8f76e68304360165566ec1c Mon Sep 17 00:00:00 2001 From: Dante Soares Date: Tue, 28 Jan 2025 10:55:20 -0600 Subject: [PATCH] CORE-670 Split ToastContainer into BodyPortalToastContainer and non-body portal version (#67) * Added a "portal" boolean that can turn off BodyPortal, used in ToastContainer * Use composition instead to split ToastContainer into BodyPortal and normal versions * DRY up the ToastContainer --- package.json | 2 +- src/components/ToastContainer.spec.tsx | 34 ++++- src/components/ToastContainer.stories.tsx | 12 +- src/components/ToastContainer.tsx | 44 ++++-- .../ToastContainer.spec.tsx.snap | 140 +++++++++++++++++- 5 files changed, 210 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index e66c99abd..bfa70405a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@openstax/ui-components", - "version": "1.11.2", + "version": "1.12.0", "license": "MIT", "source": "./src/index.ts", "types": "./dist/index.d.ts", diff --git a/src/components/ToastContainer.spec.tsx b/src/components/ToastContainer.spec.tsx index 39ca19015..5fbef6c44 100644 --- a/src/components/ToastContainer.spec.tsx +++ b/src/components/ToastContainer.spec.tsx @@ -1,5 +1,5 @@ -import { render, act } from '@testing-library/react'; -import { ToastContainer } from './ToastContainer'; +import { act, render } from '@testing-library/react'; +import { BodyPortalToastContainer, ToastContainer } from './ToastContainer'; import { ToastData } from '../../src/types'; jest.useFakeTimers(); @@ -10,7 +10,7 @@ const toasts: ToastData[] = [ { id: '3', title: 'Success', message: 'message', variant: 'success', dismissAfterMs: 2000 }, ]; -describe('ToastContainer', () => { +describe('BodyPortalToastContainer', () => { let root: HTMLElement; beforeEach(() => { @@ -20,18 +20,40 @@ describe('ToastContainer', () => { }); it('matches snapshot', () => { - render(, { container: root }); + render(, { container: root }); + expect(document.body).toMatchSnapshot(); + }); + + it('uses inline prop', () => { + render(, { container: root }); + expect(document.body).toMatchSnapshot(); + }); + + it('runs callback', () => { + const callback = jest.fn(); + render(, { container: root }); + act(() => { + jest.advanceTimersByTime(10000); + expect(callback).toHaveBeenCalledWith('3'); + expect(callback).toHaveBeenCalledWith('2'); + }); + }); +}); + +describe('ToastContainer', () => { + it('matches snapshot', () => { + render(); expect(document.body).toMatchSnapshot(); }); it('uses inline prop', () => { - render(, { container: root }); + render(); expect(document.body).toMatchSnapshot(); }); it('runs callback', () => { const callback = jest.fn(); - render(, { container: root }); + render(); act(() => { jest.advanceTimersByTime(10000); expect(callback).toHaveBeenCalledWith('3'); diff --git a/src/components/ToastContainer.stories.tsx b/src/components/ToastContainer.stories.tsx index 5269152f5..db83d5524 100644 --- a/src/components/ToastContainer.stories.tsx +++ b/src/components/ToastContainer.stories.tsx @@ -1,6 +1,12 @@ import styled from 'styled-components'; import { ToastData } from '../../src/types'; -import { ToastContainer } from './ToastContainer'; +import { BodyPortalToastContainer, ToastContainer } from './ToastContainer'; + +const StyledBodyPortalToastContainer = styled(BodyPortalToastContainer)` + top: 2rem; + left: 2rem; + right: unset; +`; const StyledToastContainer = styled(ToastContainer)` top: 2rem; @@ -13,4 +19,6 @@ const toasts: ToastData[] = [ { title: 'Error', message: 'message', variant: 'failure', dismissAfterMs: 4000 }, { title: 'Success', message: 'message', variant: 'success', dismissAfterMs: 2000 }, ]; -export const Default = () => ; + +export const UsingBodyPortal = () => ; +export const WithoutBodyPortal = () => ; diff --git a/src/components/ToastContainer.tsx b/src/components/ToastContainer.tsx index c39d2be09..7f814de55 100644 --- a/src/components/ToastContainer.tsx +++ b/src/components/ToastContainer.tsx @@ -1,10 +1,13 @@ -import styled, { css } from 'styled-components'; +import styled, { css, ThemedStyledFunction } from 'styled-components'; import { BodyPortal } from './BodyPortal'; import { Toast } from './Toast'; import { zIndex } from '../../src/theme'; import { ToastData } from '../../src/types'; +import { ComponentType } from 'react'; -const StyledToastContainer = styled(BodyPortal)` +const makeStyledToastContainer = >( + func: ThemedStyledFunction +) => func` ${(props: {inline: boolean}) => !props.inline && css` position: fixed; right: 2rem; @@ -16,15 +19,38 @@ const StyledToastContainer = styled(BodyPortal)` gap: 1vh; `; -export const ToastContainer = ({ toasts, onDismissToast, inline = false, className }: { - toasts: ToastData[], onDismissToast?: ToastData['onDismiss'], inline?: boolean, className?: string -}) => { - return - {toasts.map((toast, index) => JSX.Element; + +const makeToasts = (toasts: ToastData[], inline: boolean, onDismissToast?: (id: string | undefined) => void) => ( + toasts.map((toast, index) => + {toast.message})} + >{toast.message} + ) +); + +export const ToastContainer: ToastContainerComponent = ({ toasts, onDismissToast, inline = false, className }) => ( + + {makeToasts(toasts, inline, onDismissToast)} -} +); + +export const BodyPortalToastContainer: ToastContainerComponent = ({ toasts, onDismissToast, inline = false, className }) => ( + + {makeToasts(toasts, inline, onDismissToast)} + +); diff --git a/src/components/__snapshots__/ToastContainer.spec.tsx.snap b/src/components/__snapshots__/ToastContainer.spec.tsx.snap index 938e59dd4..373111a53 100644 --- a/src/components/__snapshots__/ToastContainer.spec.tsx.snap +++ b/src/components/__snapshots__/ToastContainer.spec.tsx.snap @@ -1,12 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ToastContainer matches snapshot 1`] = ` +exports[`BodyPortalToastContainer matches snapshot 1`] = `
`; -exports[`ToastContainer uses inline prop 1`] = ` +exports[`BodyPortalToastContainer uses inline prop 1`] = `
`; + +exports[`ToastContainer matches snapshot 1`] = ` + +
+
+
+
+
+ Neutral +
+
+ message +
+
+
+
+
+
+ Error +
+
+ message +
+
+
+
+
+
+ Success +
+
+ message +
+
+
+
+
+ +`; + +exports[`ToastContainer uses inline prop 1`] = ` + +
+
+
+
+
+ Neutral +
+
+ message +
+
+
+
+
+
+ Error +
+
+ message +
+
+
+
+
+
+ Success +
+
+ message +
+
+
+
+
+ +`;