diff --git a/src/components/Switch/Switch.stories.tsx b/src/components/Switch/Switch.stories.tsx new file mode 100644 index 00000000..422be6aa --- /dev/null +++ b/src/components/Switch/Switch.stories.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { action } from '@storybook/addon-actions'; +import Switch from './Switch'; + +export default { + title: 'Switch', + component: Switch, +}; + +export const DefaultSwitch = () => ( +
+ + + +
+); + +export const DiffTypeSwitch = () => ( +
+ + + +
+); + +export const DiffSizeSwitch = () => ( +
+ + +
+); diff --git a/src/components/Switch/Switch.test.tsx b/src/components/Switch/Switch.test.tsx new file mode 100644 index 00000000..dfd157b7 --- /dev/null +++ b/src/components/Switch/Switch.test.tsx @@ -0,0 +1,110 @@ +import React from 'react'; + +import { render, fireEvent } from '@testing-library/react'; +import Switch, { patSwitchProps } from './Switch'; + +describe('Switch', () => { + it('should match snapshot', () => { + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); + + it('should render default switch', () => { + const switchProps = { + onClick: jest.fn(), + }; + const wrapper = render(); + + // for switch-wrapper + const switch_wrapper = wrapper.queryByTestId( + 'switch-wrapper' + ) as HTMLElement; + expect(switch_wrapper).toBeInTheDocument(); + expect(switch_wrapper.tagName).toBe('DIV'); + expect(switch_wrapper).toHaveClass( + 'switch-wrapper switch-default switch-medium' + ); + + // for input checkbox + const input_checkbox = wrapper.queryByTestId( + 'switch-input' + ) as HTMLInputElement; + expect(input_checkbox).toBeInTheDocument(); + expect(input_checkbox.tagName).toBe('INPUT'); + expect(input_checkbox.type).toBe('checkbox'); + expect(input_checkbox).toBeEnabled(); + expect(input_checkbox.checked).toBe(false); + + // for track + const track = wrapper.queryByTestId('switch-track') as HTMLElement; + expect(track).toBeInTheDocument(); + expect(track.tagName).toBe('SPAN'); + expect(track).toHaveClass('switch__slider'); + + // for label + const label = wrapper.queryByText('Default Switch') as HTMLElement; + expect(label).toBeInTheDocument(); + expect(label.tagName).toBe('LABEL'); + expect(label).toHaveClass('switch__text'); + + expect(switchProps.onClick).toHaveBeenCalledTimes(0); + fireEvent.click(label); + expect(switchProps.onClick).toHaveBeenCalledTimes(1); + expect(input_checkbox.checked).toBe(true); + fireEvent.click(wrapper.queryByTestId('switch') as HTMLElement); + expect(switchProps.onClick).toHaveBeenCalledTimes(2); + expect(input_checkbox.checked).toBe(false); + }); + + it('should render diff size & type switch', () => { + const switchProps: patSwitchProps = { + switchSize: 'small', + switchType: 'primary', + onClick: jest.fn(), + checked: true, + color: "#FFA500", + }; + const wrapper = render(); + + // for switch-wrapper + const switch_wrapper = wrapper.queryByTestId( + 'switch-wrapper' + ) as HTMLElement; + expect(switch_wrapper).toBeInTheDocument(); + expect(switch_wrapper.tagName).toBe('DIV'); + expect(switch_wrapper).toHaveClass( + 'switch-wrapper switch-primary switch-small' + ); + + // for input checkbox + const input_checkbox = wrapper.queryByTestId( + 'switch-input' + ) as HTMLInputElement; + expect(input_checkbox).toBeInTheDocument(); + expect(input_checkbox.tagName).toBe('INPUT'); + expect(input_checkbox.type).toBe('checkbox'); + expect(input_checkbox).toBeEnabled(); + expect(input_checkbox.checked).toBe(true); + + // for track + const track = wrapper.queryByTestId('switch-track') as HTMLElement; + expect(track).toBeInTheDocument(); + expect(track.tagName).toBe('SPAN'); + expect(track).toHaveClass('switch__slider'); + expect(track).toHaveStyle('background-color: #FFA500'); + + // for label + const label = wrapper.queryByText('Default Switch') as HTMLElement; + expect(label).toBeInTheDocument(); + expect(label.tagName).toBe('LABEL'); + expect(label).toHaveClass('switch__text'); + + expect(switchProps.onClick).toHaveBeenCalledTimes(0); + fireEvent.click(label); + expect(switchProps.onClick).toHaveBeenCalledTimes(1); + expect(input_checkbox.checked).toBe(false); + fireEvent.click(wrapper.queryByTestId('switch') as HTMLElement); + expect(switchProps.onClick).toHaveBeenCalledTimes(2); + expect(input_checkbox.checked).toBe(true); + }); +}); diff --git a/src/components/Switch/Switch.tsx b/src/components/Switch/Switch.tsx new file mode 100644 index 00000000..1f2906a1 --- /dev/null +++ b/src/components/Switch/Switch.tsx @@ -0,0 +1,92 @@ +import React, { + ButtonHTMLAttributes, + AnchorHTMLAttributes, + FC, + MouseEvent, +} from 'react'; +import { classNames } from '../../utils/classNames'; + +export interface ISwitchProps { + /** Indicate if the button is disabled*/ + disabled?: boolean; + + /** Inital State of the Switch */ + checked?: boolean; + + /** Text to put besides Switch */ + label?: string; + + /** Type of Switch */ + switchType?: 'default' | 'primary' | 'secondary'; + + /** Size of Switch */ + switchSize?: 'medium' | 'small'; + + /** set action on switch toggled */ + onClick?: (arg0: object) => void; + + /** Pass Customed Color*/ + color?: string; +} + +export type patSwitchProps = ISwitchProps; + +/** + * A Switch provides a toggle checkbox. + * + * ```js + * import {Switch} from 'pat-ui' + * ``` + */ +export const Switch: FC = (props) => { + const [checked, setChecked] = React.useState(props.checked || false); + + const switch_classes = classNames('switch-wrapper', { + [`switch-${props.switchType || 'default'}`]: true, + [`switch-${props.switchSize || 'medium'}`]: true, + [`switch-color-custum`]: props.color ? true : false, + disabled: props.disabled || false, + }); + + return ( +
{ + if (props.onClick) { + props.onClick(event); + } + + if (!props.disabled) { + setChecked((e) => !e); + } + }} + data-testid="switch-wrapper" + > +
+ null} + data-testid="switch-input" + /> + +
+ +
+ ); +}; + +Switch.defaultProps = { + checked: false, + disabled: false, + switchType: 'default', + switchSize: 'medium', +}; + +export default Switch; diff --git a/src/components/Switch/_Switch.scss b/src/components/Switch/_Switch.scss new file mode 100644 index 00000000..8bed978b --- /dev/null +++ b/src/components/Switch/_Switch.scss @@ -0,0 +1,296 @@ +/* Variables For Switch */ + +//Switch +$sw-transition-time: 0.3s; +$sw-thumb-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.12), + 0px 1px 1px 0px rgba(0, 0, 0, 0.14), 0px 2px 1px 0px rgba(0, 0, 0, 0.2); + +//Switch Label +$sw-font-weight-normal: $font-weight-normal; +$sw-font-family: $font-family-base !default; +$sw-font-size: $font-size-base !default; +$sw-line-height: $line-height-base !default; + +//Switch track +$sw-track-width: 34px; +$sw-track-height: 14px; +$sw-track-padding: 12px; +$sw-track-width-small: 26px; +$sw-track-height-small: 10px; +$sw-track-padding-small: 7px; +$sw-track-color-default-off: rgba(0, 0, 0, 0.3799999952316284); +$sw-track-color-default-on: rgba(0, 0, 0, 0.5); +$sw-track-color-disabled: rgba(0, 0, 0, 0.12); +$sw-track-color-primary-off: rgba(0, 0, 0, 0.3799999952316284); +$sw-track-color-primary-on: rgba(63, 81, 181, 0.5); +$sw-track-color-secondary-off: rgba(0, 0, 0, 0.3799999952316284); +$sw-track-color-secondary-on: rgba(245, 0, 87, 0.5); + +//Switch Thumb +$sw-thumb-diameter: 20px; +$sw-thumb-diameter-small: 16px; +$sw-thumb-color-default-off: #fafafa; +$sw-thumb-color-default-on: #fafafa; +$sw-thumb-color-disabled: #bdbdbd; +$sw-thumb-color-primary-off: #fafafa; +$sw-thumb-color-primary-on: #3f51b5; +$sw-thumb-color-secondary-off: #fafafa; +$sw-thumb-color-secondary-on: #f50057; + +@mixin switch-style( + $track-width, + $track-height, + $track-padding, + $thumb-diameter, + $transition-time, + $thumb-shadow, + $track-color-off, + $track-color-on, + $thumb-color-off, + $thumb-color-on +) { + + display: inline-flex; + align-items: center; + box-sizing: content-box; + padding: 0; + margin: 0; + cursor: pointer; + + /* The Label - Text besides the Switch*/ + .switch__text { + font-size: $sw-font-size; + font-family: $sw-font-family; + font-style: normal; + font-weight: $sw-font-weight-normal; + line-height: $sw-line-height; + margin: 0; + cursor: inherit; + } + + /* The switch - the wrapper around the slider */ + .switch { + width: $track-width; + height: $track-height; + padding: $track-padding; + position: relative; + display: inline-block; + box-sizing: content-box; + margin: 0; + + /* Hide default HTML checkbox */ + input { + opacity: 0; + width: 0; + height: 0; + } + + /* The Track of the switch */ + .switch__slider { + position: absolute; + top: $track-padding; + left: $track-padding; + right: $track-padding; + bottom: $track-padding; + background-color: $track-color-off; + border-radius: $track-height/2; + } + + /* Thumb of the Switch */ + .switch__slider:before { + position: absolute; + content: ''; + height: $thumb-diameter; + width: $thumb-diameter; + left: $track-width/2 - $thumb-diameter; + bottom: ($track-height - $thumb-diameter)/2; + background-color: $thumb-color-on; + transition: $transition-time; + border-radius: $thumb-diameter/2; + + /* shadow-1 */ + box-shadow: $thumb-shadow; + } + + input:checked + .switch__slider { + background-color: $track-color-on; + } + + input:checked + .switch__slider::before { + transform: translateX($thumb-diameter); + background-color: $thumb-color-on; + } + } + + .switch:hover .switch__slider::before { + box-shadow: $thumb-shadow, + 0px 0px 0px ($track-height/2 + $track-padding - $thumb-diameter/2) + color-mix(in srgb, currentColor 8%, transparent); + } + + // .switch:hover { + // .switch__slider::before { + // box-shadow: $thumb-shadow, + // 0px 0px 0px ($track-height/2 + $track-padding - $thumb-diameter/2) + // color-mix(in srgb, currentColor 8%, transparent); + // } + // } + +} + +/** Syle Just for changing color */ +@mixin switch-style-color( + $track-color-off, + $track-color-on, + $thumb-color-off, + $thumb-color-on +) { + /* The switch - the wrapper around the slider */ + .switch { + /* The Track of the switch */ + .switch__slider { + background-color: $track-color-off; + } + + /* Thumb of the Switch */ + .switch__slider:before { + background-color: $thumb-color-off; + } + + input:checked + .switch__slider { + background-color: $track-color-on; + } + + input:checked + .switch__slider::before { + background-color: $thumb-color-on; + } + } +} + +/** Style for changing size and thumb shadow*/ +@mixin switch-style-size( + $track-width, + $track-height, + $track-padding, + $thumb-diameter, + $thumb-shadow +) { + /* The switch - the wrapper around the slider */ + .switch { + width: $track-width; + height: $track-height; + padding: $track-padding; + + /* The Track of the switch */ + .switch__slider { + top: $track-padding; + left: $track-padding; + right: $track-padding; + bottom: $track-padding; + border-radius: $track-height/2; + } + + /* Thumb of the Switch */ + .switch__slider::before { + height: $thumb-diameter; + width: $thumb-diameter; + left: $track-width/2 - $thumb-diameter; + bottom: ($track-height - $thumb-diameter)/2; + border-radius: $thumb-diameter/2; + + /* shadow-1 */ + box-shadow: $thumb-shadow; + } + + input:checked + .switch__slider::before { + transform: translateX($thumb-diameter); + } + + } + + :hover .switch__slider::before { + box-shadow: $thumb-shadow, + 0px 0px 0px ($track-height/2 + $track-padding - $thumb-diameter/2) + color-mix(in srgb, currentColor 8%, transparent); + } + +} + +/** Default style */ +.switch-wrapper { + @include switch-style( + $sw-track-width, + $sw-track-height, + $sw-track-padding, + $sw-thumb-diameter, + $sw-transition-time, + $sw-thumb-shadow, + $sw-track-color-default-off, + $sw-track-color-default-on, + $sw-thumb-color-default-off, + $sw-thumb-color-default-on + ); +} + +// Diff Switch Type +.switch-primary { + @include switch-style-color( + $sw-track-color-primary-off, + $sw-track-color-primary-on, + $sw-thumb-color-primary-off, + $sw-thumb-color-primary-on + ); +} + +.switch-secondary { + @include switch-style-color( + $sw-track-color-secondary-off, + $sw-track-color-secondary-on, + $sw-thumb-color-secondary-off, + $sw-thumb-color-secondary-on + ); +} + +// Diff Switch Size +.switch-small { + @include switch-style-size( + $sw-track-width-small, + $sw-track-height-small, + $sw-track-padding-small, + $sw-thumb-diameter-small, + $sw-thumb-shadow + ) +} + +// If disabled +.disabled { + @include switch-style-color( + $sw-track-color-disabled, + $sw-track-color-disabled, + $sw-thumb-color-disabled, + $sw-thumb-color-disabled + ); + + // Cancel Hover shadow + :hover .switch__slider::before { + box-shadow: $sw-thumb-shadow; + } + + cursor: not-allowed; +} + +// If color customed +.switch-color-custum { + .switch { + /* Thumb of the Switch */ + .switch__slider:before { + opacity: 100%; + background-color: inherit; + } + + input:checked + .switch__slider::before { + opacity: 100%; + background-color: inherit; + } + } +} diff --git a/src/components/Switch/__snapshots__/Switch.test.tsx.snap b/src/components/Switch/__snapshots__/Switch.test.tsx.snap new file mode 100644 index 00000000..348dbde6 --- /dev/null +++ b/src/components/Switch/__snapshots__/Switch.test.tsx.snap @@ -0,0 +1,28 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Switch should match snapshot 1`] = ` + +
+
+ + +
+
+
+`; diff --git a/src/components/Switch/index.tsx b/src/components/Switch/index.tsx new file mode 100644 index 00000000..b03db7f5 --- /dev/null +++ b/src/components/Switch/index.tsx @@ -0,0 +1 @@ +export {default} from './Switch'; diff --git a/src/index.tsx b/src/index.tsx index 70aeb63c..ba5d50b6 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -5,3 +5,4 @@ export { default as Message } from './components/Message'; export { default as Card } from './components/Card'; export { default as Dropdown } from './components/Dropdown'; export { default as Progress } from './components/Progress'; +export { default as Switch } from './components/Switch'; diff --git a/src/styles/index.scss b/src/styles/index.scss index 33fa969f..00fa3910 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -12,3 +12,6 @@ @import '../components/Input/Input'; @import '../components/Card/Card'; @import '../components/Progress/Progress'; + +//Switch +@import '../components/Switch/Switch';