Skip to content

Commit

Permalink
Ever 15000 rover UI add toggle switch component (#341)
Browse files Browse the repository at this point in the history
* add sub-component toggle to input folder

* Toggle component use css variable wherever possible

* allow forwarding ref and use inline-block in div wrapper

* highlight toggle when child element are focused

* add onChange event test and remove console log in example

Co-authored-by: Haosheng Li <haosheng.li@cision.com>
  • Loading branch information
lihnick and lihnick authored Sep 17, 2021
1 parent 9921c16 commit 2733a59
Show file tree
Hide file tree
Showing 10 changed files with 238 additions and 1 deletion.
13 changes: 13 additions & 0 deletions example/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
Tooltip,
Callout,
Input,
Toggle,
InputTime,
Typography,
Modal,
Expand All @@ -44,6 +45,7 @@ const App = () => {
const [tooltipOpen, setTooltipOpen] = useState(false);
const [inputValue, setInputValue] = useState('');
const [inputCheckboxValue, setInputCheckboxValue] = useState(false);
const [inputToggleValue, setInputToggleValue] = useState(false);
const [inputTimeValue, setInputTimeValue] = useState('');
const [isModalOpen, setIsModalOpen] = useState(false);
const [isKiteVisible, setIsKiteVisible] = useState(false);
Expand Down Expand Up @@ -415,6 +417,17 @@ const App = () => {
{JSON.stringify(inputCheckboxValue)}
</Section>

<Section title="Input.Toggle">
<h1>Input.Toggle</h1>
<Toggle
onClick={(e) => setInputToggleValue(e.target.checked)}
checked={inputToggleValue}
/>{' '}
{JSON.stringify(inputToggleValue)}
<Toggle fauxDisabled />
<Toggle checked fauxDisabled />
</Section>

<Section title="InputTime">
<InputTime
value={inputTimeValue}
Expand Down
9 changes: 9 additions & 0 deletions src/components/Input/Toggle/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# \<Toggle\>

A div wrapper with html input element for toggle functionality.

Optional props

- _checked_ default false
- _fauxDisabled_ default false
- Applies the same style as disabled, but, unlike the real thing, doesn't stop propagation of events. Useful for adding tooltips or other helpful behavior when a user tries to interact with a disabled field. Because it doesn't stop click or change events, the consumer is responsible for making faux-disabled fields read-only.
59 changes: 59 additions & 0 deletions src/components/Input/Toggle/Toggle.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
.Toggle {
display: inline-block;
position: relative;
width: 48px;
height: 24px;
background: var(--rvr-gray-40);
border-radius: 24px;
}

.input {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
opacity: 0;
z-index: 1;
cursor: pointer;
border-radius: 24px;
}

.notch {
pointer-events: none;
position: absolute;
width: 16px;
height: 16px;
margin: 4px;
border-radius: 8px;
background: var(--rvr-white);
transition: all var(--rvr-transition-duration-fast) var(--rvr-linear);
box-shadow: 0px 0px 3px 2px rgba(0 0 0 / 20%);
}

.checked {
background: var(--rvr-blue);
}

.disabled {
opacity: 0.5;
}

.disabled > input.input {
cursor: not-allowed;
}

.disabled > span.notch {
box-shadow: none;
}

.checked > span.notch {
transform: translateX(24px);
}

.Toggle:focus-within:not(.disabled) {
outline: 0 transparent none;
box-shadow: 0px 0px 0px 2px var(--rvr-white),
0px 0px 1px 3px var(--rvr-color-primary-hover),
0px 0px 8px 2px var(--rvr-color-primary-hover);
}
48 changes: 48 additions & 0 deletions src/components/Input/Toggle/Toggle.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React from 'react';

import { render } from '@testing-library/react';
import '@testing-library/jest-dom';

import Toggle from './Toggle';

describe('Toggle', () => {
it('renders', () => {
render(<Toggle readOnly />);
});

describe('when rendered with id attribute', () => {
it('should have the id attribute accessible in the dom element', () => {
const { getByTestId } = render(
<Toggle data-testid="toggleElem" id="toggleId" onChange={() => {}} />
);

const toggleElem = getByTestId('toggleElem');
const toggleId = toggleElem.getAttribute('id') || '';
expect(toggleId).toBe('toggleId');
});
});

describe('when clicked with onChange handler', () => {
it('should call the handler with the expected value', async () => {
let checkValue = false;
const changeHandler = () => {
checkValue = !checkValue;
};
const { getByTestId } = render(
<Toggle
data-testid="toggleElem"
onChange={changeHandler}
checked={checkValue}
/>
);

const toggleElem = getByTestId('toggleElem');

toggleElem.click();
expect(checkValue).toBe(true);

toggleElem.click();
expect(checkValue).toBe(false);
});
});
});
46 changes: 46 additions & 0 deletions src/components/Input/Toggle/Toggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React from 'react';

import classNames from 'classnames';

import type { InputProps } from '../Input';

import styles from './Toggle.module.css';

interface ToggleWithRefProps extends InputProps {
fauxDisabled?: boolean;
forwardedRef?: React.Ref<HTMLInputElement>;
}

const ToggleWithRef: React.FC<ToggleWithRefProps> = ({
checked = false,
fauxDisabled = false,
forwardedRef: ref,
className = '',
...passedProps
}) => {
const mainClass = classNames(styles.Toggle, className, {
[styles.checked]: checked,
[styles.disabled]: fauxDisabled,
});

return (
<div role="checkbox" aria-checked={checked} className={mainClass}>
<input
type="checkbox"
aria-hidden
tabIndex={fauxDisabled ? -1 : undefined}
checked={checked}
ref={ref || undefined}
{...passedProps}
className={styles.input}
/>
<span className={styles.notch} />
</div>
);
};

const Toggle = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => (
<ToggleWithRef {...props} forwardedRef={ref || undefined} />
));

export default Toggle;
1 change: 1 addition & 0 deletions src/components/Input/Toggle/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './Toggle';
59 changes: 59 additions & 0 deletions src/components/Input/Toggle/story.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { boolean } from '@storybook/addon-knobs';

import Toggle from './Toggle';
import Readme from './README.md';

import { Wrap } from '../../../stories/storybook-helpers';

storiesOf('Planets/Input/Toggle', module)
.addParameters({
readme: {
sidebar: Readme,
},
})
.add(
'Overview',
() => (
<Wrap>
<Toggle
fauxDisabled={boolean('disabled', false)}
checked={boolean('toggled', false)}
onClick={action('onClick (HTML)')}
/>
</Wrap>
),
{
info: {
inline: false,
source: true,
},
}
)
.add(
'Examples',
() => (
<>
<Wrap>
<Toggle />
</Wrap>
<Wrap>
<Toggle checked />
</Wrap>
<Wrap>
<Toggle fauxDisabled />
</Wrap>
<Wrap>
<Toggle checked fauxDisabled />
</Wrap>
</>
),
{
info: {
inline: false,
source: true,
},
}
);
1 change: 1 addition & 0 deletions src/components/Input/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default } from './Input';
export { default as Checkbox } from './Checkbox';
export { default as Toggle } from './Toggle';
export type { InputProps } from './Input';
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export {
} from './components/TabMenu';

export { default as Tooltip, EasyRichTooltip } from './components/Tooltip';
export { default as Input, Checkbox } from './components/Input';
export { default as Input, Checkbox, Toggle } from './components/Input';
export { default as InputTime } from './components/InputTime';
export { default as Typography } from './components/Typography';
export { default as Modal } from './components/Modal';
Expand Down
1 change: 1 addition & 0 deletions src/stories/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import '../components/Avatar/story';
import '../components/Tooltip/story';
import '../components/Input/story';
import '../components/Input/Checkbox/story';
import '../components/Input/Toggle/story';
import '../components/InputTime/story';
import '../components/Select/story';
import '../components/Typography/story';
Expand Down

0 comments on commit 2733a59

Please sign in to comment.