Skip to content

Commit

Permalink
Merge pull request #6463 from uktrade/fix/watch-text-content
Browse files Browse the repository at this point in the history
Fixed a bug when WatchTextContent didnt work with a routed component
  • Loading branch information
peterhudec authored Jan 26, 2024
2 parents 46257e9 + 600973f commit 761dbe5
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 17 deletions.
49 changes: 33 additions & 16 deletions src/client/components/WatchTextContent.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { useEffect, useRef } from 'react'
import ReactDOM from 'react-dom'
import React, { useEffect, useRef } from 'react'

/**
* @function WatchTextContent
* @description Calls {onTextContentChange} anytime the `textContent`
* of {children} changes. The {children} won't be rendered.
* of {children} changes. The {children} will be rendered hidden.
* @param {Object} props
* @param {React.Children} props.children - Children
* @param {React.Children} props.children - Vdom to whose changes the component subscribes to.
* It will be rendered in a hidden div.
* @param {(textContent: string) => void} props.onTextContentChange - A callback that will
* be called anytime the `textContent` of this component changes with the value
* of the current `textContent`.
Expand All @@ -16,24 +16,41 @@ const WatchTextContent = ({ onTextContentChange, ...props }) => {
const previousTextContent = useRef()

useEffect(() => {
ref.current = document.createElement('div')
return () => {
ref.current.remove()
}
}, [])
// Detach the element so that the content is not searchable by Cypress tests.
// This is the only way to make an element really hidden from Cypress.
ref.current.remove()

useEffect(() => {
ReactDOM.render(props.children, ref.current)
// The most recent update only takes effect in the next event tick
setTimeout(() => {
onTextContentChange(ref.current.textContent)
previousTextContent.current = ref.current.textContent

const observer = new MutationObserver(() => {
if (ref.current.textContent !== previousTextContent.current) {
onTextContentChange(ref.current.textContent)
previousTextContent.current = ref.current.textContent
}
}, 0)
})
})

if (ref.current) {
observer.observe(ref.current, {
childList: true,
subtree: true,
attributes: true,
characterData: true,
})
}

return () => {
observer.disconnect()
}
}, [])

return null
return (
// This wrapper is necessary because if we remove topmost element returned from render,
// React would throw a NotFoundError.
<div hidden={true}>
<div ref={ref} {...props} />
</div>
)
}

export default WatchTextContent
29 changes: 28 additions & 1 deletion test/component/cypress/specs/WatchTextContent.cy.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,32 @@ const Counter = ({ children }) => {
}

describe('WatchTextContent', () => {
it("calls onTextContentChange when it's text content changes", () => {
it('can be unmounted', () => {
const onTextContentChange = cy.stub()
cy.mount(
<>
First render
<WatchTextContent onTextContentChange={onTextContentChange}>
<ul>
<li>foo</li>
<li>bar</li>
<li>baz</li>
</ul>
</WatchTextContent>
</>
)

cy.contains('First render').then(() =>
expect(onTextContentChange.args).to.deep.eq([['foobarbaz']])
)

cy.mount(<div>Second render</div>)
cy.contains('First render').should('not.exist')
cy.contains('Second render')
})

it("calls onTextContentChange when it's text content changes", () => {
const onTextContentChange = cy.stub()
cy.mount(
<Counter>
{(count) => (
Expand All @@ -37,11 +60,15 @@ describe('WatchTextContent', () => {
</Counter>
)

cy.contains('Headingfoobar0baz').should('not.exist')
cy.get('.counter').click()
cy.contains('Headingfoobar1baz').should('not.exist')
cy.get('.counter').click()
cy.contains('Headingfoobar2baz').should('not.exist')
cy.get('.counter')
.click()
.then(() => {
cy.contains('Headingfoobar3baz').should('not.exist')
expect(onTextContentChange.args).to.deep.eq([
['Headingfoobar0baz'],
['Headingfoobar1baz'],
Expand Down

0 comments on commit 761dbe5

Please sign in to comment.