Search code examples
reactjstypescriptstyled-componentsstorybook

React Storybook Checkbox change handler not working


I am creating a checkbox component using React, Typescript, Storybook and Styled-Components, after following this tutorial: building-a-checkbox-component-with-react-and-styled-components. I have had to adjust the code as I am using a FunctionComponent, but I am facing an issue with the change handler. I cannot check or uncheck the Checkbox which seems to be readonly, and I'm not sure where I am going wrong. Here is my code:

Checkbox.tsx

import React from 'react';
import styled from 'styled-components';
import { FunctionComponent } from 'react';

type CheckboxProps = {
    checked?: boolean;
    onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
};

const CheckboxContainer = styled.div`
    display: inline-block;
    vertical-align: middle;
`;

const Icon = styled.svg`
    fill: none;
    stroke: white;
    stroke-width: 2px;
`;

const HiddenCheckbox = styled.input.attrs({ type: 'checkbox' })`
    border: 0;
    clip: rect(0 0 0 0);
    clippath: inset(50%);
    height: 1px;
    margin: -1px;
    overflow: hidden;
    padding: 0;
    position: absolute;
    white-space: nowrap;
    width: 1px;
`;

const StyledCheckbox = styled.div`
    display: inline-block;
    width: 16px;
    height: 16px;
    background: ${(props: CheckboxProps) => (props.checked ? 'green' : 'white')};
    border-color: 'dark gray';
    border-width: 1px;
    border-style: solid;
    border-radius: 2px;

    ${HiddenCheckbox}:focus + & {
        box-shadow: 0 0 0 3px grey;
    }

    ${Icon} {
        visibility: ${(props: CheckboxProps) => (props.checked ? 'visible' : 'hidden')};
    }
`;

const Checkbox: FunctionComponent<CheckboxProps> = ({ onChange, checked, ...props }) => {
    return (
        <CheckboxContainer>
            <HiddenCheckbox checked={checked} {...props} onChange={onChange} />
            <StyledCheckbox data-testid="styledcheckbox" checked={checked} {...props} onChange={onChange}>
                <Icon viewBox="0 0 24 24">
                    <polyline points="20 6 9 17 4 12" />
                </Icon>
            </StyledCheckbox>
        </CheckboxContainer>
    );
};

export default Checkbox;

Checkbox.stories.js

// Checkbox.stories.js
import React, { useState, useRef } from 'react';
import Checkbox from '@components/Checkbox/Checkbox';
import { action } from '@storybook/addon-actions';
import { storiesOf } from '@storybook/react';

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
const CheckboxStateful = props => {
    const [value, setValue] = useState(props);
    const valRef = useRef(value);
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
    const onChange = event => {
        setValue(event.target.value);
        valRef.current = event.target.value;
    };

    return (
        <Checkbox
            value={value}
            onChange={event => {
                onChange(event);
            }}
        ></Checkbox>
    );
};
storiesOf('Checkbox', module)
    .add('with checked', () => {
        const value = true;
        // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
        const onChange = event => setValue(event.target.value);
        return <CheckboxStateful value={value} onChange={onChange}></CheckboxStateful>;
    })
    .add('with unchecked', () => {
        const value = false;
        // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
        const onChange = event => setValue(event.target.value);
        return <CheckboxStateful value={value} onChange={onChange}></CheckboxStateful>;
    });

As I am a novice at both React and Storybook I may require some expert input into whether my change handler code is correct or not. I have looked at other examples, but none of them use Typescript. Any help would be appreciated.


Solution

  • Solution is to change onChange to onClick in CheckboxProps in Checkbox.tsx, as follows:

    onClick: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
    

    and to simplify CheckboxStateful in Checkbox.stories.js as follows:

    const CheckboxStateful = () => {
    const [value, setValue] = useState(false);
    return <Checkbox checked={value} onClick={() => setValue(!value)}></Checkbox>;
    

    };

    The complete updated code is now as follows:

    Checkbox.tsx

    import * as React from 'react';
    import styled from 'styled-components';
    import { FunctionComponent } from 'react';
    
    type CheckboxProps = {
        checked?: boolean;
        onClick: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
    };
    
    const CheckboxContainer = styled.div`
        display: inline-block;
        vertical-align: middle;
    `;
    
    const Icon = styled.svg`
        fill: none;
        stroke: white;
        stroke-width: 2px;
    `;
    
    const HiddenCheckbox = styled.input.attrs({ type: 'checkbox' })`
        border: 0;
        clip: rect(0 0 0 0);
        clippath: inset(50%);
        height: 1px;
        margin: -1px;
        overflow: hidden;
        padding: 0;
        position: absolute;
        white-space: nowrap;
        width: 1px;
    `;
    
    const StyledCheckbox = styled.div`
        display: inline-block;
        width: 16px;
        height: 16px;
        background: ${(props: CheckboxProps) => (props.checked ? 'green' : 'white')};
        border-color: 'dark gray';
        border-width: 1px;
        border-style: solid;
        border-radius: 2px;
    
        ${HiddenCheckbox}:focus + & {
            box-shadow: 0 0 0 3px grey;
        }
    
        ${Icon} {
            visibility: ${(props: CheckboxProps) => (props.checked ? 'visible' : 'hidden')};
        }
    `;
    
    const Checkbox: FunctionComponent<CheckboxProps> = ({ onClick, checked, ...props }) => {
        return (
            <CheckboxContainer>
                <HiddenCheckbox checked={checked} {...props} />
                <StyledCheckbox checked={checked} {...props} onClick={onClick} data-testid="styledcheckbox">
                    <Icon viewBox="0 0 24 24">
                        <polyline points="20 6 9 17 4 12" />
                    </Icon>
                </StyledCheckbox>
            </CheckboxContainer>
        );
    };
    
    export default Checkbox;
    

    Checkbox.stories.js

    /* eslint-disable @typescript-eslint/explicit-function-return-type */
    // Checkbox.stories.js
    import * as React from 'react';
    import { useState } from 'react';
    import Checkbox from '@components/Checkbox/Checkbox';
    import { action } from '@storybook/addon-actions';
    import { storiesOf } from '@storybook/react';
    
    export default {
        title: 'Checkbox',
        component: Checkbox,
    };
    
    const CheckboxStateful = () => {
        const [value, setValue] = useState(false);
        return <Checkbox checked={value} onClick={() => setValue(!value)}></Checkbox>;
    };
    
    storiesOf('Checkbox', module)
        .add('with checked', () => {
            const value = true;
            return <Checkbox checked={value} onClick={action('checked')}></Checkbox>;
        })
        .add('with unchecked', () => {
            const value = false;
            return <Checkbox checked={value} onClick={action('checked')}></Checkbox>;
        })
        .add('stateful', () => {
            return <CheckboxStateful />;
        });
    

    Once you create the React project and add Storybook, just run yarn storybook and the checkboxes will show up correctly.