Skip to content
Merged
495 changes: 399 additions & 96 deletions demo/js/esri-datasets.js

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,11 @@
}

// GovUK style overide
.im-c-datasets-layers__item .govuk-checkboxes__item {
.im-c-datasets-layers__item .govuk-checkboxes__item,
.im-c-datasets-layers__item .govuk-radios__item {
flex-wrap: nowrap;

.govuk-checkboxes__input {
.govuk-checkboxes__input, .govuk-radios__input {
margin-left: -3px;
}
}
Expand Down
41 changes: 29 additions & 12 deletions plugins/beta/datasets/src/components/LayersMenu/LayersMenu.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React from 'react'
import { setDatasetVisibility } from '../../api/setDatasetVisibility.js'
import { LayersMenuCheckbox } from './LayersMenuCheckbox.jsx'
import { LayersRadioGroupWrapper } from './LayersRadioGroupWrapper.jsx'
import { LayersMenuGroupWrapper } from './LayersMenuGroupWrapper.jsx'

export const LayersMenu = ({ pluginState }) => {
const { menu = [] } = pluginState

const handleDatasetChange = (e) => {
const { value, checked } = e.target
setDatasetVisibility({ pluginState }, checked, { datasetId: value })
Expand All @@ -15,18 +17,33 @@ export const LayersMenu = ({ pluginState }) => {
return (
<div className={containerClass}>
{// Each menuGroup
menu.map(menuGroup =>
<LayersMenuGroupWrapper key={menuGroup.id} menuGroup={menuGroup}>
{// Each menuGroupItem
menuGroup.items.map(menuGroupItem =>
<LayersMenuCheckbox
key={menuGroupItem.id}
menuGroupItem={menuGroupItem}
onChange={handleDatasetChange}
/>
)
}
</LayersMenuGroupWrapper>)
menu.map(menuGroup => {
const { type } = menuGroup
if (type === 'checkbox') {
return (
<LayersMenuGroupWrapper key={menuGroup.id} menuGroup={menuGroup}> {
// Each menuGroupItem
menuGroup.items.map(menuGroupItem => (
<LayersMenuCheckbox
key={menuGroupItem.id}
menuGroupItem={menuGroupItem}
onChange={handleDatasetChange}
/>)
)
}
</LayersMenuGroupWrapper>
)
} else {
return (
<LayersRadioGroupWrapper
key={menuGroup.id}
menuGroup={menuGroup}
pluginState={pluginState}
/>
)
}
}
)
}
</div>
)
Expand Down
175 changes: 175 additions & 0 deletions plugins/beta/datasets/src/components/LayersMenu/LayersMenu.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { render } from '@testing-library/react'
import { LayersMenu } from './LayersMenu'
import { setDatasetVisibility } from '../../api/setDatasetVisibility.js'

jest.mock('../../api/setDatasetVisibility.js', () => ({
setDatasetVisibility: jest.fn()
}))

let capturedOnChange = null

jest.mock('./LayersMenuCheckbox.jsx', () => ({
LayersMenuCheckbox: ({ menuGroupItem, onChange }) => {
capturedOnChange = onChange
return (
<div
data-testid='layers-menu-checkbox'
data-item-id={menuGroupItem.id}
/>
)
}
}))

jest.mock('./LayersRadioGroupWrapper.jsx', () => ({
LayersRadioGroupWrapper: ({ menuGroup }) => (
<div data-testid='layers-radio-group-wrapper' data-group-id={menuGroup.id} />
)
}))

jest.mock('./LayersMenuGroupWrapper.jsx', () => ({
LayersMenuGroupWrapper: ({ menuGroup, children }) => (
<div data-testid='layers-menu-group-wrapper' data-group-id={menuGroup.id}>
{children}
</div>
)
}))

const makePluginState = (menu = [], extra = {}) => ({
menu,
dispatch: jest.fn(),
...extra
})

describe('LayersMenu', () => {
beforeEach(() => {
setDatasetVisibility.mockClear()
})

describe('container class', () => {
it('renders the base container class when no groups have a groupLabel', () => {
const pluginState = makePluginState([{ id: 'g1', type: 'checkbox', items: [] }])
const { container } = render(<LayersMenu pluginState={pluginState} />)
const div = container.firstChild
expect(div.className).toBe('im-c-datasets-layers')
})

it('adds the --has-groups modifier when at least one group has a groupLabel', () => {
const pluginState = makePluginState([{ id: 'g1', type: 'checkbox', groupLabel: 'My Group', items: [] }])
const { container } = render(<LayersMenu pluginState={pluginState} />)
const div = container.firstChild
expect(div.className).toBe('im-c-datasets-layers im-c-datasets-layers--has-groups')
})
})

describe('empty menu', () => {
it('renders the container with no children when menu is empty', () => {
const pluginState = makePluginState([])
const { container } = render(<LayersMenu pluginState={pluginState} />)
expect(container.firstChild.children).toHaveLength(0)
})

it('uses an empty array when menu is not provided', () => {
const { container } = render(<LayersMenu pluginState={{}} />)
expect(container.firstChild.children).toHaveLength(0)
})
})

describe('checkbox groups', () => {
it('renders a LayersMenuGroupWrapper for each checkbox group', () => {
const pluginState = makePluginState([
{ id: 'g1', type: 'checkbox', items: [] },
{ id: 'g2', type: 'checkbox', items: [] }
])
const { getAllByTestId } = render(<LayersMenu pluginState={pluginState} />)
expect(getAllByTestId('layers-menu-group-wrapper')).toHaveLength(2)
})

it('renders a LayersMenuCheckbox for each item in a checkbox group', () => {
const pluginState = makePluginState([
{ id: 'g1', type: 'checkbox', items: [{ id: 'item-a' }, { id: 'item-b' }] }
])
const { getAllByTestId } = render(<LayersMenu pluginState={pluginState} />)
expect(getAllByTestId('layers-menu-checkbox')).toHaveLength(2)
})

it('passes the correct group to LayersMenuGroupWrapper', () => {
const group = { id: 'group-x', type: 'checkbox', items: [] }
const pluginState = makePluginState([group])
const { getByTestId } = render(<LayersMenu pluginState={pluginState} />)
expect(getByTestId('layers-menu-group-wrapper').dataset.groupId).toBe('group-x')
})

it('passes the correct item to LayersMenuCheckbox', () => {
const pluginState = makePluginState([
{ id: 'g1', type: 'checkbox', items: [{ id: 'item-1' }] }
])
const { getByTestId } = render(<LayersMenu pluginState={pluginState} />)
expect(getByTestId('layers-menu-checkbox').dataset.itemId).toBe('item-1')
})
})

describe('radio groups', () => {
it('renders a LayersRadioGroupWrapper for non-checkbox groups', () => {
const pluginState = makePluginState([
{ id: 'r1', type: 'radio', items: [{ id: 'opt1' }, { id: 'opt2' }] }
])
const { getAllByTestId } = render(<LayersMenu pluginState={pluginState} />)
expect(getAllByTestId('layers-radio-group-wrapper')).toHaveLength(1)
})

it('passes the correct group to LayersRadioGroupWrapper', () => {
const pluginState = makePluginState([
{ id: 'radio-group-1', type: 'radio', items: [] }
])
const { getByTestId } = render(<LayersMenu pluginState={pluginState} />)
expect(getByTestId('layers-radio-group-wrapper').dataset.groupId).toBe('radio-group-1')
})

it('does not render a LayersMenuGroupWrapper for radio groups', () => {
const pluginState = makePluginState([{ id: 'r1', type: 'radio', items: [] }])
const { queryByTestId } = render(<LayersMenu pluginState={pluginState} />)
expect(queryByTestId('layers-menu-group-wrapper')).toBeNull()
})
})

describe('mixed groups', () => {
it('renders both checkbox and radio wrappers when both types are present', () => {
const pluginState = makePluginState([
{ id: 'c1', type: 'checkbox', items: [] },
{ id: 'r1', type: 'radio', items: [] }
])
const { getByTestId } = render(<LayersMenu pluginState={pluginState} />)
expect(getByTestId('layers-menu-group-wrapper')).toBeTruthy()
expect(getByTestId('layers-radio-group-wrapper')).toBeTruthy()
})
})

describe('handleDatasetChange', () => {
it('calls setDatasetVisibility with the correct arguments when a checkbox changes', () => {
const pluginState = makePluginState([
{ id: 'g1', type: 'checkbox', items: [{ id: 'dataset-abc' }] }
])
render(<LayersMenu pluginState={pluginState} />)
capturedOnChange({ target: { value: 'dataset-abc', checked: true } })
expect(setDatasetVisibility).toHaveBeenCalledTimes(1)
expect(setDatasetVisibility).toHaveBeenCalledWith(
{ pluginState },
true,
{ datasetId: 'dataset-abc' }
)
})

it('passes checked: false to setDatasetVisibility when unchecking', () => {
const pluginState = makePluginState([
{ id: 'g1', type: 'checkbox', items: [{ id: 'dataset-xyz' }] }
])
render(<LayersMenu pluginState={pluginState} />)
capturedOnChange({ target: { value: 'dataset-xyz', checked: false } })
expect(setDatasetVisibility).toHaveBeenCalledWith(
{ pluginState },
false,
{ datasetId: 'dataset-xyz' }
)
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { render, screen, act } from '@testing-library/react'
import { datasetRegistry } from '../../registry/datasetRegistry.js'
import { LayersMenuCheckbox } from './LayersMenuCheckbox'

jest.mock('../../registry/datasetRegistry.js', () => ({
datasetRegistry: {
getDataset: jest.fn()
}
}))

const onChange = jest.fn()
const menuGroupItem = { id: 'dataset-1' }

const baseDataset = {
id: 'dataset-1',
label: 'Dataset One',
visible: true,
isLocallyVisible: true,
isSublayer: false,
parentId: undefined
}

beforeEach(() => {
datasetRegistry.getDataset.mockReset()
onChange.mockReset()
})

describe('LayersMenuCheckbox', () => {
describe('when the dataset is not in the registry', () => {
it('returns null', () => {
datasetRegistry.getDataset.mockReturnValue(undefined)
const { container } = render(<LayersMenuCheckbox menuGroupItem={menuGroupItem} onChange={onChange} />)
expect(container.firstChild).toBeNull()
})
})

describe('when the dataset exists', () => {
it('renders a checkbox input', () => {
datasetRegistry.getDataset.mockReturnValue(baseDataset)
const { container } = render(<LayersMenuCheckbox menuGroupItem={menuGroupItem} onChange={onChange} />)
expect(container.querySelector('input[type="checkbox"]')).toBeTruthy()
})

it('renders the dataset label', () => {
datasetRegistry.getDataset.mockReturnValue(baseDataset)
render(<LayersMenuCheckbox menuGroupItem={menuGroupItem} onChange={onChange} />)
expect(screen.getByText('Dataset One')).toBeTruthy()
})

it('associates the label with the input via htmlFor', () => {
datasetRegistry.getDataset.mockReturnValue(baseDataset)
const { container } = render(<LayersMenuCheckbox menuGroupItem={menuGroupItem} onChange={onChange} />)
const label = container.querySelector('label')
const input = container.querySelector('input')
expect(label.htmlFor).toBe(input.id)
})

it('calls onChange when the checkbox changes', () => {
datasetRegistry.getDataset.mockReturnValue(baseDataset)
const { container } = render(<LayersMenuCheckbox menuGroupItem={menuGroupItem} onChange={onChange} />)
const input = container.querySelector('input')
const propsKey = Object.keys(input).find(k => k.startsWith('__reactProps'))
act(() => { input[propsKey].onChange({ target: { checked: true, value: input.value } }) })
expect(onChange).toHaveBeenCalledTimes(1)
})
})

describe('checked state', () => {
it('is checked when isLocallyVisible is true', () => {
datasetRegistry.getDataset.mockReturnValue({ ...baseDataset, isLocallyVisible: true })
const { container } = render(<LayersMenuCheckbox menuGroupItem={menuGroupItem} onChange={onChange} />)
expect(container.querySelector('input').checked).toBe(true)
})

it('is unchecked when isLocallyVisible is false', () => {
datasetRegistry.getDataset.mockReturnValue({ ...baseDataset, isLocallyVisible: false })
const { container } = render(<LayersMenuCheckbox menuGroupItem={menuGroupItem} onChange={onChange} />)
expect(container.querySelector('input').checked).toBe(false)
})
})

describe('item class', () => {
it('does not include the --checked modifier when visible is true', () => {
datasetRegistry.getDataset.mockReturnValue({ ...baseDataset, visible: true })
const { container } = render(<LayersMenuCheckbox menuGroupItem={menuGroupItem} onChange={onChange} />)
expect(container.firstChild.className).not.toContain('im-c-datasets-layers__item--checked')
})

it('includes the --checked modifier when visible is false', () => {
datasetRegistry.getDataset.mockReturnValue({ ...baseDataset, visible: false })
const { container } = render(<LayersMenuCheckbox menuGroupItem={menuGroupItem} onChange={onChange} />)
expect(container.firstChild.className).toContain('im-c-datasets-layers__item--checked')
})
})

describe('data attributes', () => {
it('uses the dataset id as data-dataset-id when not a sublayer', () => {
datasetRegistry.getDataset.mockReturnValue({ ...baseDataset, isSublayer: false })
const { container } = render(<LayersMenuCheckbox menuGroupItem={menuGroupItem} onChange={onChange} />)
const input = container.querySelector('input')
expect(input.dataset.datasetId).toBe('dataset-1')
expect(input.dataset.sublayerId).toBeUndefined()
})

it('uses parentId as data-dataset-id when a sublayer', () => {
datasetRegistry.getDataset.mockReturnValue({ ...baseDataset, id: 'sub-1', isSublayer: true, parentId: 'parent-1' })
const { container } = render(<LayersMenuCheckbox menuGroupItem={menuGroupItem} onChange={onChange} />)
expect(container.querySelector('input').dataset.datasetId).toBe('parent-1')
})

it('uses the sublayer id as data-sublayer-id when a sublayer', () => {
datasetRegistry.getDataset.mockReturnValue({ ...baseDataset, id: 'sub-1', isSublayer: true, parentId: 'parent-1' })
const { container } = render(<LayersMenuCheckbox menuGroupItem={menuGroupItem} onChange={onChange} />)
expect(container.querySelector('input').dataset.sublayerId).toBe('sub-1')
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@ export const LayersMenuGroupWrapper = ({ menuGroup, children }) => {
if (!menuGroup.groupLabel) {
return <>{children}</>
}
// TODO - is there any need for im-c-datasets-layers-group--items-checked - it isn't used anywhere
// const anyItemsChecked = false
// const wrapperClass = `govuk-form-group im-c-datasets-layers-group${anyItemsChecked ? ' im-c-datasets-layers-group--items-checked' : ''}`
const wrapperClass = 'govuk-form-group im-c-datasets-layers-group'
return (
<div key={menuGroup.id} className={wrapperClass}>
Expand Down
Loading
Loading