Skip to content

Commit

Permalink
feat: implement :focus-visible for the Checkbox component with targ…
Browse files Browse the repository at this point in the history
…eted focus style for non-pointer devices
  • Loading branch information
cheton committed Sep 18, 2023
1 parent 6439e86 commit 0793561
Show file tree
Hide file tree
Showing 4 changed files with 316 additions and 326 deletions.
7 changes: 4 additions & 3 deletions packages/react/src/checkbox/Checkbox.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useMergeRefs } from '@tonic-ui/react-hooks';
import { callAll, dataAttr, isNullish } from '@tonic-ui/utils';
import { ensureArray } from 'ensure-type';
import React, { forwardRef } from 'react';
import React, { forwardRef, useRef } from 'react';
import { Box } from '../box';
import { VisuallyHidden } from '../visually-hidden';
import CheckboxControlBox from './CheckboxControlBox';
Expand All @@ -18,7 +18,7 @@ const Checkbox = forwardRef((
id,
indeterminate,
inputProps,
inputRef,
inputRef: inputRefProp,
name,
onBlur,
onChange,
Expand All @@ -31,7 +31,8 @@ const Checkbox = forwardRef((
},
ref,
) => {
const combinedInputRef = useMergeRefs(ref, inputRef); // TODO: Move the `ref` to the outermost element in the next major version
const inputRef = useRef();
const combinedInputRef = useMergeRefs(ref, inputRefProp, inputRef); // TODO: Move the `ref` to the outermost element in the next major version
const styleProps = useCheckboxStyle({ disabled });
const checkboxGroupContext = useCheckboxGroup();

Expand Down
207 changes: 196 additions & 11 deletions packages/react/src/checkbox/CheckboxControlBox.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { ariaAttr } from '@tonic-ui/utils';
import { ensureArray, ensureString } from 'ensure-type';
import React, { forwardRef } from 'react';
import { Box, ControlBox } from '../box';
import { Box } from '../box';
import { useColorMode } from '../color-mode';
import { useTheme } from '../theme';
import { defaultSize, defaultVariantColor } from './constants';
import IconChecked from './IconChecked';
Expand All @@ -13,11 +16,13 @@ const CheckboxControlBox = forwardRef((
indeterminate,
size = defaultSize,
variantColor = defaultVariantColor,
sx: sxProp,
...rest
},
ref,
) => {
const { sizes } = useTheme();
const [colorMode] = useColorMode();
const iconSize = {
lg: sizes['6x'],
md: sizes['4x'],
Expand All @@ -33,19 +38,199 @@ const CheckboxControlBox = forwardRef((
md: '4x',
sm: '3x',
}[size];
const styleProps = useCheckboxControlBoxStyle({
color: variantColor,
indeterminate,
width,
height,
});
const icon = indeterminate
const icon = !!indeterminate
? <IconIndeterminate size={iconSize} />
: <IconChecked size={iconSize} />;

const inputType = 'checkbox';
const getCheckboxControlBoxSelector = (pseudos) => {
return `input[type="${inputType}"]` + ensureString(pseudos) + ' + &';
};
const getDeterminateStyle = ({ variantColor }) => {
const color = {
dark: 'white:emphasis',
light: 'white:emphasis',
}[colorMode];
const hoverColor = color;
const disabledColor = color;
const checkedColor = color;
const checkedAndHoverColor = color;
const checkedAndFocusVisibleColor = color;
const checkedAndDisabledColor = {
dark: 'white:emphasis',
light: 'black:primary',
}[colorMode];

// background color
const backgroundColor = 'transparent';
const hoverBackgroundColor = backgroundColor;
const disabledBackgroundColor = backgroundColor;
const checkedBackgroundColor = {
dark: `${variantColor}:60`,
light: `${variantColor}:60`,
}[colorMode];
const checkedAndHoverBackgroundColor = {
dark: `${variantColor}:50`,
light: `${variantColor}:50`,
}[colorMode];
const checkedAndFocusVisibleBackgroundColor = checkedBackgroundColor;
const checkedAndDisabledBackgroundColor = {
dark: 'gray:60',
light: 'gray:40',
}[colorMode];

// border color
const borderColor = {
dark: 'gray:50',
light: 'gray:40',
}[colorMode];
const hoverBorderColor = {
dark: `${variantColor}:50`,
light: `${variantColor}:50`,
}[colorMode];
const disabledBorderColor = {
dark: 'gray:60',
light: 'gray:40',
}[colorMode];
const checkedBorderColor = {
dark: `${variantColor}:60`,
light: `${variantColor}:60`,
}[colorMode];
const checkedAndHoverBorderColor = hoverBorderColor;
const checkedAndFocusVisibleBorderColor = 'transparent';
const checkedAndDisabledBorderColor = disabledBorderColor;

// outline color
const focusOutlineColor = {
dark: `${variantColor}:60`,
light: `${variantColor}:60`,
}[colorMode];

return {
backgroundColor: backgroundColor,
borderColor: borderColor,
color: color, // icon color
[getCheckboxControlBoxSelector(':hover')]: {
backgroundColor: hoverBackgroundColor,
borderColor: hoverBorderColor,
color: hoverColor, // icon color
},
[getCheckboxControlBoxSelector(':disabled')]: {
backgroundColor: disabledBackgroundColor,
borderColor: disabledBorderColor,
color: disabledColor, // icon color
opacity: 0.28,
},
[getCheckboxControlBoxSelector(':focus-visible')]: {
outlineStyle: 'solid',
outlineColor: focusOutlineColor,
outlineWidth: '1h',
},
[getCheckboxControlBoxSelector(':checked')]: {
backgroundColor: checkedBackgroundColor,
borderColor: checkedBorderColor,
color: checkedColor, // icon color
},
[getCheckboxControlBoxSelector(':checked:hover:not(:disabled)')]: {
backgroundColor: checkedAndHoverBackgroundColor,
borderColor: checkedAndHoverBorderColor,
color: checkedAndHoverColor, // icon color
},
[getCheckboxControlBoxSelector(':checked:focus-visible')]: {
backgroundColor: 'inherit',
borderColor: checkedAndFocusVisibleBorderColor,
color: checkedAndFocusVisibleColor, // icon color
},
[getCheckboxControlBoxSelector(':checked:focus-visible') + '> div:first-of-type']: {
backgroundColor: checkedAndFocusVisibleBackgroundColor,
},
[getCheckboxControlBoxSelector(':checked:disabled')]: {
backgroundColor: checkedAndDisabledBackgroundColor,
borderColor: checkedAndDisabledBorderColor,
color: checkedAndDisabledColor, // icon color
opacity: 0.28,
},
};
};
const getIndeterminateStyle = ({ variantColor }) => {
// icon color
const color = {
dark: `${variantColor}:60`,
light: `${variantColor}:60`,
}[colorMode];
const hoverColor = {
dark: `${variantColor}:50`,
light: `${variantColor}:50`,
}[colorMode];
const disabledColor = {
dark: 'gray:60',
light: 'gray:40',
}[colorMode];

// border color
const borderColor = {
dark: 'gray:50',
light: 'gray:40',
}[colorMode];
const hoverBorderColor = {
dark: `${variantColor}:50`,
light: `${variantColor}:50`,
}[colorMode];
const disabledBorderColor = {
dark: 'gray:60',
light: 'gray:40',
}[colorMode];

// outline color
const focusOutlineColor = {
dark: `${variantColor}:60`,
light: `${variantColor}:60`,
}[colorMode];

return {
[getCheckboxControlBoxSelector('[data-indeterminate]')]: {
borderColor: borderColor,
color: color, // icon color
},
[getCheckboxControlBoxSelector('[data-indeterminate]:hover:not(:disabled)')]: {
borderColor: hoverBorderColor,
color: hoverColor,
},
[getCheckboxControlBoxSelector('[data-indeterminate]:focus-visible')]: {
outlineStyle: 'solid',
outlineColor: focusOutlineColor,
outlineWidth: '1h',
},
[getCheckboxControlBoxSelector('[data-indeterminate]:disabled')]: {
borderColor: disabledBorderColor,
color: disabledColor, // icon color
opacity: 0.28,
},
};
};
const sx = {
position: 'relative',
border: 1,
width,
height,
zIndex: 0,
[getCheckboxControlBoxSelector() + '> *']: {
opacity: 0,
},
[getCheckboxControlBoxSelector(':checked') + '> *']: {
opacity: 1,
},
[getCheckboxControlBoxSelector('[data-indeterminate]') + '> *']: {
opacity: 1,
},
...(!!indeterminate ? getIndeterminateStyle({ variantColor }) : getDeterminateStyle({ variantColor })),
};
const styleProps = useCheckboxControlBoxStyle();

return (
<ControlBox
type="checkbox"
<Box
aria-hidden={ariaAttr(true)}
sx={[sx, ...ensureArray(sxProp)]}
{...styleProps}
{...rest}
>
Expand All @@ -58,7 +243,7 @@ const CheckboxControlBox = forwardRef((
right="0"
/>
{icon}
</ControlBox>
</Box>
);
});

Expand Down
Loading

0 comments on commit 0793561

Please sign in to comment.