Lucas/t 353 checkbox should change state when clicking on their whole (#167)
* Added on click on Checkbox component * - Added test in story - Added sleep util - Fixed click target collision (thanks to test !) * Use a new CheckboxCell to wrap Checkbox * Fixed lint * Refactored CSS after comment * Fixed tests
This commit is contained in:
@ -6,13 +6,17 @@ type OwnProps = {
|
|||||||
id: string;
|
id: string;
|
||||||
checked?: boolean;
|
checked?: boolean;
|
||||||
indeterminate?: boolean;
|
indeterminate?: boolean;
|
||||||
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
onChange?: (newCheckedValue: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledContainer = styled.span`
|
const StyledContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
input[type='checkbox'] {
|
input[type='checkbox'] {
|
||||||
accent-color: ${(props) => props.theme.blue};
|
accent-color: ${(props) => props.theme.blue};
|
||||||
margin: 8px;
|
margin: 2px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
width: 14px;
|
width: 14px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -37,8 +41,15 @@ const StyledContainer = styled.span`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function Checkbox({ name, id, checked, onChange, indeterminate }: OwnProps) {
|
export function Checkbox({
|
||||||
|
name,
|
||||||
|
id,
|
||||||
|
checked,
|
||||||
|
onChange,
|
||||||
|
indeterminate,
|
||||||
|
}: OwnProps) {
|
||||||
const ref = React.useRef<HTMLInputElement>(null);
|
const ref = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (ref.current === null) return;
|
if (ref.current === null) return;
|
||||||
if (typeof indeterminate === 'boolean') {
|
if (typeof indeterminate === 'boolean') {
|
||||||
@ -46,6 +57,12 @@ function Checkbox({ name, id, checked, onChange, indeterminate }: OwnProps) {
|
|||||||
}
|
}
|
||||||
}, [ref, indeterminate, checked]);
|
}, [ref, indeterminate, checked]);
|
||||||
|
|
||||||
|
function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
if (onChange) {
|
||||||
|
onChange(event.target.checked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<input
|
<input
|
||||||
@ -55,10 +72,8 @@ function Checkbox({ name, id, checked, onChange, indeterminate }: OwnProps) {
|
|||||||
id={id}
|
id={id}
|
||||||
name={name}
|
name={name}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
onChange={onChange}
|
onChange={handleInputChange}
|
||||||
/>
|
/>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Checkbox;
|
|
||||||
|
|||||||
65
front/src/components/table/CheckboxCell.tsx
Normal file
65
front/src/components/table/CheckboxCell.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { Checkbox } from '../form/Checkbox';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
checked?: boolean;
|
||||||
|
indeterminate?: boolean;
|
||||||
|
onChange?: (newCheckedValue: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledContainer = styled.div`
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
margin-left: -${(props) => props.theme.table.horizontalCellMargin};
|
||||||
|
padding-left: ${(props) => props.theme.table.horizontalCellMargin};
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function CheckboxCell({
|
||||||
|
name,
|
||||||
|
id,
|
||||||
|
checked,
|
||||||
|
onChange,
|
||||||
|
indeterminate,
|
||||||
|
}: OwnProps) {
|
||||||
|
const [internalChecked, setInternalChecked] = React.useState(checked);
|
||||||
|
|
||||||
|
function handleContainerClick() {
|
||||||
|
handleCheckboxChange(!internalChecked);
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setInternalChecked(checked);
|
||||||
|
}, [checked]);
|
||||||
|
|
||||||
|
function handleCheckboxChange(newCheckedValue: boolean) {
|
||||||
|
setInternalChecked(newCheckedValue);
|
||||||
|
|
||||||
|
if (onChange) {
|
||||||
|
onChange(newCheckedValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledContainer
|
||||||
|
onClick={handleContainerClick}
|
||||||
|
data-testid="input-checkbox-cell-container"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
id={id}
|
||||||
|
name={name}
|
||||||
|
checked={internalChecked}
|
||||||
|
onChange={handleCheckboxChange}
|
||||||
|
indeterminate={indeterminate}
|
||||||
|
/>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -34,12 +34,12 @@ type OwnProps<
|
|||||||
|
|
||||||
const StyledTable = styled.table`
|
const StyledTable = styled.table`
|
||||||
min-width: 1000px;
|
min-width: 1000px;
|
||||||
width: calc(100% - ${(props) => props.theme.spacing(4)});
|
width: calc(100% - 2 * ${(props) => props.theme.table.horizontalCellMargin});
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
margin-left: ${(props) => props.theme.spacing(2)};
|
margin-left: ${(props) => props.theme.table.horizontalCellMargin};
|
||||||
margin-right: ${(props) => props.theme.spacing(2)};
|
margin-right: ${(props) => props.theme.table.horizontalCellMargin};
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
|
|
||||||
th {
|
th {
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
import Checkbox from '../form/Checkbox';
|
import { CheckboxCell } from './CheckboxCell';
|
||||||
|
|
||||||
export const SelectAllCheckbox = ({
|
export const SelectAllCheckbox = ({
|
||||||
indeterminate,
|
indeterminate,
|
||||||
onChange,
|
onChange,
|
||||||
}: {
|
}: {
|
||||||
indeterminate?: boolean;
|
indeterminate?: boolean;
|
||||||
onChange?: any;
|
onChange?: (newCheckedValue: boolean) => void;
|
||||||
} & React.HTMLProps<HTMLInputElement>) => {
|
} & React.HTMLProps<HTMLInputElement>) => {
|
||||||
return (
|
return (
|
||||||
<Checkbox
|
<CheckboxCell
|
||||||
name="select-all-checkbox"
|
name="select-all-checkbox"
|
||||||
id="select-all-checkbox"
|
id="select-all-checkbox"
|
||||||
indeterminate={indeterminate}
|
indeterminate={indeterminate}
|
||||||
|
|||||||
@ -16,6 +16,10 @@ const commonTheme = {
|
|||||||
fontFamily: 'Inter, sans-serif',
|
fontFamily: 'Inter, sans-serif',
|
||||||
|
|
||||||
spacing: (multiplicator: number) => `${multiplicator * 4}px`,
|
spacing: (multiplicator: number) => `${multiplicator * 4}px`,
|
||||||
|
|
||||||
|
table: {
|
||||||
|
horizontalCellMargin: '8px',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const lightThemeSpecific = {
|
const lightThemeSpecific = {
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import { updateCompany } from '../../services/api/companies';
|
|||||||
import { User, mapToUser } from '../../interfaces/entities/user.interface';
|
import { User, mapToUser } from '../../interfaces/entities/user.interface';
|
||||||
|
|
||||||
import ColumnHead from '../../components/table/ColumnHead';
|
import ColumnHead from '../../components/table/ColumnHead';
|
||||||
import Checkbox from '../../components/form/Checkbox';
|
|
||||||
import { SelectAllCheckbox } from '../../components/table/SelectAllCheckbox';
|
import { SelectAllCheckbox } from '../../components/table/SelectAllCheckbox';
|
||||||
import EditableDate from '../../components/editable-cell/EditableDate';
|
import EditableDate from '../../components/editable-cell/EditableDate';
|
||||||
import EditableRelation from '../../components/editable-cell/EditableRelation';
|
import EditableRelation from '../../components/editable-cell/EditableRelation';
|
||||||
@ -29,6 +28,7 @@ import {
|
|||||||
} from 'react-icons/tb';
|
} from 'react-icons/tb';
|
||||||
import { QueryMode } from '../../generated/graphql';
|
import { QueryMode } from '../../generated/graphql';
|
||||||
import { getLogoUrlFromDomainName } from '../../services/utils';
|
import { getLogoUrlFromDomainName } from '../../services/utils';
|
||||||
|
import { CheckboxCell } from '../../components/table/CheckboxCell';
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<Company>();
|
const columnHelper = createColumnHelper<Company>();
|
||||||
|
|
||||||
@ -41,15 +41,15 @@ export const useCompaniesColumns = () => {
|
|||||||
<SelectAllCheckbox
|
<SelectAllCheckbox
|
||||||
checked={table.getIsAllRowsSelected()}
|
checked={table.getIsAllRowsSelected()}
|
||||||
indeterminate={table.getIsSomeRowsSelected()}
|
indeterminate={table.getIsSomeRowsSelected()}
|
||||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
onChange={(newValue) => table.toggleAllRowsSelected(newValue)}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
cell: (props: CellContext<Company, string>) => (
|
cell: (props: CellContext<Company, string>) => (
|
||||||
<Checkbox
|
<CheckboxCell
|
||||||
id={`company-selected-${props.row.original.id}`}
|
id={`company-selected-${props.row.original.id}`}
|
||||||
name={`company-selected-${props.row.original.id}`}
|
name={`company-selected-${props.row.original.id}`}
|
||||||
checked={props.row.getIsSelected()}
|
checked={props.row.getIsSelected()}
|
||||||
onChange={props.row.getToggleSelectedHandler()}
|
onChange={(newValue) => props.row.toggleSelected(newValue)}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
size: 25,
|
size: 25,
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import People from '../People';
|
|||||||
import { Story } from './People.stories';
|
import { Story } from './People.stories';
|
||||||
import { mocks, render } from './shared';
|
import { mocks, render } from './shared';
|
||||||
import { mockedPeopleData } from '../../../testing/mock-data/people';
|
import { mockedPeopleData } from '../../../testing/mock-data/people';
|
||||||
|
import { sleep } from '../../../testing/sleep';
|
||||||
|
|
||||||
const meta: Meta<typeof People> = {
|
const meta: Meta<typeof People> = {
|
||||||
title: 'Pages/People',
|
title: 'Pages/People',
|
||||||
@ -39,7 +40,7 @@ export const ChangeEmail: Story = {
|
|||||||
|
|
||||||
await userEvent.click(secondRowEmailCell);
|
await userEvent.click(secondRowEmailCell);
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 25));
|
await sleep(25);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
canvas.queryByTestId('editable-cell-edit-mode-container'),
|
canvas.queryByTestId('editable-cell-edit-mode-container'),
|
||||||
@ -47,7 +48,7 @@ export const ChangeEmail: Story = {
|
|||||||
|
|
||||||
await userEvent.click(secondRowEmailCell);
|
await userEvent.click(secondRowEmailCell);
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 25));
|
await sleep(25);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
canvas.queryByTestId('editable-cell-edit-mode-container'),
|
canvas.queryByTestId('editable-cell-edit-mode-container'),
|
||||||
@ -57,3 +58,34 @@ export const ChangeEmail: Story = {
|
|||||||
msw: mocks,
|
msw: mocks,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const Checkbox: Story = {
|
||||||
|
render,
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const inputCheckboxContainers = await canvas.findAllByTestId(
|
||||||
|
'input-checkbox-cell-container',
|
||||||
|
);
|
||||||
|
|
||||||
|
const inputCheckboxes = await canvas.findAllByTestId('input-checkbox');
|
||||||
|
|
||||||
|
const secondCheckboxContainer = inputCheckboxContainers[1];
|
||||||
|
const secondCheckbox = inputCheckboxes[1] as HTMLInputElement;
|
||||||
|
|
||||||
|
expect(secondCheckboxContainer).toBeDefined();
|
||||||
|
|
||||||
|
await userEvent.click(secondCheckboxContainer);
|
||||||
|
|
||||||
|
expect(secondCheckbox.checked).toBe(true);
|
||||||
|
|
||||||
|
await userEvent.click(secondCheckbox);
|
||||||
|
|
||||||
|
expect(secondCheckbox.checked).toBe(false);
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
msw: mocks,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import { Person } from '../../interfaces/entities/person.interface';
|
|||||||
import { updatePerson } from '../../services/api/people';
|
import { updatePerson } from '../../services/api/people';
|
||||||
|
|
||||||
import ColumnHead from '../../components/table/ColumnHead';
|
import ColumnHead from '../../components/table/ColumnHead';
|
||||||
import Checkbox from '../../components/form/Checkbox';
|
|
||||||
import { SelectAllCheckbox } from '../../components/table/SelectAllCheckbox';
|
import { SelectAllCheckbox } from '../../components/table/SelectAllCheckbox';
|
||||||
import EditablePhone from '../../components/editable-cell/EditablePhone';
|
import EditablePhone from '../../components/editable-cell/EditablePhone';
|
||||||
import { EditablePeopleFullName } from '../../components/people/EditablePeopleFullName';
|
import { EditablePeopleFullName } from '../../components/people/EditablePeopleFullName';
|
||||||
@ -19,6 +18,7 @@ import {
|
|||||||
TbUser,
|
TbUser,
|
||||||
} from 'react-icons/tb';
|
} from 'react-icons/tb';
|
||||||
import { PeopleCompanyCell } from '../../components/people/PeopleCompanyCell';
|
import { PeopleCompanyCell } from '../../components/people/PeopleCompanyCell';
|
||||||
|
import { CheckboxCell } from '../../components/table/CheckboxCell';
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<Person>();
|
const columnHelper = createColumnHelper<Person>();
|
||||||
|
|
||||||
@ -31,15 +31,15 @@ export const usePeopleColumns = () => {
|
|||||||
<SelectAllCheckbox
|
<SelectAllCheckbox
|
||||||
checked={table.getIsAllRowsSelected()}
|
checked={table.getIsAllRowsSelected()}
|
||||||
indeterminate={table.getIsSomeRowsSelected()}
|
indeterminate={table.getIsSomeRowsSelected()}
|
||||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
onChange={(newValue) => table.toggleAllRowsSelected(newValue)}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
cell: (props: CellContext<Person, string>) => (
|
cell: (props: CellContext<Person, string>) => (
|
||||||
<Checkbox
|
<CheckboxCell
|
||||||
id={`person-selected-${props.row.original.id}`}
|
id={`person-selected-${props.row.original.id}`}
|
||||||
name={`person-selected-${props.row.original.id}`}
|
name={`person-selected-${props.row.original.id}`}
|
||||||
checked={props.row.getIsSelected()}
|
checked={props.row.getIsSelected()}
|
||||||
onChange={props.row.getToggleSelectedHandler()}
|
onChange={(newValue) => props.row.toggleSelected(newValue)}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
size: 25,
|
size: 25,
|
||||||
|
|||||||
5
front/src/testing/sleep.ts
Normal file
5
front/src/testing/sleep.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export async function sleep(ms: number) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, ms);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user