Skip to content

Commit

Permalink
feat: add switch component
Browse files Browse the repository at this point in the history
  • Loading branch information
itupix committed Jun 20, 2024
1 parent 5a8273f commit aba2a30
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 0 deletions.
3 changes: 3 additions & 0 deletions packages/core/src/components/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
@forward './validation/validation';
@use './accordion/accordion.scss';
@forward './accordion/accordion.scss';
@use './switch/switch.scss';
@forward './switch/switch.scss';

@mixin components() {
@include asterisk.Asterisk();
Expand All @@ -69,4 +71,5 @@
@include tooltip.Tooltip();
@include validation.Validation();
@include accordion.Accordion();
@include switch.Switch();
}
1 change: 1 addition & 0 deletions packages/core/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export * from './progress/progress';
export * from './radio/radio';
export * from './select/select';
export * from './spinner/spinner';
export * from './switch/switch';
export * from './textarea/textarea';
export * from './tooltip/tooltip';
export * from './validation/validation';
132 changes: 132 additions & 0 deletions packages/core/src/components/switch/switch.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/* stylelint-disable selector-max-compound-selectors */

@use '../../animations';
@use '../../helpers';
@use '../../mixins';

@mixin Switch() {
@if not mixins.includes('Switch') {
@include _Switch();
}
}

@mixin _Switch() {
.ods-switch-label {
align-items: center;
color: helpers.color('content-main');
cursor: pointer;
display: inline-flex;
gap: helpers.space(1);
position: relative;

input {
opacity: 0;
pointer-events: none;
position: absolute;
}

// Hover
input + .ods-switch-indicator:hover {
border-color: #636670;
}

input + .ods-switch-indicator:hover::before {
background-color: #636670;
}

// Checked
input:checked + .ods-switch-indicator {
background-color: helpers.color('background-input-selected');
border-color: helpers.color('border-input-selected');
}

input:checked + .ods-switch-indicator::before {
background-color: helpers.color('background-input');
transform: translateY(-50%) translateX(helpers.space(2));
}

input:checked + .ods-switch-indicator .ods-icon {
opacity: 1;
transform: translateY(-50%) translateX(helpers.space(2));
}

input:checked + .ods-switch-indicator:hover {
background-color: #5666f9;
border-color: #5666f9;
}

input:checked + .ods-switch-indicator:hover .ods-icon {
border-color: #5666f9;
}

// Focus
input:focus + .ods-switch-indicator {
box-shadow: //
0 0 0 helpers.space(0.25) helpers.color('border-focus-inner'),
0 0 0 helpers.space(0.5) helpers.color('border-action-focus');
}

// Disabled
input:disabled + .ods-switch-indicator {
cursor: not-allowed;
}

input:disabled + .ods-switch-indicator,
input:disabled + .ods-switch-indicator:hover,
input:disabled + .ods-switch-indicator .ods-icon,
input:disabled + .ods-switch-indicator:hover .ods-icon {
border-color: helpers.color('border-disabled');
}

input:disabled:checked + .ods-switch-indicator,
input:disabled + .ods-switch-indicator::before {
background-color: helpers.color('background-disabled');
}

input:disabled:checked + .ods-switch-indicator::before {
background-color: #fff;
}
}

.ods-switch-indicator {
$border-width: helpers.space(0.25);

background-color: helpers.color('background-input');
border: $border-width solid helpers.color('border-input');
border-radius: helpers.border-radius('full');
box-sizing: border-box;
height: helpers.space(3);
margin: helpers.space(1.5) helpers.space(1);
position: relative;
width: helpers.space(5);

// slider
&::before {
background-color: #828893;
border-radius: helpers.border-radius('full');
content: '';
display: inline-block;
height: 18px;
left: 1px;
position: absolute;
top: 50%;
transform: translateY(-50%);
transition: transform calc(var(--ods-transition-duration) * 2) ease-out;
width: 18px;
}

// check icon
.ods-icon {
content: '';
fill: helpers.color('border-input-selected');
height: helpers.space(2);
left: 2px;
opacity: 0;
position: absolute;
top: calc(50% - 1px);
transform: translateY(-50%) translateX(0);
transition: transform calc(var(--ods-transition-duration) * 2) ease-out;
width: helpers.space(2);
}
}
}
4 changes: 4 additions & 0 deletions packages/core/src/components/switch/switch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface SwitchProps {
disabled?: boolean;
checked?: boolean;
}
1 change: 1 addition & 0 deletions packages/react/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export * from './select/option';
export * from './select/option-group';
export * from './select/select';
export * from './spinner/spinner';
export * from './switch/switch';
export * from './textarea/textarea';
export * from './tooltip/tooltip';
export * from './validation/validation';
62 changes: 62 additions & 0 deletions packages/react/src/components/switch/switch.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { color, type SwitchProps } from '@onfido/castor';
import { Field, FieldLabel, Fieldset, Switch } from '@onfido/castor-react';
import React, { useState, type ChangeEvent } from 'react';
import { Meta, Story } from '../../../../../docs';

export default {
title: 'React/Switch',
component: Switch,
argTypes: {
checked: {
type: 'boolean',
},
children: { description: 'Acts as a label for the `<input>`.' },
disabled: {
type: 'boolean',
},
},
args: {
children: '',
},
parameters: {},
} as Meta<SwitchProps>;

export const Playground: Story<SwitchProps> = {};

export const Examples: Story<SwitchProps> = {
render: () => {
const [switchState, setSwitchState] = useState(true);
const handleOnChange = (event: ChangeEvent<HTMLInputElement>) => {
setSwitchState(event.target.checked);
};

return (
<Fieldset>
<Field>
<Switch id="test1">
<FieldLabel htmlFor="test1" style={{ width: 250 }}>
Label
</FieldLabel>
</Switch>
</Field>
<Field>
<Switch id="test2" disabled>
<FieldLabel
htmlFor="test2"
style={{ width: 250, color: color('content-disabled') }}
>
Label with disabled switch
</FieldLabel>
</Switch>
</Field>
<Field>
<Switch id="test3" checked={switchState} onChange={handleOnChange}>
<FieldLabel htmlFor="test3" style={{ width: 250 }}>
Label with checked switch
</FieldLabel>
</Switch>
</Field>
</Fieldset>
);
},
};
28 changes: 28 additions & 0 deletions packages/react/src/components/switch/switch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { c, classy, m, type SwitchProps as BaseProps } from '@onfido/castor';
import React, { type FC } from 'react';
import { Icon } from '../icon/icon';

let idCount = 0;

export const Switch: FC<SwitchProps> = ({
id = `castor_switch_${++idCount}`,
disabled,
className,
children,
...props
}) => (
<label
htmlFor={id}
className={classy(className, c('switch-label'), m({ disabled }))}
>
{children && <span>{children}</span>}
<input {...props} type="checkbox" id={id} disabled={disabled} />
<span className={classy(c('switch-indicator'), m({ disabled }))}>
<Icon name="check" aria-hidden="true" />
</span>
</label>
);

export type SwitchProps = BaseProps & Omit<SwitchElementProps, 'children'>;

type SwitchElementProps = JSX.IntrinsicElements['input'];

0 comments on commit aba2a30

Please sign in to comment.