diff --git a/packages/core/src/components/index.scss b/packages/core/src/components/index.scss index fc1dd84aa..93892b2e6 100644 --- a/packages/core/src/components/index.scss +++ b/packages/core/src/components/index.scss @@ -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(); @@ -69,4 +71,5 @@ @include tooltip.Tooltip(); @include validation.Validation(); @include accordion.Accordion(); + @include switch.Switch(); } diff --git a/packages/core/src/components/index.ts b/packages/core/src/components/index.ts index cf9e912c1..faa986538 100644 --- a/packages/core/src/components/index.ts +++ b/packages/core/src/components/index.ts @@ -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'; diff --git a/packages/core/src/components/switch/switch.scss b/packages/core/src/components/switch/switch.scss new file mode 100644 index 000000000..e07596560 --- /dev/null +++ b/packages/core/src/components/switch/switch.scss @@ -0,0 +1,136 @@ +@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-grid; + gap: helpers.space(1); + grid-template-columns: 1fr auto; + position: relative; + + input { + opacity: 0; + 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::after { + opacity: 1; + transform: translateY(-50%) translateX(helpers.space(1)) rotateZ(45deg) + scale(0.5); + } + + input:checked + .ods-switch-indicator:hover { + background-color: #5666f9; + border-color: #5666f9; + } + + input:checked + .ods-switch-indicator:hover::after { + 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::after, + input:disabled + .ods-switch-indicator:hover::after { + 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(0.5); + position: relative; + transition: var(--ods-transition-duration) ease; + transition-property: background-color, border-color; + 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: background-color linear 0.2s, + transform calc(var(--ods-transition-duration) * 2) ease; + width: 18px; + } + + // check icon + &::after { + border: solid helpers.color('border-input-selected'); + border-width: 0 helpers.space(0.5) helpers.space(0.5) 0; + content: ''; + display: inline-block; + left: helpers.space(1.5); + opacity: 0; + padding: helpers.space(1) helpers.space(0.5); + position: relative; + top: calc(50% - 1px); + transform: translateY(-50%) translateX(-100%) rotateZ(-45deg) scale(0); + transition: calc(var(--ods-transition-duration) * 2) ease; + transition-property: opacity, transform; + } + } +} diff --git a/packages/core/src/components/switch/switch.ts b/packages/core/src/components/switch/switch.ts new file mode 100644 index 000000000..49df94d5c --- /dev/null +++ b/packages/core/src/components/switch/switch.ts @@ -0,0 +1,8 @@ +import { type ChangeEvent } from 'react'; + +export interface SwitchProps { + id?: string; + disabled?: boolean; + onChange: (event: ChangeEvent) => void; + className?: string; +} diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts index b19eb3447..7c2efbe88 100644 --- a/packages/react/src/components/index.ts +++ b/packages/react/src/components/index.ts @@ -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'; diff --git a/packages/react/src/components/switch/switch.stories.tsx b/packages/react/src/components/switch/switch.stories.tsx new file mode 100644 index 000000000..fc0e277af --- /dev/null +++ b/packages/react/src/components/switch/switch.stories.tsx @@ -0,0 +1,67 @@ +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: { + onChange: { + description: 'Called when checked value changed', + }, + }, + args: { + disabled: false, + onChange: (event) => { + console.log(event?.target.checked); + }, + }, + parameters: {}, +} as Meta; + +export const Playground: Story = {}; + +export const Examples: Story = { + render: () => { + const [switchState, setSwitchState] = useState(true); + const handleOnChange = (event: ChangeEvent) => { + setSwitchState(event.target.checked); + }; + + return ( +
+ + { + console.log(event.target.checked); + }} + > + Label + + + + { + console.log(event.target.checked); + }} + disabled + > + + Label with disabled switch + + + + + + + Label with checked switch + + + +
+ ); + }, +}; diff --git a/packages/react/src/components/switch/switch.tsx b/packages/react/src/components/switch/switch.tsx new file mode 100644 index 000000000..a90fca5e5 --- /dev/null +++ b/packages/react/src/components/switch/switch.tsx @@ -0,0 +1,32 @@ +import { c, classy, m, SwitchProps as BaseProps } from '@onfido/castor'; +import React, { type FC } from 'react'; + +export const Switch: FC = ({ + id = `switch-${++idCount}`, + disabled, + className, + children, + onChange, + ...props +}) => ( + +); + +export type SwitchProps = BaseProps & Omit; + +type SwitchElementProps = JSX.IntrinsicElements['input']; + +let idCount = 0;