Skip to content

Commit

Permalink
Implement Switch component.
Browse files Browse the repository at this point in the history
  • Loading branch information
mkrause committed Nov 7, 2024
1 parent 9507ac8 commit 1d64f7b
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 0 deletions.
63 changes: 63 additions & 0 deletions src/components/forms/controls/Switch/Switch.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@

@use '../../../../styling/defs.scss' as bk;

/* Note: `light-dark()` does not seem to be animatable in Chrome 125, even with `@property` */
// @property --bk-switch-track-color { syntax: '<color>'; inherits: true; initial-value: bk.$color-grey-600; }
// @property --bk-switch-thumb-color { syntax: '<color>'; inherits: true; initial-value: bk.$color-grey-400; }
@property --bk-switch-pos { syntax: '<length-percentage>'; inherits: true; initial-value: 50%; }

@layer baklava.components {
.bk-switch {
@include bk.component-base(bk-switch);

--bk-switch-track-color: #{bk.$theme-switch-slider-off};
--bk-switch-thumb-color: #{bk.$theme-switch-knob-off};

--w: 34px; // Track width
--h: 14px; // Track height
--r: 10px; // Thumb radius
--pos-start: var(--r);
--pos-end: calc(100% - var(--r));
--bk-switch-pos: var(--pos-start);

cursor: pointer;

appearance: none;
height: calc(var(--r) * 2);
width: calc(var(--w) + 6px); // Add a little bit of extra width so that the thumb overflows the track
border-radius: var(--r);

// Render the track (using circles to emulate a capsule shape)
// Note: add `1px` difference between the stops so that we have a subtle antialiasing
$col: var(--bk-switch-track-color);
$a: 1px; // Antialias
background:
// https://css-tricks.com/drawing-images-with-css-gradients
linear-gradient($col, $col) 50% 50% / calc(var(--w) - var(--h)) calc(var(--h)) no-repeat,
radial-gradient(circle at var(--r), $col calc(var(--h) / 2 - $a), transparent calc(var(--h) / 2)),
radial-gradient(circle at calc(100% - var(--r)), $col calc(var(--h) / 2 - $a), transparent calc(var(--h) / 2));
// Render the thumb
border-image:
radial-gradient(circle at var(--bk-switch-pos),
var(--bk-switch-thumb-color) calc(var(--r) - $a),
transparent var(--r)
) fill 0 / 1 / 0;

&:checked {
--bk-switch-track-color: #{bk.$theme-switch-slider-default};
--bk-switch-thumb-color: #{bk.$theme-switch-knob-default};
--bk-switch-pos: var(--pos-end);
}
&:disabled {
--bk-switch-thumb-color: #{bk.$theme-switch-knob-disabled};
}
&.bk-switch--nonactive {
--bk-switch-thumb-color: #{bk.$theme-switch-knob-non-active};
}

@media (prefers-reduced-motion: no-preference) {
transition: none 200ms ease-out;
transition-property: --bk-switch-pos, --bk-switch-track-color, --bk-switch-thumb-color;
}
}
}
50 changes: 50 additions & 0 deletions src/components/forms/controls/Switch/Switch.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@

import type { Meta, StoryObj } from '@storybook/react';
import * as React from 'react';

import { type SwitchProps, Switch } from './Switch.tsx';


type Story = StoryObj<SwitchProps>;
export default {
component: Switch,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {},
args: {
defaultChecked: true,
},
decorators: [
Story => <form onSubmit={event => { event.preventDefault(); }}><Story/></form>,
],
render: (args) => <Switch {...args}/>,
} satisfies Meta<SwitchProps>;


export const Checked: Story = {};

export const Unchecked: Story = {
args: { defaultChecked: false },
};

export const NonactiveChecked: Story = {
name: 'Nonactive (checked)',
args: { nonactive: true, defaultChecked: true },
};

export const NonactiveUnchecked: Story = {
name: 'Nonactive (unchecked)',
args: { nonactive: true, defaultChecked: false },
};

export const DisabledChecked: Story = {
name: 'Disabled (checked)',
args: { disabled: true, defaultChecked: true },
};

export const DisabledUnchecked: Story = {
name: 'Disabled (unchecked)',
args: { disabled: true, defaultChecked: false },
};
46 changes: 46 additions & 0 deletions src/components/forms/controls/Switch/Switch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@

import { classNames as cx, type ComponentProps } from '../../../../util/componentUtil.ts';
import * as React from 'react';

import { Checkbox } from '../Checkbox/Checkbox.tsx';
import cl from './Switch.module.scss';


export { cl as SwitchClassNames };

export type SwitchProps = ComponentProps<'input'> & {
/** Whether this component should be unstyled. */
unstyled?: undefined | boolean,

/**
* Whether the button is in "nonactive" state. This is a variant of `disabled`, but instead of completely graying
* out the button, it only becomes a muted variation of the button's appearance. When true, also implies `disabled`.
*/
nonactive?: undefined | boolean,
};
/**
* Switch control.
*/
export const Switch = (props: SwitchProps) => {
const {
unstyled = false,
nonactive = false,
...propsRest
} = props;

const isInteractive = !propsRest.disabled && !nonactive;

return (
<Checkbox
//switch // https://webkit.org/blog/15054/an-html-switch-control
disabled={!isInteractive}
{...propsRest}
unstyled
className={cx(
{ [cl['bk-switch']]: !unstyled },
{ [cl['bk-switch--nonactive']]: nonactive },
propsRest.className,
)}
/>
);
};

0 comments on commit 1d64f7b

Please sign in to comment.