Skip to content

Commit 9bba808

Browse files
ElliotLuke BowermanLuke Bowerman
authored
Action List: Selecting test suite (#800)
* Minor checkbox updates - Clicking on checkbox container of disabled row now does not select row - Added on key down for checkbox * Added ActionListCheckbox test - Resolved bug where clicking on checkbox fired both container onChange and checkbox onChange * Started select test suite: * Realigned header with items * Added css properties to assure header stays aligned with items - When shrinking the window, ActionList items and header became misaligned; added overflow: hidden on header column content and flex-basis/flex-shrink so that header follows items shrink behavior * Added selecting test suite - Modified ActionList docs to respect ActionListItem.id string type * Minor addition to sorting documentation - Minor addition discusses what to do when data initially loads with a sort * Added CHANGELOG updates * PR Feedback Co-authored-by: Luke Bowerman <[email protected]> Co-authored-by: Luke Bowerman <[email protected]>
1 parent b6ae8bf commit 9bba808

File tree

9 files changed

+137
-43
lines changed

9 files changed

+137
-43
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- `ActionList` documentation and select test suite
13+
- `ActionListCheckbox` test suite
1214
- `ActionList` documentation
1315
- `CrossFilter` Icon
1416
- `Checkbox` & `Radio` now support `readOnly` property

packages/components/src/ActionList/ActionList.test.tsx

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ const columns: ActionListColumns = [
6161

6262
const data = [
6363
{
64-
id: '1',
64+
id: 1,
6565
name: 'Richard Garfield',
6666
type: 'Game Designer',
6767
},
@@ -83,7 +83,7 @@ const items = data.map(({ id, name, type }) => {
8383
)
8484

8585
return (
86-
<ActionListItem key={id} id={id} actions={availableActions}>
86+
<ActionListItem key={id} id={String(id)} actions={availableActions}>
8787
<ActionListItemColumn>{id}</ActionListItemColumn>
8888
<ActionListItemColumn>{name}</ActionListItemColumn>
8989
<ActionListItemColumn>{type}</ActionListItemColumn>
@@ -178,7 +178,7 @@ describe('ActionList', () => {
178178

179179
return (
180180
<ActionListItem
181-
id={id}
181+
id={String(id)}
182182
key={id}
183183
actions={availableActions}
184184
onClick={handleListItemClick}
@@ -231,14 +231,16 @@ describe('ActionList', () => {
231231
</ActionList>
232232
)
233233

234+
afterEach(() => {
235+
onSort.mockClear()
236+
})
237+
234238
test('Calls onSort if canSort property is true', () => {
235239
const { getByText } = renderWithTheme(actionListWithSort)
236240

237241
const idColumnHeader = getByText('ID')
238242
fireEvent.click(idColumnHeader)
239243
expect(onSort.mock.calls.length).toBe(1)
240-
241-
onSort.mockClear()
242244
})
243245

244246
test('Does not call onSort if canSort property is false', () => {
@@ -247,14 +249,57 @@ describe('ActionList', () => {
247249
const nameColumnHeader = getByText('Name')
248250
fireEvent.click(nameColumnHeader)
249251
expect(onSort.mock.calls.length).toBe(0)
250-
251-
onSort.mockClear()
252252
})
253253
})
254254

255-
xdescribe('Selecting', () => {
256-
xtest('Select tests needed', () => {
257-
return null
255+
describe.only('Selecting', () => {
256+
const onSelect = jest.fn()
257+
const actionListWithSelect = (
258+
<ActionList columns={columns} canSelect onSelect={onSelect}>
259+
{items}
260+
</ActionList>
261+
)
262+
const actionListWithItemsSelected = (
263+
<ActionList
264+
columns={columns}
265+
canSelect
266+
itemsSelected={['1']}
267+
onSelect={onSelect}
268+
>
269+
{items}
270+
</ActionList>
271+
)
272+
const onClickRowSelect = (
273+
<ActionList
274+
columns={columns}
275+
canSelect
276+
onSelect={onSelect}
277+
onClickRowSelect
278+
>
279+
{items}
280+
</ActionList>
281+
)
282+
afterEach(() => {
283+
onSelect.mockClear()
284+
})
285+
286+
test('Checkbox click calls onSelect', () => {
287+
const { getByRole } = renderWithTheme(actionListWithSelect)
288+
fireEvent.click(getByRole('checkbox'))
289+
expect(onSelect).toHaveBeenCalledTimes(1)
290+
})
291+
292+
test('Row click calls onSelect when onClickRowSelect is true', () => {
293+
const { getByText } = renderWithTheme(onClickRowSelect)
294+
const nameCell = getByText('Richard Garfield')
295+
fireEvent.click(nameCell)
296+
expect(onSelect).toHaveBeenCalledTimes(1)
297+
})
298+
299+
test('itemsSelected determines if a checkbox is checked', () => {
300+
const { getByRole } = renderWithTheme(actionListWithItemsSelected)
301+
const checkbox = getByRole('checkbox')
302+
expect((checkbox as HTMLInputElement).checked).toEqual(true)
258303
})
259304
})
260305
})

packages/components/src/ActionList/ActionList.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
} from './ActionListHeader'
3333
import { ActionListItemColumn } from './ActionListItemColumn'
3434
import { ActionListRowColumns } from './ActionListRow'
35+
import { actionListCheckboxWidth } from './ActionListCheckbox'
3536
import { ActionListContext } from './ActionListContext'
3637
import { ActionListHeaderColumn } from './ActionListHeader/ActionListHeaderColumn'
3738
import {
@@ -150,11 +151,16 @@ export const ActionList = styled(ActionListLayout)<ActionListProps>`
150151
grid-template-columns: ${(props) =>
151152
props.columns.map((column) => `${column.widthPercent}%`).join(' ')};
152153
align-items: center;
154+
}
155+
156+
${/* sc-selector */ ActionListItemColumn}:first-child {
157+
padding-left: ${({ canSelect, theme }) =>
158+
canSelect ? theme.space.none : undefined};
159+
}
153160
154-
${/* sc-selector */ ActionListItemColumn}:first-child {
155-
padding-left: ${({ canSelect, theme }) =>
156-
canSelect ? theme.space.none : undefined};
157-
}
161+
${/* sc-selector */ ActionListHeaderColumn}:first-child {
162+
padding-left: ${({ canSelect, theme }) =>
163+
canSelect ? theme.space.none : undefined};
158164
}
159165
160166
${/* sc-selector */ ActionListItemColumn},
@@ -164,7 +170,8 @@ export const ActionList = styled(ActionListLayout)<ActionListProps>`
164170
}
165171
166172
${ActionListHeader} {
167-
padding-left: ${({ canSelect }) => (canSelect ? '2.75rem' : undefined)};
173+
padding-left: ${({ canSelect }) =>
174+
canSelect ? actionListCheckboxWidth : undefined};
168175
}
169176
170177
${(props) => numericColumnCSS(getNumericColumnIndices(props.columns))}

packages/components/src/ActionList/ActionListCheckbox.test.tsx

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,39 @@
2424
2525
*/
2626

27+
import React from 'react'
28+
import { renderWithTheme } from '@looker/components-test-utils'
29+
import { fireEvent } from '@testing-library/react'
30+
import { ActionListCheckbox } from './ActionListCheckbox'
31+
2732
describe('ActionListCheckbox', () => {
28-
xtest('Renders checked', () => {
29-
return null
33+
test('Renders checked', () => {
34+
const { getByRole } = renderWithTheme(<ActionListCheckbox checked />)
35+
const actionListCheckbox = getByRole('checkbox')
36+
expect((actionListCheckbox as HTMLInputElement).checked).toEqual(true)
3037
})
3138

32-
xtest('Renders unchecked', () => {
33-
return null
39+
test('Renders unchecked', () => {
40+
const { getByRole } = renderWithTheme(<ActionListCheckbox />)
41+
const actionListCheckbox = getByRole('checkbox')
42+
expect((actionListCheckbox as HTMLInputElement).checked).toEqual(false)
3443
})
3544

36-
xtest('Renders disable', () => {
37-
return null
45+
test('Renders disabled', () => {
46+
const { getByRole } = renderWithTheme(<ActionListCheckbox disabled />)
47+
const actionListCheckbox = getByRole('checkbox')
48+
expect((actionListCheckbox as HTMLInputElement).disabled).toEqual(true)
3849
})
3950

40-
xtest('Calls onChange callback', () => {
41-
return null
51+
test('Calls onChange callback', () => {
52+
const onChange = jest.fn()
53+
const { getByRole } = renderWithTheme(
54+
<ActionListCheckbox onChange={onChange} />
55+
)
56+
const actionListCheckbox = getByRole('checkbox')
57+
fireEvent.click(actionListCheckbox)
58+
expect(onChange).toHaveBeenCalledTimes(1)
59+
fireEvent.click(actionListCheckbox)
60+
expect(onChange).toHaveBeenCalledTimes(2)
4261
})
4362
})

packages/components/src/ActionList/ActionListCheckbox.tsx

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,26 +37,43 @@ export interface ActionListCheckboxProps {
3737

3838
export const checkListProps = ['checked', 'disabled', 'onChange']
3939

40+
export const actionListCheckboxWidth = '2.75rem'
41+
4042
const ActionListCheckboxLayout: FC<ActionListCheckboxProps> = ({
4143
onChange,
4244
checked,
4345
disabled,
4446
className,
45-
}) => (
46-
<div onClick={onChange} className={className}>
47-
<Checkbox
48-
disabled={disabled}
49-
checked={checked}
50-
onChange={disabled ? undefined : onChange}
51-
/>
52-
</div>
53-
)
47+
}) => {
48+
const handleOnChange = (event: React.MouseEvent<HTMLInputElement>) => {
49+
const isNotFromChild = event.currentTarget === event.target
50+
isNotFromChild && !disabled && onChange && onChange()
51+
}
52+
53+
const handleOnKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
54+
if (event.keyCode === 13) {
55+
event.currentTarget.click()
56+
}
57+
}
58+
59+
return (
60+
<div onClick={handleOnChange} className={className}>
61+
<Checkbox
62+
disabled={disabled}
63+
checked={checked}
64+
onChange={handleOnChange}
65+
onKeyDown={handleOnKeyDown}
66+
/>
67+
</div>
68+
)
69+
}
5470

5571
export const ActionListCheckbox = styled(ActionListCheckboxLayout)`
5672
align-items: center;
5773
display: flex;
5874
justify-content: center;
59-
width: 3.5rem;
75+
flex-basis: ${actionListCheckboxWidth};
76+
flex-shrink: 0;
6077
6178
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'default')};
6279
`

packages/components/src/ActionList/ActionListHeader/ActionListHeaderColumn.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,5 @@ ActionListHeaderColumnLayout.displayName = 'ActionListHeaderColumnLayout'
7474
export const ActionListHeaderColumn = styled(ActionListHeaderColumnLayout)`
7575
display: flex;
7676
align-items: center;
77+
word-break: break-all;
7778
`

packages/components/src/ActionList/ActionListItemColumn.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export const ActionListItemColumn = styled(ActionListItemColumnLayout)<
6565
color: ${(props) => props.theme.colors.palette.charcoal700};
6666
display: ${(props) => (props.indicator ? 'flex' : undefined)};
6767
font-size: ${(props) => props.theme.fontSizes.xsmall};
68-
overflow: hidden;
68+
word-break: break-all;
6969
7070
${ActionListItemColumnInnerLayout} {
7171
display: flex;

packages/playground/src/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@
2727
import React from 'react'
2828
import ReactDOM from 'react-dom'
2929
import { ComponentsProvider } from '@looker/components'
30-
import { FieldsDemo } from './Form/FieldsDemo'
30+
import { ActionListDemo } from './ActionList/ActionListDemo'
3131

3232
const App: React.FC = () => {
3333
return (
3434
<ComponentsProvider>
35-
<FieldsDemo />
35+
<ActionListDemo />
3636
</ComponentsProvider>
3737
)
3838
}

packages/www/src/documentation/components/content/action-list.mdx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -166,16 +166,18 @@ You can also use an `<ActionListManager>` element to render a "No Results" messa
166166

167167
## Sorting an Action List
168168

169-
You can implement sorting on an `<ActionList>` by passing a function into the optional `onSort` prop. Note that only columns whose corresponding column objects have `canSort: true` will be sortable.
169+
You can implement sorting on an `<ActionList>` by passing a function into the optional `onSort` prop.
170170

171171
Clicking on the column header will lead to an `onSort` call, with (1) the column's id and (2) the next sort direction, passed in as arguments. The sort direction argument will either be a string 'desc' if the current sort direction of the column is 'asc', or 'asc' in all other cases.
172172

173+
**Note:** Only columns whose corresponding column objects have `canSort: true` will be sortable. In addition, if your data is initially sorted on a specific column, you may want to default the corresponding column object with a `sortDirection` property.
174+
173175
The function passed into `onSort`, generally speaking, should perform these actions for proper sorting behavior:
174176

175-
- Update the corresponding column object to have a new sort direction
177+
- Update the corresponding column object's `sortDirection`
176178
- Sort the collection representing your data
177179

178-
**Note:** If you want default sorting behavior, you can use the `doDefaultActionListSort` helper function. The `doDefaultActionListSort` helper function will return a sorted data array as well as an updated columns array, which can then be used to replace your existing arrays post-sort.
180+
If you want default sorting behavior, you can use the `doDefaultActionListSort` helper function. The `doDefaultActionListSort` helper function will return a sorted data array as well as an updated columns array, which can then be used to replace your existing arrays post-sort.
179181

180182
```jsx
181183
;() => {
@@ -201,6 +203,7 @@ The function passed into `onSort`, generally speaking, should perform these acti
201203
primaryKey: true,
202204
title: 'ID',
203205
type: 'number',
206+
sortDirection: 'asc',
204207
widthPercent: 20,
205208
},
206209
{
@@ -259,13 +262,13 @@ An `<ActionList/>` with the `canSelect` prop passed in will display `<Checkbox/>
259262

260263
Clicking on a `<Checkbox/>` will trigger the `onSelect` callback prop, with the id of the clicked `<ActionListItem/>` passed in as an argument.
261264

262-
The `itemsSelected` prop should be given the id's of all selected rows.
265+
The `itemsSelected` prop should be given the id's of all selected `<ActionListItem>`s. Note that because the id of an `<ActionListItem>`s must be a string, the `itemSelected` prop should be an array of strings.
263266

264-
**Note:** If you would like a row's `onClick` to toggle a row's selected state, rather than have a custom-defined `onClick`, you can do so by passing in the `onClickRowSelect` prop into the `<ActionList/>`.
267+
If you would like a row's `onClick` to toggle a row's selected state, rather than have a custom-defined `onClick`, you can do so by passing in the `onClickRowSelect` prop into the `<ActionList/>`.
265268

266269
```jsx
267270
;() => {
268-
const [selections, setSelections] = useState([2])
271+
const [selections, setSelections] = useState([])
269272
const onSelect = (selection) => {
270273
setSelections(
271274
selections.includes(selection)
@@ -317,7 +320,7 @@ The `itemsSelected` prop should be given the id's of all selected rows.
317320
return (
318321
<ActionListItem
319322
key={id}
320-
id={id}
323+
id={String(id)}
321324
onClick={() => alert('Row clicked')}
322325
actions={actions}
323326
disabled={id === 3 && true}

0 commit comments

Comments
 (0)