Skip to content

Commit

Permalink
feat: implement :focus-visible for the Switch component with target…
Browse files Browse the repository at this point in the history
…ed focus style for non-pointer devices
  • Loading branch information
cheton committed Aug 30, 2023
1 parent 7bb1416 commit ca80209
Show file tree
Hide file tree
Showing 2 changed files with 133 additions and 100 deletions.
171 changes: 133 additions & 38 deletions packages/react/src/switch/SwitchControlBox.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { createTransitionStyle } from '@tonic-ui/utils';
import { ariaAttr, createTransitionStyle } 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 { defaultSize, defaultVariantColor } from './constants';
import { useSwitchControlBoxStyle } from './styles';

const SwitchControlBox = forwardRef((
{
size = defaultSize,
variantColor = defaultVariantColor,
sx: sxProp,
...rest
},
ref,
Expand All @@ -29,30 +30,128 @@ const SwitchControlBox = forwardRef((
md: 9,
lg: 12,
}[size];
const trackHaloWidth = 2;
const trackBorderWidth = 1;
const trackHaloX = 0;
const trackHaloY = 0;
const trackBorderX = trackHaloX + trackHaloWidth;
const trackBorderY = trackHaloY + trackHaloWidth;
const trackX = trackBorderX + trackBorderWidth;
const trackY = trackBorderY + trackBorderWidth;
const viewBoxWidth = width + (trackHaloWidth + trackBorderWidth) * 2;
const viewBoxHeight = height + (trackHaloWidth + trackBorderWidth) * 2;
const trackFillColor = {
const switchOuterBorderWidth = 2;
const switchInnerBorderWidth = 1;
const switchOuterBorderX = 0;
const switchOuterBorderY = 0;
const switchInnerBorderX = switchOuterBorderX + switchOuterBorderWidth;
const switchInnerBorderY = switchOuterBorderY + switchOuterBorderWidth;
const switchTrackX = switchInnerBorderX + switchInnerBorderWidth;
const switchTrackY = switchInnerBorderY + switchInnerBorderWidth;
const viewBoxWidth = width + (switchOuterBorderWidth + switchInnerBorderWidth) * 2;
const viewBoxHeight = height + (switchOuterBorderWidth + switchInnerBorderWidth) * 2;

// switch-outer-border
const switchOuterBorderColor = {
dark: `${variantColor}:60`,
light: `${variantColor}:60`,
}[colorMode];

// switch-inner-border
const switchInnerBorderColor = {
dark: 'black',
light: 'white',
}[colorMode];

// switch-track
const switchTrackColor = {
dark: 'gray:60',
light: 'gray:30',
}[colorMode];
const styleProps = useSwitchControlBoxStyle({
color: variantColor,
width,
height,
});
const switchTrackHoverColor = {
dark: 'gray:50',
light: 'gray:20',
}[colorMode];
const switchTrackCheckedColor = {
dark: `${variantColor}:60`,
light: `${variantColor}:60`,
}[colorMode];
const switchTrackCheckedHoverColor = {
dark: `${variantColor}:50`,
light: `${variantColor}:50`,
}[colorMode];

// switch-thumb
const switchThumbColor = {
dark: 'white',
light: 'white',
}[colorMode];

const inputType = 'checkbox';
const getInputControlBoxSelector = (pseudos) => {
return `input[type="${inputType}"]` + ensureString(pseudos) + ' + &';
};
const getSwitchOuterBorderSelector = (pseudos) => {
return getInputControlBoxSelector(pseudos) + '> [data-switch] > [data-switch-outer-border]';
};
const getSwitchInnerBorderSelector = (pseudos) => {
return getInputControlBoxSelector(pseudos) + '> [data-switch] > [data-switch-inner-border]';
};
const getSwitchTrackSelector = (pseudos) => {
return getInputControlBoxSelector(pseudos) + '> [data-switch] > [data-switch-track]';
};
const getSwitchThumbSelector = (pseudos) => {
return getInputControlBoxSelector(pseudos) + '> [data-switch] [data-switch-thumb]';
};

const sx = {
width: viewBoxWidth,
height: viewBoxHeight,

[getInputControlBoxSelector(':disabled')]: {
opacity: 0.28,
},

// switch-outer-border
[getSwitchOuterBorderSelector()]: {
fill: 'none',
},
[getSwitchOuterBorderSelector(':focus-visible')]: {
fill: switchOuterBorderColor,
},

// switch-inner-border
[getSwitchInnerBorderSelector()]: {
fill: 'none',
},
[getSwitchInnerBorderSelector(':focus-visible')]: {
fill: switchInnerBorderColor,
},

// switch-track
[getSwitchTrackSelector()]: {
fill: switchTrackColor,
},
[getSwitchTrackSelector(':hover:not(:disabled)')]: {
fill: switchTrackHoverColor,
},
[getSwitchTrackSelector(':checked:not(:disabled)')]: {
fill: switchTrackCheckedColor,
},
[getSwitchTrackSelector(':checked:hover:not(:disabled)')]: {
fill: switchTrackCheckedHoverColor,
},

// switch-thumb
[getSwitchThumbSelector()]: {
fill: switchThumbColor,
},
[getSwitchThumbSelector(':checked')]: {
transform: `translateX(${height}px)`,
},
};

return (
<ControlBox
type="checkbox"
{...styleProps}
<Box
type={inputType}
display="inline-flex"
alignItems="center"
justifyContent="center"
transition="all 120ms"
flexShrink="0"
aria-hidden={ariaAttr(true)}
userSelect="none"
sx={[sx, ...ensureArray(sxProp)]}
{...rest}
>
<Box
Expand All @@ -64,35 +163,32 @@ const SwitchControlBox = forwardRef((
>
<Box
as="rect"
data-switch-track-halo
x={trackHaloX}
y={trackHaloY}
data-switch-outer-border
x={switchOuterBorderX}
y={switchOuterBorderY}
width={viewBoxWidth}
height={viewBoxHeight}
rx={`${viewBoxHeight / 2}`}
fill="none"
strokeWidth={0}
/>
<Box
as="rect"
data-switch-track-border
x={trackBorderX}
y={trackBorderY}
width={viewBoxWidth - 2 * trackHaloWidth}
height={viewBoxHeight - 2 * trackHaloWidth}
rx={(viewBoxHeight - 2 * trackHaloWidth) / 2}
fill="none"
data-switch-inner-border
x={switchInnerBorderX}
y={switchInnerBorderY}
width={viewBoxWidth - 2 * switchOuterBorderWidth}
height={viewBoxHeight - 2 * switchOuterBorderWidth}
rx={(viewBoxHeight - 2 * switchOuterBorderWidth) / 2}
strokeWidth={0}
/>
<Box
as="rect"
data-switch-track
x={trackX}
y={trackY}
x={switchTrackX}
y={switchTrackY}
width={width}
height={height}
rx={height / 2}
fill={trackFillColor}
pointerEvents="all"
/>
<Box
Expand All @@ -101,13 +197,12 @@ const SwitchControlBox = forwardRef((
cx={viewBoxHeight / 2}
cy={viewBoxHeight / 2}
r={radius}
fill="white:emphasis"
transform="translateX(0)"
transformBox="fill-box"
transition={createTransitionStyle(['transform'], { duration: 250 })}
/>
</Box>
</ControlBox>
</Box>
);
});

Expand Down
62 changes: 0 additions & 62 deletions packages/react/src/switch/styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,68 +9,6 @@ const useSwitchStyle = ({ disabled }) => {
};
};

const useSwitchControlBoxStyle = ({
color,
width,
height,
}) => {
const [colorMode] = useColorMode();
const trackHaloWidth = 2;
const trackBorderWidth = 1;
const viewBoxWidth = width + (trackHaloWidth + trackBorderWidth) * 2;
const viewBoxHeight = height + (trackHaloWidth + trackBorderWidth) * 2;
const focusAndCheckedColor = {
dark: `${color}:60`,
light: `${color}:40`,
}[colorMode];
const checkedAndHoverColor = {
dark: `${color}:50`,
light: `${color}:30`,
}[colorMode];
const trackBorderColor = {
dark: 'black',
light: 'white',
}[colorMode];

return {
width: viewBoxWidth,
height: viewBoxHeight,
_child: {
opacity: 1,
},
_hover: {
'[data-switch] [data-switch-track]': {
fill: 'gray:50',
},
},
_focus: {
'[data-switch] [data-switch-track-halo]': {
fill: focusAndCheckedColor,
},
'[data-switch] [data-switch-track-border]': {
fill: trackBorderColor,
}
},
_checked: {
'[data-switch] [data-switch-track]': {
fill: focusAndCheckedColor,
},
'[data-switch] [data-switch-thumb]': {
transform: `translateX(${height}px)`,
},
},
_checkedAndHover: {
'[data-switch] [data-switch-track]': {
fill: checkedAndHoverColor,
},
},
_disabled: {
opacity: 0.28,
}
};
};

export {
useSwitchStyle,
useSwitchControlBoxStyle,
};

0 comments on commit ca80209

Please sign in to comment.