Skip to content

Commit

Permalink
adding a switch components
Browse files Browse the repository at this point in the history
  • Loading branch information
Nick Grato committed Dec 22, 2022
1 parent 6947ad9 commit 0e9f7f0
Show file tree
Hide file tree
Showing 8 changed files with 238 additions and 30 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
dist
node_modules
node_modules

# VS code local settings
/.vscode
67 changes: 39 additions & 28 deletions src/components/Input/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type InputProps = {
onChange?: Function;
validator?: Function;
required?: boolean;
isError?: boolean;
customErrorMessage?: string;
pattern?: string;
maxLength?: number;
Expand All @@ -68,6 +69,7 @@ const Input = forwardRef(
onFocus,
validator = () => true,
required = false,
isError,
customErrorMessage,
pattern,
maxLength,
Expand All @@ -81,8 +83,12 @@ const Input = forwardRef(
) => {
const [isValid, setIsValid] = useState<boolean>(false);
const [isDirty, setIsDirty] = useState<boolean>(false);
const [showError, setShowError] = useState<boolean>(false);
const [showInfo, setShowInfo] = useState<boolean>(false);
const [initialValue, setInitialValue] = useState(value); // used to check if dirty
const [currentErrorMessage, setCurrentErrorMessage] = useState<string>('');
const [currentErrorMessage, setCurrentErrorMessage] = useState<string>(
customErrorMessage ? customErrorMessage : ''
);
const inputRef = useRef<HTMLInputElement>(null);

/**
Expand All @@ -95,6 +101,38 @@ const Input = forwardRef(
};
});

/**
* Validate Input
*/
useEffect(() => {
const input = inputRef.current;
if (!input) return;

const valid = input.validity.valid;
const validationMessage = input.validationMessage;
const validation = valid && validator(value);

// Prop error message takes precedence
if (!validation) {
const message = customErrorMessage
? customErrorMessage
: validationMessage;
message ? setCurrentErrorMessage(message) : null;
}
setIsValid(validation);
}, [value, customErrorMessage, validator]);

/**
* Error UI logic
*/
useEffect(() => {
setShowError(isError || (isDirty && !isValid));
}, [isDirty, isValid, isError]);

useEffect(() => {
setShowInfo(Boolean(info.length) && !showError);
}, [info, showError]);

/**
* Handle Input Change
* @param event
Expand Down Expand Up @@ -126,33 +164,6 @@ const Input = forwardRef(
typeof onFocus === 'function' && onFocus();
};

/**
* Validate Input
*/
useEffect(() => {
const input = inputRef.current;
if (!input) return;

const valid = input.validity.valid;
const validationMessage = input.validationMessage;
const validation = valid && validator(value);

// Prop error message takes precedence
if (!validation) {
const message = customErrorMessage
? customErrorMessage
: validationMessage;
message ? setCurrentErrorMessage(message) : null;
}
setIsValid(validation);
}, [value, customErrorMessage, validator]);

/**
* Error UI logic
*/
const showError = isDirty && !isValid;
const showInfo = info.length && !showError;

return (
<div
className={`${styles.input_wrapper} ${
Expand Down
80 changes: 80 additions & 0 deletions src/components/Switch/Switch.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
@use '../../styles/boilerplate.scss'as *;

.wrapper {
display: flex;
align-items: center;

&:hover {
.handle_shadow {
background: rgba(0, 0, 0, 0.1);
}
}

button {
border: 0px;
background-color: transparent;
position: relative;
cursor: pointer;
}

button.disabled {
cursor: not-allowed;
}

label {
@include font-size(16);
color: $color-text-main;
line-height: 24px;
font-weight: 600;
margin-left: 12px;
}
}

.track {
height: 16px;
width: 40px;
background: #e0e0e0; // need to add this to theme.
border-radius: 8px;
}

@mixin handle {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
height: 24px;
width: 24px;
border-radius: 40px;
top: -3px;
left: -4px;
transition: 500ms background ease-in-out, 500ms transform ease-in-out;
}

.handle {
&_on {
@include handle;
transform: translateX(130%);
background: $color-interaction-primary;
}

&_off {
@include handle;
transform: translateX(-5%);
background: #676767; // need to add this to theme.
}

&_shadow {
position: absolute;
height: 35px;
width: 35px;
border-radius: 40px;
top: -5px;
left: -5px;
background: rgba(0, 0, 0, 0);
transition: 250ms background ease-in-out;
}
}

.icon {
stroke: #ffffff;
}
76 changes: 76 additions & 0 deletions src/components/Switch/Switch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React, { useState, useEffect } from 'react';
import Icon, { IconT } from '../Icon';
import styles from './Switch.module.scss';

export type SwitchPropsT = {
label?: string;
defaultState?: boolean;
disabled?: boolean;
onChange: Function;
icon: IconT;
iconOn: IconT; // Note: to use "iconOn", "icon" needs to have a value.
classProp?: string;
};

const Switch = ({
label = 'switch',
defaultState = false,
disabled,
icon,
iconOn,
onChange,
classProp = '',
}: SwitchPropsT) => {
const [isOn, setIsOn] = useState(defaultState);
const [currentIcon, setCurrentIcon] = useState<IconT | undefined>(
defaultState === true && iconOn !== undefined ? iconOn : icon
);

useEffect(() => {
typeof onChange === 'function' && onChange(isOn);
}, [isOn]);

useEffect(() => {
// If "on" find out if we put iconOn up or not
if (isOn) {
if (icon && iconOn) {
setCurrentIcon(iconOn);
return;
}
setCurrentIcon(icon);
return;
}

// if "off" Icon is the default icon
icon && setCurrentIcon(icon);
}, [isOn, icon, iconOn]);

const handleSwitchClick = () => {
if (disabled) return;
setIsOn((state) => !state);
};

return (
<div className={`${classProp} ${styles.wrapper}`}>
<button
aria-label={label}
role="switch"
type="button"
onClick={handleSwitchClick}
className={disabled && styles.disabled}
>
<div className={styles.track}></div>
<div className={isOn ? styles.handle_on : styles.handle_off}>
<div className={styles.handle_shadow}></div>
{currentIcon && (
<Icon classProp={styles.icon} size={16} name={currentIcon} />
)}
</div>
</button>

{label && <label>{label}</label>}
</div>
);
};

export default Switch;
1 change: 1 addition & 0 deletions src/components/Switch/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './Switch';
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export {
TextAreaInterfaceT,
TextAreaIconColorE,
} from './TextArea';
export { default as Switch } from './Switch';
export { default as Select, OptionT, SelectInterfaceT } from './Select';
export { default as ProgressBar } from './ProgressBar';
export { default as RadioButton } from './RadioButton';
Expand Down
2 changes: 1 addition & 1 deletion src/stories/Input.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default {
const Template: ComponentStory<typeof Input> = (args) => {
const [value, setValue] = useState(initialValues.firstName);
const onChange = (e: any) => {
setValue(e.target.valid);
setValue(e.target.value);
};

const [darkValue, setDarkValue] = useState(initialValues.firstName);
Expand Down
36 changes: 36 additions & 0 deletions src/stories/Switch.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { Switch } from '../components';

export default {
title: 'Example/Switch',
component: Switch,
} as ComponentMeta<typeof Switch>;

const onChange = (value: any) => {
console.log('onchange value', value);
};

const Template: ComponentStory<typeof Switch> = (args) => (
<>
<main data-theme="light" style={{ padding: '20px' }}>
<h3>Light Theme</h3>
<Switch
{...args}
onChange={onChange}
icon="x"
iconOn="check"
disabled={true}
/>
</main>
<main data-theme="dark" style={{ background: '#000000', padding: '20px' }}>
<h3 style={{ color: '#ffffff' }}>Dark Theme</h3>
<Switch {...args} />
</main>
</>
);

export const Main = Template.bind({});
Main.args = {
label: 'Switch me!',
};

0 comments on commit 0e9f7f0

Please sign in to comment.