Skip to content

Commit

Permalink
Merge pull request #6442 from uktrade/fix/default-layout
Browse files Browse the repository at this point in the history
Allow pageTitle prop of DefaultLayout to be a JSX expression
  • Loading branch information
peterhudec authored Jan 22, 2024
2 parents e0aaf4b + 729e68e commit 14a719c
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 5 deletions.
16 changes: 11 additions & 5 deletions src/client/components/Layout/DefaultLayout.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'
import React, { useState } from 'react'
import { createGlobalStyle } from 'styled-components'
import PropTypes from 'prop-types'
import GridCol from '@govuk-react/grid-col'
Expand All @@ -8,6 +8,7 @@ import Footer from '../Footer'
import Main from '../Main'
import LocalHeader from '../LocalHeader/LocalHeader'
import DataHubHeader from '../DataHubHeader'
import WatchTextContent from '../WatchTextContent'

const GlobalStyles = createGlobalStyle`
*, *:before, *:after {
Expand All @@ -27,11 +28,16 @@ const DefaultLayout = ({
localHeaderChildren,
}) => {
const [showVerticalNav, setShowVerticalNav] = useState(false)
useEffect(() => {
document.title = `${pageTitle} - DBT Data Hub`
}, [pageTitle])

return (
<>
<WatchTextContent
onTextContentChange={(text) => {
document.title = text
}}
>
{pageTitle} - DBT Data Hub
</WatchTextContent>
<GlobalStyles />
<DataHubHeader
showVerticalNav={showVerticalNav}
Expand Down Expand Up @@ -66,7 +72,7 @@ DefaultLayout.propTypes = {
text: PropTypes.string.isRequired,
}),
subheading: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
pageTitle: PropTypes.string.isRequired,
pageTitle: PropTypes.node.isRequired,
children: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
}

Expand Down
39 changes: 39 additions & 0 deletions src/client/components/WatchTextContent.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useEffect, useRef } from 'react'
import ReactDOM from 'react-dom'

/**
* @function WatchTextContent
* @description Calls {onTextContentChange} anytime the `textContent`
* of {children} changes. The {children} won't be rendered.
* @param {Object} props
* @param {React.Children} props.children - Children
* @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`.
*/
const WatchTextContent = ({ onTextContentChange, ...props }) => {
const ref = useRef()
const previousTextContent = useRef()

useEffect(() => {
ref.current = document.createElement('div')
return () => {
ref.current.remove()
}
}, [])

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

return null
}

export default WatchTextContent
53 changes: 53 additions & 0 deletions test/component/cypress/specs/WatchTextContent.cy.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React, { useState } from 'react'

import WatchTextContent from '../../../../src/client/components/WatchTextContent.jsx'

const Counter = ({ children }) => {
const [count, setCount] = useState(0)
return (
<>
<button className="counter" onClick={() => setCount((x) => x + 1)}>
increase
</button>
{children(count)}
</>
)
}

describe('WatchTextContent', () => {
it("calls onTextContentChange when it's text content changes", () => {
const onTextContentChange = cy.stub()

cy.mount(
<Counter>
{(count) => (
<>
count: {count}
<WatchTextContent onTextContentChange={onTextContentChange}>
<h1>Heading</h1>
<ul>
<li>foo</li>
<li>bar</li>
<li>{count}</li>
<li>baz</li>
</ul>
</WatchTextContent>
</>
)}
</Counter>
)

cy.get('.counter').click()
cy.get('.counter').click()
cy.get('.counter')
.click()
.then(() => {
expect(onTextContentChange.args).to.deep.eq([
['Headingfoobar0baz'],
['Headingfoobar1baz'],
['Headingfoobar2baz'],
['Headingfoobar3baz'],
])
})
})
})

0 comments on commit 14a719c

Please sign in to comment.