Skip to content

Commit

Permalink
feat(community-toggle-switch): idd sLoading and ref props
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Toggle no longer automatically sets focus to itself. Manually control focus using ref.
  • Loading branch information
invalidred authored and theetrain committed Nov 22, 2019
1 parent 4dd9d0f commit affec8e
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 81 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ To learn how to make contributions to TDS Community, See the [contributing guide
| :---: | :---: | :---: | :---: | :---: | :---: | :---: |
| [<img src="https://avatars3.githubusercontent.com/u/42220619?v=4" width="100px;"/><br /><sub><b>Christina L.</b></sub>](https://github.com/Christina-Lo)<br />[](#tds-Christina-Lo "") | [<img src="https://avatars0.githubusercontent.com/u/931411?v=4" width="100px;"/><br /><sub><b>Andrew Lam</b></sub>](https://github.com/Andrew-K-Lam)<br />[](#tds-Andrew-K-Lam "") | [<img src="https://avatars0.githubusercontent.com/u/1036187?v=4" width="100px;"/><br /><sub><b>Jordan Raffoul</b></sub>](http://jordanraffoul.com)<br />[](#tds-jraff "") | [<img src="https://avatars2.githubusercontent.com/u/22725151?v=4" width="100px;"/><br /><sub><b>Nicholas Mak</b></sub>](https://github.com/nicmak)<br />[](#tds-nicmak "") | [<img src="https://avatars1.githubusercontent.com/in/2740?v=4" width="100px;"/><br /><sub><b>renovate[bot]</b></sub>](https://github.com/apps/renovate)<br />[](#tds-renovate[bot] "") | [<img src="https://avatars1.githubusercontent.com/u/3803746?v=4" width="100px;"/><br /><sub><b>Mike Bunce</b></sub>](https://github.com/sketchidea)<br />[](#tds-sketchidea "") | [<img src="https://avatars2.githubusercontent.com/u/2739819?v=4" width="100px;"/><br /><sub><b>Ani</b></sub>](https://github.com/simpleimpulse)<br />[](#tds-simpleimpulse "") |
| [<img src="https://avatars0.githubusercontent.com/u/1015398?v=4" width="100px;"/><br /><sub><b>Samantha Vale</b></sub>](https://github.com/karlasamantha)<br />[](#tds-karlasamantha "") | [<img src="https://avatars0.githubusercontent.com/u/10473576?v=4" width="100px;"/><br /><sub><b>Tyler Dewald</b></sub>](https://github.com/DewaldoDev)<br />[](#tds-DewaldoDev "") | [<img src="https://avatars1.githubusercontent.com/u/3495961?v=4" width="100px;"/><br /><sub><b>Varun Jain</b></sub>](https://github.com/varunj90)<br />[](#tds-varunj90 "") | [<img src="https://avatars0.githubusercontent.com/u/5270458?v=4" width="100px;"/><br /><sub><b>abdul khan</b></sub>](https://github.com/invalidred)<br />[](#tds-invalidred "") | [<img src="https://avatars3.githubusercontent.com/u/4324431?v=4" width="100px;"/><br /><sub><b>Nate X</b></sub>](https://github.com/nateriesling)<br />[](#tds-nateriesling "") | [<img src="https://avatars1.githubusercontent.com/u/3803746?v=4" width="100px;"/><br /><sub><b>Mike Bunce</b></sub>](https://github.com/mike-bunce)<br />[](#tds-mike-bunce "") | [<img src="https://avatars3.githubusercontent.com/u/21316148?v=4" width="100px;"/><br /><sub><b>Donna Vitan</b></sub>](http://donnavitan.com)<br />[](#tds-donnavitan "") |

<!-- ALL-CONTRIBUTORS-LIST:END -->

[circle-url]: https://circleci.com/gh/telus/tds-community
Expand Down
159 changes: 83 additions & 76 deletions packages/ToggleSwitch/ToggleSwitch.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from 'react'
import React from 'react'
import PropTypes from 'prop-types'

import { safeRest } from '@tds/util-helpers'
Expand All @@ -14,87 +14,86 @@ import warn from '../../shared/utils/warn'
* @version ./package.json
*/

const ToggleSwitch = ({
id,
label,
tooltipText,
checked,
onClick,
tooltipCopy,
spinnerLabel,
...rest
}) => {
if (tooltipText && !tooltipCopy) {
warn('@tds/community-toggle-switch', 'You must provide tooltipCopy when using tooltipText')
}

const labelledById = `${id}-label`
const buttonRef = React.useRef()

const [isPressed, setIsPressed] = useState(checked)
const [isLoading, setIsLoading] = useState(false)
const [isSpinning, setIsSpinning] = useState(false)

React.useEffect(() => {
setIsSpinning(false)
setIsLoading(false)
}, [checked])
const ToggleSwitch = React.forwardRef(
(
{
id,
label,
tooltipText,
checked,
onClick,
tooltipCopy,
spinnerLabel,
autofocus,
isLoading,
...rest
},
ref
) => {
if (tooltipText && !tooltipCopy) {
warn('@tds/community-toggle-switch', 'You must provide tooltipCopy when using tooltipText')
}

React.useEffect(() => {
const timer = setTimeout(() => {
if (isLoading) {
setIsSpinning(true)
} else {
setIsSpinning(false)
const labelledById = `${id}-label`
const buttonRef = React.useRef()

/* The purpose of this hook is to allow the parent
to focus on the ToggleSwitch at will by forwarding
a ref to parent and exposing ONLY a single `focus` method
*/
React.useImperativeHandle(ref, () => ({
focus: () => {
buttonRef.current.focus()
},
}))

React.useEffect(() => {
if (autofocus) {
buttonRef.current.focus()
}
}, 250) // time needed for a slider to move
return () => {
buttonRef.current.focus()
clearTimeout(timer)
}
}, [isLoading])
/* If either checked or isLoading changes we need
to focus on buttonRef ONLY when autofocus is set
*/
}, [checked, isLoading])

const handleClick = event => {
setIsPressed(!isPressed)
setIsLoading(true)
onClick(event)
}
const handleTooltipClick = event => {
event.preventDefault()
}

const handleTooltipClick = event => {
event.preventDefault()
return (
<StyledLabel htmlFor={id}>
<Box inline between={2}>
<Text id={labelledById} size="medium">
{label}
</Text>
{tooltipText && tooltipCopy && (
<Tooltip onClick={handleTooltipClick} copy={tooltipCopy}>
{tooltipText}
</Tooltip>
)}
</Box>
<InputSwitchWrapper>
<Spinner tag="span" spinning={isLoading} label={spinnerLabel} size="small" inline>
<Button
{...safeRest(rest)}
id={id}
role="switch"
aria-checked={checked}
aria-labelledby={labelledById}
data-testid={`${id}-switch`}
onClick={!isLoading ? onClick : null}
ref={buttonRef}
>
<Slider pressed={checked} />
</Button>
</Spinner>
</InputSwitchWrapper>
</StyledLabel>
)
}
)

return (
<StyledLabel htmlFor={id}>
<Box inline between={2}>
<Text id={labelledById} size="medium">
{label}
</Text>
{tooltipText && tooltipCopy && (
<Tooltip onClick={handleTooltipClick} copy={tooltipCopy}>
{tooltipText}
</Tooltip>
)}
</Box>
<InputSwitchWrapper>
<Spinner tag="span" spinning={isSpinning} label={spinnerLabel} size="small" inline>
<Button
{...safeRest(rest)}
id={id}
role="switch"
aria-checked={checked}
aria-labelledby={labelledById}
data-testid={`${id}-switch`}
onClick={!isLoading ? handleClick : null}
ref={buttonRef}
>
<Slider pressed={isPressed} />
</Button>
</Spinner>
</InputSwitchWrapper>
</StyledLabel>
)
}
ToggleSwitch.displayName = 'ToggleSwitch'

ToggleSwitch.propTypes = {
/** The unique HTML id for this form input. */
Expand All @@ -118,12 +117,20 @@ ToggleSwitch.propTypes = {
/** A callback function to be invoked when the ToggleSwitch button is clicked on.
@param {SyntheticEvent} event The React `SyntheticEvent` */
onClick: PropTypes.func.isRequired,

/** Boolean to automatically focus on ToggleSwitch after interacting with it */
autofocus: PropTypes.bool,

/** Boolean to show or hide spinner */
isLoading: PropTypes.bool,
}

ToggleSwitch.defaultProps = {
checked: false,
tooltipText: undefined,
tooltipCopy: undefined,
autofocus: false,
isLoading: false,
}

export default ToggleSwitch
118 changes: 117 additions & 1 deletion packages/ToggleSwitch/ToggleSwitch.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ Toggle switches are digital on/off switches. They prompt users to choose between
- May group similar toggles; grouped toggles do not affect each other as each toggle provides on/off states for a specific option
- `ToggleSwitch` component is a controlled component and requires an `onClick` handler
- Use the `ToggleSwitch` button to trigger an API request or change the state of your application
- Use the `isLoading` prop to show overlay spinner above `ToggleSwitch` whil API request is processing (see example _Asynchoronous usage_)
- Use the `autofocus` prop to automatically focus back to `ToggleSwitch` between state transitions (see example _Asynchoronous usage with autofocus back on toggle after checked prop transition_)
- Use the `ref` prop to mangually focus on `ToggleSwitch` (see example _Manual focus using Ref_)
- If the intent is to use it as a form input element with a designated value, please use either [@tds/core-checkbox](https://tds.telus.com/components/index.html#checkbox) or [@tds/core-radio](https://tds.telus.com/components/index.html#radio)
- By default, the `ToggleSwitch` button is right aligned of the container
- The distance between the label and the button can be controlled by the width of the container that wraps `ToggleSwitch`. Highly recommend to limit the width of the `ToggleSwitch` button so that it's not too far from the label for better user experience and accessibility. Use `FlexGrid` as a container to control the distance between the button and label. Please refer to the `ToggleSwitch` example sandbox
Expand All @@ -19,14 +22,125 @@ Toggle switches are digital on/off switches. They prompt users to choose between
- `ToggleSwitch` component is an html `<button>` element with a role `switch` that has two states `aria-checked="true"` or `aria-checked="false"`
- `ToggleSwitch` has a built-in `Spinner` that requires an assistive text. You must provide this assistive text by passing a string to `spinnerLabel` prop

**Basic usage**

```jsx
const App = () => {
const [isChecked, setIsChecked] = React.useState(false)

const handleClick = event => {
/* this setTimeout imitates an async call to an API */
setIsChecked(!isChecked)
}

return (
<FlexGrid gutter={false}>
<FlexGrid.Row>
<FlexGrid.Col xs={12} md={3}>
<ToggleSwitch
id="toggle-accessibility"
label="Enable data"
tooltipCopy="en"
tooltipText="Tool Tip Text"
checked={isChecked}
onClick={handleClick}
/>
</FlexGrid.Col>
</FlexGrid.Row>
</FlexGrid>
)
}

;<App />
```

**Asynchoronous usage**

```jsx
const App = () => {
const [isChecked, setIsChecked] = React.useState(false)
const [isLoading, setIsLoading] = React.useState(false)
const handleClick = event => {
setIsLoading(true)
setTimeout(() => {
setIsChecked(!isChecked)
setIsLoading(false)
}, 2000)
}

return (
<FlexGrid gutter={false}>
<FlexGrid.Row>
<FlexGrid.Col xs={12} md={3}>
<ToggleSwitch
id="toggle-accessibility"
label="Enable data"
tooltipCopy="en"
tooltipText="Tool Tip Text"
spinnerLabel="Request is processing."
checked={isChecked}
onClick={handleClick}
isLoading={isLoading}
/>
</FlexGrid.Col>
</FlexGrid.Row>
</FlexGrid>
)
}

;<App />
```

**Asynchoronous usage with autofocus back on toggle after checked prop transition**

```jsx
const App = () => {
const [isChecked, setIsChecked] = React.useState(false)
const [isLoading, setIsLoading] = React.useState(false)
const handleClick = event => {
setIsLoading(true)
setTimeout(() => {
setIsChecked(!isChecked)
setIsLoading(false)
}, 2000)
}

return (
<FlexGrid gutter={false}>
<FlexGrid.Row>
<FlexGrid.Col xs={12} md={3}>
<ToggleSwitch
id="toggle-accessibility"
label="Enable data"
tooltipCopy="en"
tooltipText="Tool Tip Text"
spinnerLabel="Request is processing."
checked={isChecked}
onClick={handleClick}
isLoading={isLoading}
autofocus
/>
</FlexGrid.Col>
</FlexGrid.Row>
</FlexGrid>
)
}

;<App />
```

**Manual focus using Ref**

```jsx
const App = () => {
const [isChecked, setIsChecked] = React.useState(false)
const [isLoading, setIsLoading] = React.useState(false)
const toggleSwitchRef = React.useRef()
const handleClick = event => {
setIsLoading(true)
setTimeout(() => {
setIsChecked(!isChecked)
setIsLoading(false)
toggleSwitchRef.current.focus()
}, 2000)
}

Expand All @@ -35,13 +149,15 @@ const App = () => {
<FlexGrid.Row>
<FlexGrid.Col xs={12} md={3}>
<ToggleSwitch
ref={toggleSwitchRef}
id="toggle-accessibility"
label="Enable data"
tooltipCopy="en"
tooltipText="Tool Tip Text"
spinnerLabel="Request is processing."
checked={isChecked}
onClick={handleClick}
isLoading={isLoading}
/>
</FlexGrid.Col>
</FlexGrid.Row>
Expand Down
12 changes: 12 additions & 0 deletions packages/ToggleSwitch/__tests__/ToggleSwitch.spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,4 +173,16 @@ describe('ToggleSwitch', () => {
'some value'
)
})

it('should not show spinner when isLoading=false', () => {
const toggleSwitch = doShallow({ isLoading: false })
const spinner = toggleSwitch.find('Spinner')
expect(spinner.prop('spinning')).toEqual(false)
})

it('show spinner when isLoading=true', () => {
const toggleSwitch = doShallow({ isLoading: true })
const spinner = toggleSwitch.find('Spinner')
expect(spinner.prop('spinning')).toEqual(true)
})
})
Loading

0 comments on commit affec8e

Please sign in to comment.