Skip to content

Commit

Permalink
Refactor: use pure css switch and checkbox
Browse files Browse the repository at this point in the history
  • Loading branch information
emielvanseveren committed Aug 2, 2024
1 parent f0a154c commit ad83637
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 277 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { FC } from 'react';
import { useController } from 'react-hook-form';
import { GenericCheckBox } from '.';
import { Container, CheckboxContainer } from './style';
import { Container, LoadingCheckBox } from './style';

import { defaultInputProps, defaultInputPropsFactory, ControlledInputProps } from '../InputProps';
import { Label, ErrorMessage, InputWrapper, Description } from '../layout';
Expand Down Expand Up @@ -54,7 +54,7 @@ export const ControlledCheckBox: FC<ControlledCheckBoxProps> = (props) => {
htmlFor={name}
/>
)}
<CheckboxContainer className="placeholder" readOnly={readOnly} hasError={!!error} disabled={disabled} />
<LoadingCheckBox className="placeholder" />
{/* CASE: show label after <CheckBox /> */}
{labelPosition === 'right' && label && (
<Label
Expand Down Expand Up @@ -101,7 +101,6 @@ export const ControlledCheckBox: FC<ControlledCheckBoxProps> = (props) => {
onChange={field.onChange}
onBlur={field.onBlur}
value={field.value}
ref={field.ref}
/>

{/* CASE: show label after <CheckBox /> */}
Expand Down
128 changes: 36 additions & 92 deletions packages/lib-components/src/components/inputs/CheckBox/Generic.tsx
Original file line number Diff line number Diff line change
@@ -1,98 +1,42 @@
import { forwardRef, useRef } from 'react';
import { BackgroundContainer, CheckboxContainer, CheckMarkContainer } from './style';

import { AiOutlineCheck as Icon } from 'react-icons/ai';

import { ChangeEvent, forwardRef } from 'react';
import { Input } from './style';
import { defaultInputPropsFactory, defaultInputProps, GenericInputProps } from '../InputProps';
import { setAriaDescribedBy } from '../layout';

const variants = {
unchecked: { scale: 0, opacity: 0 },
checked: { scale: 1, opacity: 1 },
};

export type GenericCheckBoxProps = GenericInputProps<boolean, HTMLInputElement>;

const defaultsApplier = defaultInputPropsFactory<GenericCheckBoxProps>(defaultInputProps);

// TODO: write a test that checks if the value is being processed as a boolean.
export const GenericCheckBox = forwardRef<HTMLButtonElement, GenericCheckBoxProps>(
function GenericCheckBox(props, ref) {
const {
readOnly,
disabled,
value = false,
hasError,
onChange,
id,
name,
hasDescription,
size,
required,
} = defaultsApplier(props);
const inputRef = useRef<HTMLInputElement>(null);

function handleOnClick(): void {
if (readOnly || disabled) return;
inputRef.current?.click();
}

function getIconSize() {
switch (size) {
case 'tiny':
return 16;
case 'small':
return 14;
case 'medium':
return 16;
case 'large':
return 18;
case 'huge':
return 20;
}
}

return (
<>
<input
type="checkbox"
aria-required={required}
aria-hidden="true"
style={{ position: 'absolute', pointerEvents: 'none', opacity: 0, margin: 0 }}
checked={value}
tabIndex={-1}
id={`${id}-hidden-input`}
onChange={(e) => {
onChange(e);
}}
ref={inputRef}
/>
<CheckboxContainer
role="checkbox"
id={id}
isChecked={value}
readOnly={readOnly}
disabled={disabled}
onClick={handleOnClick}
aria-describedby={setAriaDescribedBy(name, hasDescription)}
aria-checked={value}
hasError={hasError}
ref={ref}
tabIndex={readOnly || disabled ? -1 : 0}
type="button"
>
<BackgroundContainer
$size={size}
initial={value ? 'checked' : 'unchecked'}
animate={value ? 'checked' : 'unchecked'}
variants={variants}
>
<CheckMarkContainer isChecked={value}>
<Icon size={getIconSize()} />
</CheckMarkContainer>
</BackgroundContainer>
</CheckboxContainer>
</>
);
},
);
export const GenericCheckBox = forwardRef<HTMLInputElement, GenericCheckBoxProps>(function Switch(props, ref) {
const {
readOnly,
disabled,
value = false,
hasError,
onChange,
id,
name,
hasDescription,
required,
} = defaultsApplier(props);

function handleOnChange(e: ChangeEvent<HTMLInputElement>) {
if (readOnly || disabled) return;
onChange(e);
}

return (
<Input
name={name}
type="checkbox"
hasError={hasError}
hasDescription={hasDescription}
aria-required={required}
checked={value}
readOnly={readOnly}
disabled={disabled}
id={id}
onChange={handleOnChange}
ref={ref}
/>
);
});
139 changes: 40 additions & 99 deletions packages/lib-components/src/components/inputs/CheckBox/style.ts
Original file line number Diff line number Diff line change
@@ -1,115 +1,56 @@
import { Size, styled } from '../../../styled';
import { motion } from 'framer-motion';
import { shade } from 'polished';
import { styled } from '../../../styled';

export const Container = styled.div`
position: relative;
display: flex;
align-items: center;
justify-content: flex-start;
flex-direction: row;
text-align: left;
margin-bottom: ${({ theme }) => theme.spacing['0_5']};
`;

export const Input = styled.input`
position: absolute;
visibility: hidden;
`;

export const BackgroundContainer = styled(motion.div)<{ $size: Size }>`
background-color: ${({ theme }) => shade(0.5, theme.colors.primary)};
${({ $size }): string => {
switch ($size) {
case 'tiny':
return `
width: 1.2rem;
height: 1.2rem;
`;
case 'small':
return `
width: 1.6rem;
height: 1.6rem;
`;
case 'medium':
return `
width: 2rem;
height: 2rem;
`;
case 'large':
return `
width: 2.4rem;
height: 2.4rem;
`;
case 'huge':
return `
width: 2.8rem;
height: 2.8rem;
`;
}
}}
`;

export const CheckboxContainer = styled.button<{
isChecked?: boolean;
readOnly: boolean;
hasError: boolean;
disabled: boolean;
}>`
display: flex;
padding: 0;
background-color: transparent;
position: relative;
align-items: center;
justify-content: center;
border: 0.1rem solid
${({ theme, isChecked, hasError, disabled }): string => {
if (disabled) {
return theme.colors.disabled;
}
if (isChecked) {
return theme.colors.primary;
}
if (hasError) {
return theme.colors.error;
}
return theme.colors.backgroundAccent;
}};
border-radius: ${({ theme }) => theme.borderRadius.small};
transition:
box-shadow 100ms linear,
border-color 100ms linear;
cursor: ${({ readOnly, disabled }) => {
if (disabled) {
return 'not-allowed';
}
if (readOnly) {
return 'inherit';
}
return 'pointer';
}};
overflow: visible;
export const LoadingCheckBox = styled.div`
&.placeholder {
border: none; /* Otherwise the border does not have the animation */
border: none;
border-radius: ${({ theme }) => theme.borderRadius.small};
width: 2.4rem;
height: 2.4rem;
width: 2rem;
height: 2rem;
cursor: default;
}
`;

export const CheckMarkContainer = styled.div<{ isChecked: boolean }>`
display: flex;
visibility: ${({ isChecked }): string => (isChecked ? 'visible' : 'hidden')};
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
opacity: ${({ isChecked }): number => (isChecked ? 1 : 0)};
transition: 0.2s opacity ease-in-out cubic-bezier(0.215, 0.61, 0.355, 1);
export const Input = styled.input<{ hasError: boolean; hasDescription: boolean }>`
width: 2rem;
height: 2rem;
border: 0.1rem solid ${({ theme, hasError }) => (hasError ? theme.colors.error : theme.colors.backgroundAccent)};
cursor: pointer;
position: relative;
padding: ${({ theme }) => theme.spacing['0_75']};
background: ${({ theme }) => theme.colors.backgroundAlt};
transition: 0.15ms linear background;
vertical-align: middle;
:disabled {
background: ${({ theme }) => theme.colors.disabled};
}
svg {
fill: white;
stroke: white;
:checked {
background: ${({ theme }) => theme.colors.primary};
:disabled {
background: ${({ theme }) => theme.colors.disabled};
}
::after {
position: absolute;
content: '';
display: block;
left: 6px;
width: 4px;
top: 3px;
height: 8px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
}
`;
63 changes: 18 additions & 45 deletions packages/lib-components/src/components/inputs/Switch/Generic.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { forwardRef, useRef } from 'react';
import { getTransition } from '../../../helpers';
import { Dot, ContentContainer } from './style';
import { ChangeEvent, forwardRef } from 'react';
import { Input } from './style';
import { defaultInputProps, defaultInputPropsFactory, GenericInputProps } from '../InputProps';
import { setAriaDescribedBy } from '../layout';

export type GenericSwitchProps = GenericInputProps<boolean, HTMLInputElement>;

const defaultsApplier = defaultInputPropsFactory<GenericSwitchProps>(defaultInputProps);

export const GenericSwitch = forwardRef<HTMLButtonElement, GenericSwitchProps>(function Switch(props, ref) {
export const GenericSwitch = forwardRef<HTMLInputElement, GenericSwitchProps>(function Switch(props, ref) {
const {
readOnly,
onChange,
Expand All @@ -19,49 +17,24 @@ export const GenericSwitch = forwardRef<HTMLButtonElement, GenericSwitchProps>(f
disabled,
hasError,
} = defaultsApplier(props);
const inputRef = useRef<HTMLInputElement>(null);

function handleOnClick(): void {
if (readOnly || disabled) return;
inputRef.current?.click();
function handleOnChange(e: ChangeEvent<HTMLInputElement>) {
if (disabled || readOnly) return;
onChange(e);
}

return (
<>
<input
type="checkbox"
aria-hidden="true"
style={{ position: 'absolute', pointerEvents: 'none', opacity: 0, margin: 0 }}
checked={isChecked}
tabIndex={-1}
id={`${id}-hidden-input`}
value="on"
onChange={(e) => onChange(e)}
ref={inputRef}
/>
<ContentContainer
role="switch"
id={id}
isChecked={isChecked}
disabled={disabled}
readOnly={readOnly}
onClick={handleOnClick}
ref={ref}
aria-describedby={setAriaDescribedBy(name, hasDescription)}
aria-checked={isChecked}
hasError={hasError}
tabIndex={readOnly || disabled ? -1 : 0}
type="button"
>
<Dot
initial={{ right: isChecked ? '2px' : '25px' }}
animate={{ right: isChecked ? '2px' : '25px' }}
$readOnly={readOnly}
$isChecked={isChecked}
layout
transition={getTransition()}
/>
</ContentContainer>
</>
<Input
name={name}
hasError={hasError}
hasDescription={hasDescription}
readOnly={readOnly}
disabled={disabled}
type="checkbox"
checked={isChecked}
id={id}
onChange={handleOnChange}
ref={ref}
/>
);
});
Loading

0 comments on commit ad83637

Please sign in to comment.