Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

**Feature:** TopbarSelect hooks #943

Merged
merged 14 commits into from
Mar 11, 2019
102 changes: 31 additions & 71 deletions src/TopbarSelect/TopbarSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { Cancelable } from "lodash"
import debounce from "lodash/debounce"
import * as React from "react"
import React, { useLayoutEffect, useRef, useState } from "react"

import ContextMenu, { ContextMenuProps } from "../ContextMenu/ContextMenu"
import Icon from "../Icon/Icon"
Expand All @@ -19,10 +17,6 @@ export interface TopbarSelectProps {
onChange?: (newLabel: string | React.ReactElement<any>) => void
}

export interface State {
renderedWidth?: number
}

const TopbarSelectContainer = styled("div")<{ isActive: boolean }>`
line-height: 1;
height: ${props => props.theme.topbarHeight}px;
Expand Down Expand Up @@ -65,73 +59,39 @@ const TopbarSelectLabel = styled("p")`
font-weight: ${props => props.theme.font.weight.medium};
`

class TopbarSelect extends React.Component<TopbarSelectProps, Readonly<State>> {
public state: State = {
renderedWidth: undefined,
}

private containerRef = React.createRef<HTMLDivElement>()

public componentDidMount() {
this.updateRenderedWidth()
window.addEventListener("resize", this.handleResize)
}

public componentWillUnmount() {
window.removeEventListener("resize", this.handleResize)
}

public componentDidUpdate() {
this.updateRenderedWidth()
}
const TopbarSelect = ({ label, selected, items, onChange, ...props }: TopbarSelectProps) => {
const [containerWidth, setContainerWidth] = useState(0)
TejasQ marked this conversation as resolved.
Show resolved Hide resolved

/**
* Explicit typing is required here in order to give the typescript compiler access to typings
* used to work out type definitions for the debounce method.
* @todo look into making this unnecessary.
*/
public handleResize: (() => void) & Cancelable = debounce(() => {
this.updateRenderedWidth()
}, 200)
const containerRef = useRef<HTMLDivElement>(null)

private updateRenderedWidth() {
if (!this.containerRef || this.containerRef.current === null) {
return
}
const node = this.containerRef.current
const renderedWidth = node.clientWidth
if (renderedWidth !== this.state.renderedWidth) {
this.setState(() => ({
renderedWidth,
}))
useLayoutEffect(() => {
if (containerRef.current) {
setContainerWidth(containerRef.current.clientWidth)
}
}

public render() {
const { label, selected, items, onChange, ...props } = this.props
return (
<ContextMenu
condensed
items={items}
width={this.state.renderedWidth}
onClick={newItem => {
if (onChange) {
onChange(newItem.label)
}
}}
>
{isActive => (
<TopbarSelectContainer {...props} isActive={isActive} ref={this.containerRef}>
<TopbarSelectLabel>{label}</TopbarSelectLabel>
<TopbarSelectValue>
<TopbarSelectValueSpan active={Boolean(selected)}>{selected}</TopbarSelectValueSpan>
<Icon color="color.text.lightest" name={isActive ? "CaretUp" : "CaretDown"} size={12} />
</TopbarSelectValue>
</TopbarSelectContainer>
)}
</ContextMenu>
)
}
})

return (
<ContextMenu
condensed
items={items}
width={containerWidth}
onClick={newItem => {
if (onChange) {
onChange(newItem.label)
}
}}
>
{isActive => (
<TopbarSelectContainer {...props} isActive={isActive} ref={containerRef}>
<TopbarSelectLabel>{label}</TopbarSelectLabel>
<TopbarSelectValue>
<TopbarSelectValueSpan active={Boolean(selected)}>{selected}</TopbarSelectValueSpan>
<Icon color="color.text.lightest" name={isActive ? "CaretUp" : "CaretDown"} size={12} />
</TopbarSelectValue>
</TopbarSelectContainer>
)}
</ContextMenu>
)
}

export default TopbarSelect
22 changes: 22 additions & 0 deletions src/useDebounce/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Creates a debounced [state,setState] method that delays setting the new state until after desired milliseconds have elapsed since the last time an attempt to update the state was made.
JoshRosenstein marked this conversation as resolved.
Show resolved Hide resolved
JoshRosenstein marked this conversation as resolved.
Show resolved Hide resolved

## Basic Usage

```jsx
import React, { useState, useEffect } from "react"

const MyComponent = ({ defaultValue }) => {
const [value, setValue] = useState(defaultValue)
const debouncedText = useDebounce(value, 2000)

return (
<div>
<input defaultValue={defaultValue} onChange={e => setValue(e.target.value)} />
<p>Debounced value: {debouncedText}</p>
<p>Current value: {value}</p>
</div>
)
}

