From 685e5d911cde4e472b9baf23d6b7846f491ea533 Mon Sep 17 00:00:00 2001
From: thyhjwb6 <56078793+thyhjwb6@users.noreply.github.com>
Date: Mon, 16 Dec 2024 16:16:00 +0000
Subject: [PATCH 1/4] test(FloatingBox): Align with patterns
---
.../FloatingBox/FloatingBox.test.tsx | 194 ++++++------------
1 file changed, 66 insertions(+), 128 deletions(-)
diff --git a/packages/react-component-library/src/primitives/FloatingBox/FloatingBox.test.tsx b/packages/react-component-library/src/primitives/FloatingBox/FloatingBox.test.tsx
index dbdff52cb0..f3c5f04e9a 100644
--- a/packages/react-component-library/src/primitives/FloatingBox/FloatingBox.test.tsx
+++ b/packages/react-component-library/src/primitives/FloatingBox/FloatingBox.test.tsx
@@ -1,141 +1,79 @@
import React from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
-import { render, RenderResult } from '@testing-library/react'
+import { render, screen } from '@testing-library/react'
import { FloatingBox } from '.'
import { useStatefulRef } from '../../hooks/useStatefulRef'
-describe('FloatingBox', () => {
- let wrapper: RenderResult
- let children: React.ReactElement
+it('renders the floating box when provided with a `renderTarget` and arbitrary JSX content', () => {
+ render(
+ Hello, World!}
+ role="dialog"
+ aria-modal
+ >
+ This is some arbitrary JSX
+
+ )
+
+ expect(screen.getByTestId('floating-box')).toHaveAttribute('role', 'dialog')
+ expect(screen.getByTestId('floating-box')).toHaveAttribute('aria-modal')
+ expect(screen.getByText('Hello, World!')).toBeInTheDocument()
+ expect(screen.getByTestId('floating-box-content').innerHTML).toContain(
+ renderToStaticMarkup(
This is some arbitrary JSX
)
+ )
+ expect(screen.getByTestId('floating-box-styled-target')).toBeInTheDocument()
+})
- describe('when provided a `renderTarget` and arbitrary JSX content', () => {
- beforeEach(() => {
- children = This is some arbitrary JSX
+it('renders the floating box when provided with a `renderTargetElement` and arbitrary JSX content', () => {
+ const TestComponent = () => {
+ const [element, setElement] = useStatefulRef()
- wrapper = render(
- Hello, World!}
- role="dialog"
- aria-modal
- >
- {children}
+ return (
+ <>
+ Hello, World!
+
+ This is some arbitrary JSX
- )
- })
-
- it('applies the supplied `role`', () => {
- expect(wrapper.queryByTestId('floating-box')).toHaveAttribute(
- 'role',
- 'dialog'
- )
- })
-
- it('applies additional arbitrary props to wrapper', () => {
- expect(wrapper.queryByTestId('floating-box')).toHaveAttribute(
- 'aria-modal'
- )
- })
-
- it('applies the `role` attribute', () => {
- expect(wrapper.getByTestId('floating-box')).toHaveAttribute(
- 'role',
- 'dialog'
- )
- })
-
- it('renders an embedded target', () => {
- expect(
- wrapper.getByTestId('floating-box-styled-target')
- ).toBeInTheDocument()
- })
-
- it('renders the box', () => {
- expect(wrapper.getByTestId('floating-box')).toBeInTheDocument()
- })
-
- it('renders the provided renderTarget JSX', () => {
- expect(wrapper.getByText('Hello, World!')).toBeInTheDocument()
- })
-
- it('renders the provided arbitrary JSX', () => {
- expect(wrapper.getByTestId('floating-box-content').innerHTML).toContain(
- renderToStaticMarkup(children)
- )
- })
- })
-
- describe('when provided a `renderTargetElement` and arbitrary JSX content', () => {
- beforeEach(() => {
- children = This is some arbitrary JSX
-
- const TestComponent = () => {
- const [element, setElement] = useStatefulRef()
-
- return (
- <>
- Hello, World!
-
- {children}
-
- >
- )
- }
- wrapper = render()
- })
-
- it('renders the box', () => {
- expect(wrapper.queryByTestId('floating-box')).toBeInTheDocument()
- })
-
- it('does not render an embedded target', () => {
- expect(
- wrapper.queryByTestId('floating-box-styled-target')
- ).not.toBeInTheDocument()
- })
-
- it('renders the provided arbitrary JSX', () => {
- expect(wrapper.getByTestId('floating-box-content').innerHTML).toContain(
- renderToStaticMarkup(children)
- )
- })
- })
-
- describe('when the content changes', () => {
- const ExampleFloatingBox = ({
- children: content,
- }: {
- children: string
- }) => (
- Hello, World!}
- role="dialog"
- aria-modal
- >
- <>{content}>
-
+ >
)
- let initialId: string
-
- beforeEach(() => {
- wrapper = render(Initial content)
- initialId = wrapper.getByTestId('floating-box-content').id
- wrapper.rerender(Updated content)
- })
+ }
+
+ render()
+
+ expect(screen.getByTestId('floating-box')).toHaveAttribute('role', 'dialog')
+ expect(screen.getByTestId('floating-box')).toHaveAttribute('aria-modal')
+ expect(screen.getByText('Hello, World!')).toBeInTheDocument()
+ expect(screen.getByTestId('floating-box-content').innerHTML).toContain(
+ renderToStaticMarkup(This is some arbitrary JSX
)
+ )
+ expect(
+ screen.queryByTestId('floating-box-styled-target')
+ ).not.toBeInTheDocument()
+})
- it('does not generate a new content `id`', () => {
- expect(wrapper.getByTestId('floating-box-content')).toHaveAttribute(
- 'id',
- initialId
- )
- })
- })
+it('does not generate a new content `id` when the content changes', () => {
+ const ExampleFloatingBox = ({ children: content }: { children: string }) => (
+ Hello, World!}
+ role="dialog"
+ aria-modal
+ >
+ <>{content}>
+
+ )
+
+ const { rerender } = render(
+ Initial content
+ )
+ const initialId = screen.getByTestId('floating-box-content').id
+ rerender(Updated content)
+
+ expect(screen.getByTestId('floating-box-content')).toHaveAttribute(
+ 'id',
+ initialId
+ )
})
From 475106a8d2ae14fa22cad59a23763e6c53887d3b Mon Sep 17 00:00:00 2001
From: thyhjwb6 <56078793+thyhjwb6@users.noreply.github.com>
Date: Tue, 17 Dec 2024 16:21:16 +0000
Subject: [PATCH 2/4] test(FloatingBox): Test box position
---
.../FloatingBox/FloatingBox.test.tsx | 41 ++++++++++++++++++-
1 file changed, 40 insertions(+), 1 deletion(-)
diff --git a/packages/react-component-library/src/primitives/FloatingBox/FloatingBox.test.tsx b/packages/react-component-library/src/primitives/FloatingBox/FloatingBox.test.tsx
index f3c5f04e9a..67e96c2be0 100644
--- a/packages/react-component-library/src/primitives/FloatingBox/FloatingBox.test.tsx
+++ b/packages/react-component-library/src/primitives/FloatingBox/FloatingBox.test.tsx
@@ -1,7 +1,7 @@
import React from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
-import { render, screen } from '@testing-library/react'
+import { render, screen, waitFor } from '@testing-library/react'
import { FloatingBox } from '.'
import { useStatefulRef } from '../../hooks/useStatefulRef'
@@ -77,3 +77,42 @@ it('does not generate a new content `id` when the content changes', () => {
initialId
)
})
+
+it('sets the position of the floating box', async () => {
+ const offsetHeight = 100
+
+ jest.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue({
+ top: 0,
+ left: 0,
+ right: 100,
+ bottom: 100,
+ width: 100,
+ height: 100,
+ x: 0,
+ y: 0,
+ toJSON() {
+ return this
+ },
+ })
+
+ Object.defineProperty(HTMLElement.prototype, 'offsetHeight', {
+ value: offsetHeight,
+ })
+
+ render(
+ Hello, World!}
+ placement="bottom"
+ aria-modal
+ >
+ This is some arbitrary JSX
+
+ )
+
+ await waitFor(() => {
+ expect(screen.getByTestId('floating-box')).toHaveStyle({
+ transform: `translate(0px, ${offsetHeight}px)`,
+ })
+ })
+})
From a0933553bb673e2d6926c6a336e43db125a82217 Mon Sep 17 00:00:00 2001
From: thyhjwb6 <56078793+thyhjwb6@users.noreply.github.com>
Date: Thu, 19 Dec 2024 10:40:27 +0000
Subject: [PATCH 3/4] fix(FloatingBox): Move setting of refs
When using React 19, ref is a prop and was being overwritten with {...rest}.
---
.../FloatingBox/FloatingBox.test.tsx | 40 +++++
.../primitives/FloatingBox/FloatingBox.tsx | 159 ++++++++++--------
2 files changed, 129 insertions(+), 70 deletions(-)
diff --git a/packages/react-component-library/src/primitives/FloatingBox/FloatingBox.test.tsx b/packages/react-component-library/src/primitives/FloatingBox/FloatingBox.test.tsx
index 67e96c2be0..b6dfdf9abe 100644
--- a/packages/react-component-library/src/primitives/FloatingBox/FloatingBox.test.tsx
+++ b/packages/react-component-library/src/primitives/FloatingBox/FloatingBox.test.tsx
@@ -116,3 +116,43 @@ it('sets the position of the floating box', async () => {
})
})
})
+
+it('sets the position of the floating box when the ref is null', async () => {
+ const offsetHeight = 100
+
+ jest.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue({
+ top: 0,
+ left: 0,
+ right: 100,
+ bottom: 100,
+ width: 100,
+ height: 100,
+ x: 0,
+ y: 0,
+ toJSON() {
+ return this
+ },
+ })
+
+ Object.defineProperty(HTMLElement.prototype, 'offsetHeight', {
+ value: offsetHeight,
+ })
+
+ render(
+ Hello, World!}
+ placement="bottom"
+ aria-modal
+ ref={null}
+ >
+ This is some arbitrary JSX
+
+ )
+
+ await waitFor(() => {
+ expect(screen.getByTestId('floating-box')).toHaveStyle({
+ transform: `translate(0px, ${offsetHeight}px)`,
+ })
+ })
+})
diff --git a/packages/react-component-library/src/primitives/FloatingBox/FloatingBox.tsx b/packages/react-component-library/src/primitives/FloatingBox/FloatingBox.tsx
index a24f20d18a..91ab75abad 100644
--- a/packages/react-component-library/src/primitives/FloatingBox/FloatingBox.tsx
+++ b/packages/react-component-library/src/primitives/FloatingBox/FloatingBox.tsx
@@ -1,4 +1,4 @@
-import React, { useRef } from 'react'
+import React, { forwardRef, useRef } from 'react'
import { BasePlacement, Placement, VariationPlacement } from '@popperjs/core'
import { Transition } from 'react-transition-group'
import mergeRefs from 'react-merge-refs'
@@ -52,79 +52,98 @@ export type FloatingBoxProps =
| FloatingBoxWithExternalTargetProps
| FloatingBoxWithEmbeddedTargetProps
-export const FloatingBox = ({
- contentId: externalContentId,
- scheme = FLOATING_BOX_SCHEME.LIGHT,
- onMouseEnter,
- onMouseLeave,
- children,
- renderTarget,
- targetElement,
- isVisible,
- placement = 'auto',
- role = 'dialog',
- ...rest
-}: FloatingBoxProps) => {
- const nodeRef = useRef(null)
- const contentId = useExternalId('floating-box', externalContentId)
- const {
- targetElementRef,
- floatingElementRef,
- arrowElementRef,
- styles,
- attributes,
- } = useFloatingElement(placement, undefined, targetElement)
+// forwardRef` is being used to replicate an issue when using React 19, because
+// `ref` is a prop. When upgrading, `forwardRef` can be removed.
+// https://react.dev/blog/2024/12/05/react-19#ref-as-a-prop
+// https://github.com/Royal-Navy/design-system/issues/3969
+export const FloatingBox = forwardRef(
+ (
+ {
+ contentId: externalContentId,
+ scheme = FLOATING_BOX_SCHEME.LIGHT,
+ onMouseEnter,
+ onMouseLeave,
+ children,
+ renderTarget,
+ targetElement,
+ isVisible,
+ placement = 'auto',
+ role = 'dialog',
+ ...rest
+ },
+ ref
+ ) => {
+ const nodeRef = useRef(null)
+ const contentId = useExternalId('floating-box', externalContentId)
+ const {
+ targetElementRef,
+ floatingElementRef,
+ arrowElementRef,
+ styles,
+ attributes,
+ } = useFloatingElement(placement, undefined, targetElement)
- const calculatedPlacement = attributes?.popper?.['data-popper-placement'] as
- | BasePlacement
- | VariationPlacement
- | undefined
+ const calculatedPlacement = attributes?.popper?.[
+ 'data-popper-placement'
+ ] as BasePlacement | VariationPlacement | undefined
- const basePlacement = calculatedPlacement?.split('-', 1)?.[0] as
- | BasePlacement
- | undefined
+ const basePlacement = calculatedPlacement?.split('-', 1)?.[0] as
+ | BasePlacement
+ | undefined
- return (
- <>
- {renderTarget && (
-
- {renderTarget}
-
- )}
-
- {(state) => (
-
+ {renderTarget && (
+
-
-
- {children}
-
-
+ {renderTarget}
+
)}
-
- >
- )
-}
+
+ {(state) => (
+
+
+
+ {children}
+
+
+ )}
+
+ >
+ )
+ }
+)
FloatingBox.displayName = 'FloatingBox'
From 00fe246c58f76af034f2e7d84be61598d2801e9d Mon Sep 17 00:00:00 2001
From: thyhjwb6 <56078793+thyhjwb6@users.noreply.github.com>
Date: Thu, 19 Dec 2024 11:02:52 +0000
Subject: [PATCH 4/4] test(FloatingBox): Reuse common setup
Setup functions can be safely abstracted and reused across tests.
---
.../FloatingBox/FloatingBox.test.tsx | 88 +++++++------------
1 file changed, 30 insertions(+), 58 deletions(-)
diff --git a/packages/react-component-library/src/primitives/FloatingBox/FloatingBox.test.tsx b/packages/react-component-library/src/primitives/FloatingBox/FloatingBox.test.tsx
index b6dfdf9abe..e0aa57d2a3 100644
--- a/packages/react-component-library/src/primitives/FloatingBox/FloatingBox.test.tsx
+++ b/packages/react-component-library/src/primitives/FloatingBox/FloatingBox.test.tsx
@@ -6,10 +6,31 @@ import { render, screen, waitFor } from '@testing-library/react'
import { FloatingBox } from '.'
import { useStatefulRef } from '../../hooks/useStatefulRef'
-it('renders the floating box when provided with a `renderTarget` and arbitrary JSX content', () => {
+function setupOffsetHeightMock(offsetHeight: number) {
+ jest.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue({
+ top: 0,
+ left: 0,
+ right: 100,
+ bottom: 100,
+ width: 100,
+ height: 100,
+ x: 0,
+ y: 0,
+ toJSON() {
+ return this
+ },
+ })
+
+ Object.defineProperty(HTMLElement.prototype, 'offsetHeight', {
+ value: offsetHeight,
+ })
+}
+
+function setup() {
render(
Hello, World!}
role="dialog"
aria-modal
@@ -17,6 +38,10 @@ it('renders the floating box when provided with a `renderTarget` and arbitrary J
This is some arbitrary JSX
)
+}
+
+it('renders the floating box when provided with a `renderTarget` and arbitrary JSX content', () => {
+ setup()
expect(screen.getByTestId('floating-box')).toHaveAttribute('role', 'dialog')
expect(screen.getByTestId('floating-box')).toHaveAttribute('aria-modal')
@@ -81,34 +106,8 @@ it('does not generate a new content `id` when the content changes', () => {
it('sets the position of the floating box', async () => {
const offsetHeight = 100
- jest.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue({
- top: 0,
- left: 0,
- right: 100,
- bottom: 100,
- width: 100,
- height: 100,
- x: 0,
- y: 0,
- toJSON() {
- return this
- },
- })
-
- Object.defineProperty(HTMLElement.prototype, 'offsetHeight', {
- value: offsetHeight,
- })
-
- render(
- Hello, World!}
- placement="bottom"
- aria-modal
- >
- This is some arbitrary JSX
-
- )
+ setupOffsetHeightMock(offsetHeight)
+ setup()
await waitFor(() => {
expect(screen.getByTestId('floating-box')).toHaveStyle({
@@ -120,35 +119,8 @@ it('sets the position of the floating box', async () => {
it('sets the position of the floating box when the ref is null', async () => {
const offsetHeight = 100
- jest.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue({
- top: 0,
- left: 0,
- right: 100,
- bottom: 100,
- width: 100,
- height: 100,
- x: 0,
- y: 0,
- toJSON() {
- return this
- },
- })
-
- Object.defineProperty(HTMLElement.prototype, 'offsetHeight', {
- value: offsetHeight,
- })
-
- render(
- Hello, World!}
- placement="bottom"
- aria-modal
- ref={null}
- >
- This is some arbitrary JSX
-
- )
+ setupOffsetHeightMock(offsetHeight)
+ setup()
await waitFor(() => {
expect(screen.getByTestId('floating-box')).toHaveStyle({