2

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.

2 Answers2

1

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.

-1

Instead of creating a new Stateful component a much better approach is to use state in the components template.

An example:

const Template: ComponentStory<typeof MyComponent> = (args) => {
  const [state, setState] = useState('');

  return (
    <MyComponent
      prop1={args.prop1}
      prop2={args.prop2}
      onChange={() => setState('Change')}
    />
  );
};

Here's a link to the original answer I got this approach from: https://stackoverflow.com/a/64722559/10641401

Faizan
  • 9
  • 4