;<MyComponent defaultValue="Hello world" />
```
36 changes: 36 additions & 0 deletions src/useDebounce/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useEffect, useState } from "react"

// ref https://dev.to/gabe_ragland/debouncing-with-react-hooks-jci

/**
* debounce hook - debounces values
*/
export function useDebounce<T>(value: T, delay: number) {
// State and setters for debounced value
const [debouncedValue, setDebouncedValue] = useState(value)

useEffect(() => {
// Set debouncedValue to value (passed in) after the specified delay
const handler = setTimeout(() => {
setDebouncedValue(value)
}, delay)

// Return a cleanup function that will be called every time ...
// ... useEffect is re-called. useEffect will only be re-called ...
// ... if value changes (see the inputs array below).
// This is how we prevent debouncedValue from changing if value is ...
// ... changed within the delay period. Timeout gets cleared and restarted.
// To put it in context, if the user is typing within our app's ...
// ... search box, we don't want the debouncedValue to update until ...
// ... they've stopped typing for more than 500ms.
return () => {
clearTimeout(handler)
}
}, // You could also add the "delay" var to inputs array if you ... // Only re-call effect if value changes
// ... need to be able to change that dynamically.
[value, delay])

return debouncedValue
}

export default useDebounce
26 changes: 26 additions & 0 deletions src/useDebouncedCallback/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
## Usage

Similar to UseDebounce but a debounced Callback is returned and you can do prelim calculations prior to setting the Value.
JoshRosenstein marked this conversation as resolved.
Show resolved Hide resolved

```jsx
//import useDebouncedCallbacks from '@operational/ui/src/useDebouncedCallbacks';
JoshRosenstein marked this conversation as resolved.
Show resolved Hide resolved
function UseDebouncedCBExample({ defaultValue }) {
const [value, setValue] = useState(defaultValue)
const debouncedFunction = useDebouncedCallback(
value => {
setValue(value)
},
2000,
[],
)

return (
<div>
<input defaultValue={defaultValue} onChange={e => debouncedFunction(e.target.value)} />
<p>Debounced value: {value}</p>
</div>
)
}

;<UseDebouncedCBExample defaultValue="Hello world" />
```
30 changes: 30 additions & 0 deletions src/useDebouncedCallback/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { DependencyList, useCallback, useEffect, useRef } from "react"
TejasQ marked this conversation as resolved.
Show resolved Hide resolved

type AnyFunc = (...args: any[]) => any

type ArgumentTypes<F extends AnyFunc> = F extends (...args: infer A) => any ? A : never

/**
* useDebounced Callback wraps a callback within useCallback and returns
JoshRosenstein marked this conversation as resolved.
Show resolved Hide resolved
*/

export function useDebouncedCallback<T extends AnyFunc>(callback: T, delay: number, deps: DependencyList) {
const fnTimeoutHandler = useRef<any>(null)
const debouncedFn = useCallback(callback, deps)

useEffect(
() => () => {
clearTimeout(fnTimeoutHandler.current)
},
[],
)

return (...args: ArgumentTypes<T>) => {
clearTimeout(fnTimeoutHandler.current)
fnTimeoutHandler.current = setTimeout(() => {
debouncedFn(...args)
}, delay)
}
}

export default useDebouncedCallback
21 changes: 21 additions & 0 deletions src/useWindowEventListener/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
## Usage

This hook is used to attach a callback to a window event.

```jsx
const Example = () => {
const [pressedKeys, setKeys] = useState([])
useWindowEventListener("keypress", ev => {
setKeys([...keys, ev.key])
})

return (
<div>
<strong>Press any keys: </strong>
<output>{pressedKeys.join(" > ")}</output>
</div>
)
}

;<Example />
```
19 changes: 19 additions & 0 deletions src/useWindowEventListener/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useEffect } from "react"

/**
* Hook version of window.addEventListener.
*/

function useWindowEventListener<K extends keyof WindowEventMap>(
type: K,
listener: (ev: WindowEventMap[K]) => any,
JoshRosenstein marked this conversation as resolved.
Show resolved Hide resolved
addOptions?: boolean | AddEventListenerOptions,
removeOptions?: boolean | EventListenerOptions,
TejasQ marked this conversation as resolved.
Show resolved Hide resolved
) {
useEffect(() => {
window.addEventListener(type, listener, addOptions)
return () => window.removeEventListener(type, listener, removeOptions)
}, [])
}

export default useWindowEventListener