Skip to content

Commit 540c4c5

Browse files
authored
fix: Make create form keyboard accessible COMPASS-9335 (#6896)
1 parent 077a0fc commit 540c4c5

File tree

8 files changed

+191
-179
lines changed

8 files changed

+191
-179
lines changed

packages/compass-components/src/components/select-table.spec.tsx renamed to packages/compass-components/src/components/select-list.spec.tsx

Lines changed: 23 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
import { expect } from 'chai';
88
import React from 'react';
99
import sinon from 'sinon';
10-
import { SelectTable } from './select-table';
10+
import { SelectList } from './select-list';
1111
import { cloneDeep } from 'lodash';
1212

1313
type TestItem = {
@@ -17,25 +17,17 @@ type TestItem = {
1717
col2: string;
1818
};
1919

20-
describe('SelectTable', function () {
20+
describe('SelectList', function () {
2121
let items: TestItem[];
22-
let columns: [key: keyof TestItem, label: string | JSX.Element][];
22+
let label: { displayLabelKey: keyof TestItem; name: string | JSX.Element };
2323
let onChange: sinon.SinonStub;
2424

2525
beforeEach(function () {
2626
items = [
2727
{ id: 'id1', selected: true, col1: '1x1', col2: '1x2' },
2828
{ id: 'id2', selected: true, col1: '2x1', col2: '2x2' },
2929
];
30-
columns = [
31-
['col1', 'Column1'],
32-
[
33-
'col2',
34-
<span key="" data-testid="column2-span">
35-
Column2
36-
</span>,
37-
],
38-
];
30+
label = { displayLabelKey: 'col1', name: 'Column1' };
3931
onChange = sinon.stub();
4032
});
4133

@@ -44,28 +36,21 @@ describe('SelectTable', function () {
4436
});
4537

4638
describe('render', function () {
47-
it('allows listing multiple selectable items in a table', function () {
48-
render(
49-
<SelectTable items={items} columns={columns} onChange={onChange} />
50-
);
51-
52-
expect(screen.getByTestId('column2-span')).to.be.visible;
53-
expect(screen.getByTestId('item-id1-col1')).to.have.text('1x1');
54-
expect(screen.getByTestId('item-id1-col2')).to.have.text('1x2');
39+
it('allows listing multiple selectable items in the list', function () {
40+
render(<SelectList items={items} label={label} onChange={onChange} />);
41+
42+
expect(screen.getByLabelText('1x1')).to.be.visible;
5543
});
5644

5745
it('renders checkboxes as expected when all items are selected', function () {
58-
render(
59-
<SelectTable items={items} columns={columns} onChange={onChange} />
60-
);
46+
render(<SelectList items={items} label={label} onChange={onChange} />);
6147

6248
expect(
63-
screen.getByTestId('select-table-all-checkbox').closest('input')
64-
?.checked
49+
screen.getByTestId('select-list-all-checkbox').closest('input')?.checked
6550
).to.equal(true);
6651
expect(
6752
screen
68-
.getByTestId('select-table-all-checkbox')
53+
.getByTestId('select-list-all-checkbox')
6954
.closest('input')
7055
?.getAttribute('aria-checked')
7156
).to.equal('true');
@@ -80,17 +65,14 @@ describe('SelectTable', function () {
8065
it('renders checkboxes as expected when no items are selected', function () {
8166
items[0].selected = false;
8267
items[1].selected = false;
83-
render(
84-
<SelectTable items={items} columns={columns} onChange={onChange} />
85-
);
68+
render(<SelectList items={items} label={label} onChange={onChange} />);
8669

8770
expect(
88-
screen.getByTestId('select-table-all-checkbox').closest('input')
89-
?.checked
71+
screen.getByTestId('select-list-all-checkbox').closest('input')?.checked
9072
).to.equal(false);
9173
expect(
9274
screen
93-
.getByTestId('select-table-all-checkbox')
75+
.getByTestId('select-list-all-checkbox')
9476
.closest('input')
9577
?.getAttribute('aria-checked')
9678
).to.equal('false');
@@ -104,17 +86,14 @@ describe('SelectTable', function () {
10486

10587
it('renders checkboxes as expected when some items are selected', function () {
10688
items[0].selected = false;
107-
render(
108-
<SelectTable items={items} columns={columns} onChange={onChange} />
109-
);
89+
render(<SelectList items={items} label={label} onChange={onChange} />);
11090

11191
expect(
112-
screen.getByTestId('select-table-all-checkbox').closest('input')
113-
?.checked
92+
screen.getByTestId('select-list-all-checkbox').closest('input')?.checked
11493
).to.equal(false);
11594
expect(
11695
screen
117-
.getByTestId('select-table-all-checkbox')
96+
.getByTestId('select-list-all-checkbox')
11897
.closest('input')
11998
?.getAttribute('aria-checked')
12099
).to.equal('mixed');
@@ -131,9 +110,7 @@ describe('SelectTable', function () {
131110
it('calls onChange when a single item is selected', function () {
132111
const originalItems = cloneDeep(items);
133112
items[0].selected = false;
134-
render(
135-
<SelectTable items={items} columns={columns} onChange={onChange} />
136-
);
113+
render(<SelectList items={items} label={label} onChange={onChange} />);
137114

138115
fireEvent.click(screen.getByTestId('select-id1'));
139116
expect(onChange).to.have.been.calledWith(originalItems);
@@ -142,9 +119,7 @@ describe('SelectTable', function () {
142119
it('calls onChange when a single item is deselected', function () {
143120
const expectedItems = cloneDeep(items);
144121
items[0].selected = false;
145-
render(
146-
<SelectTable items={items} columns={columns} onChange={onChange} />
147-
);
122+
render(<SelectList items={items} label={label} onChange={onChange} />);
148123

149124
fireEvent.click(screen.getByTestId('select-id1'));
150125
expect(onChange).to.have.been.calledWith(expectedItems);
@@ -154,23 +129,19 @@ describe('SelectTable', function () {
154129
const originalItems = cloneDeep(items);
155130
items[0].selected = false;
156131
items[1].selected = false;
157-
render(
158-
<SelectTable items={items} columns={columns} onChange={onChange} />
159-
);
132+
render(<SelectList items={items} label={label} onChange={onChange} />);
160133

161-
fireEvent.click(screen.getByTestId('select-table-all-checkbox'));
134+
fireEvent.click(screen.getByTestId('select-list-all-checkbox'));
162135
expect(onChange).to.have.been.calledWith(originalItems);
163136
});
164137

165138
it('calls onChange when all items are deselected', function () {
166139
const expectedItems = cloneDeep(items);
167140
items[0].selected = false;
168141
items[1].selected = false;
169-
render(
170-
<SelectTable items={items} columns={columns} onChange={onChange} />
171-
);
142+
render(<SelectList items={items} label={label} onChange={onChange} />);
172143

173-
fireEvent.click(screen.getByTestId('select-table-all-checkbox'));
144+
fireEvent.click(screen.getByTestId('select-list-all-checkbox'));
174145
expect(onChange).to.have.been.calledWith(expectedItems);
175146
});
176147
});
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import React, { useCallback } from 'react';
2+
import { Checkbox } from './leafygreen';
3+
import { spacing } from '@leafygreen-ui/tokens';
4+
import { css, cx } from '@leafygreen-ui/emotion';
5+
import { palette } from '@leafygreen-ui/palette';
6+
import { useDarkMode } from '../hooks/use-theme';
7+
8+
const checkboxStyles = css({
9+
padding: spacing[100],
10+
});
11+
12+
const containerStyles = css({
13+
display: 'flex',
14+
flexDirection: 'column',
15+
});
16+
17+
const evenRowStylesDark = css({ backgroundColor: palette.gray.dark3 });
18+
const evenRowStylesLight = css({ backgroundColor: palette.gray.light3 });
19+
20+
const listHeaderStyles = css({
21+
display: 'flex',
22+
alignItems: 'center',
23+
fontWeight: 600,
24+
borderBottom: `${spacing[100]}px solid ${palette.gray.light2}`,
25+
flexShrink: 0,
26+
padding: `${spacing[100]}px 0px`,
27+
});
28+
const listBodyStyles = css({
29+
overflow: 'auto',
30+
});
31+
const listItemStyles = css({
32+
padding: `${spacing[100]}px 0px`,
33+
});
34+
35+
const selectAllLabelStyles = css({ lineHeight: '16px' });
36+
37+
type SelectItem = {
38+
id: string;
39+
selected: boolean;
40+
};
41+
42+
type SelectListProps<T extends SelectItem> = {
43+
items: T[];
44+
label: {
45+
displayLabelKey: string & keyof T;
46+
ariaLabelKey?: string & keyof T;
47+
name: string | JSX.Element;
48+
};
49+
onChange: (newList: T[]) => void;
50+
disabled?: boolean;
51+
className?: string;
52+
};
53+
54+
export function SelectList<T extends SelectItem>(
55+
props: SelectListProps<T>
56+
): React.ReactElement {
57+
const { items, label, disabled, onChange } = props;
58+
59+
const isDarkMode = useDarkMode();
60+
const evenRowStyles = isDarkMode ? evenRowStylesDark : evenRowStylesLight;
61+
62+
const selectAll = items.every((item) => item.selected);
63+
const selectNone = items.every((item) => !item.selected);
64+
65+
const handleSelectAllChange = useCallback(
66+
(e: React.ChangeEvent<HTMLInputElement>) => {
67+
onChange(
68+
items.map((item) => ({ ...item, selected: !!e.target.checked }))
69+
);
70+
},
71+
[items, onChange]
72+
);
73+
const handleSelectItemChange = useCallback(
74+
(e: React.ChangeEvent<HTMLInputElement>) => {
75+
onChange(
76+
items.map((item) =>
77+
e.target.name === `select-${item.id}`
78+
? { ...item, selected: !!e.target.checked }
79+
: item
80+
)
81+
);
82+
},
83+
[items, onChange]
84+
);
85+
86+
return (
87+
<div className={cx(props.className, containerStyles)}>
88+
<div className={listHeaderStyles}>
89+
<Checkbox
90+
className={cx(checkboxStyles, css({ paddingRight: 0 }))}
91+
data-testid="select-list-all-checkbox"
92+
aria-label="Select all"
93+
onChange={handleSelectAllChange}
94+
checked={selectAll}
95+
indeterminate={!selectAll && !selectNone}
96+
disabled={disabled}
97+
/>
98+
<div className={selectAllLabelStyles}>{label.name}</div>
99+
</div>
100+
<div className={listBodyStyles}>
101+
{items.map((item, index) => (
102+
<div
103+
className={cx(listItemStyles, index % 2 === 0 && evenRowStyles)}
104+
key={`select-list-item-${item.id}`}
105+
data-testid={`select-list-item-${item.id}`}
106+
>
107+
<Checkbox
108+
className={checkboxStyles}
109+
key={`select-${item.id}`}
110+
name={`select-${item.id}`}
111+
data-testid={`select-${item.id}`}
112+
label={item[label.displayLabelKey]}
113+
aria-label={
114+
item[label.ariaLabelKey ?? label.displayLabelKey] as string
115+
}
116+
onChange={handleSelectItemChange}
117+
checked={item.selected}
118+
disabled={disabled}
119+
/>
120+
</div>
121+
))}
122+
</div>
123+
</div>
124+
);
125+
}

packages/compass-components/src/components/select-table.tsx

Lines changed: 0 additions & 105 deletions
This file was deleted.

0 commit comments

Comments
 (0)