Merge pull request #14 from unix/hooks

Add hooks and remove some duplicate functions
This commit is contained in:
witt
2020-03-25 02:39:55 +08:00
committed by GitHub
19 changed files with 157 additions and 66 deletions

View File

@@ -1,5 +1,6 @@
import React, { MouseEvent, useCallback, useEffect, useMemo, useState } from 'react'
import React, { MouseEvent, useCallback, useMemo, useRef, useState } from 'react'
import useTheme from '../styles/use-theme'
import useClickAway from '../utils/use-click-away'
import { getColor } from './styles'
import ButtonDropdownIcon from './icon'
import ButtonDropdownItem from './item'
@@ -36,6 +37,7 @@ const stopPropagation = (event: MouseEvent<HTMLElement>) => {
const ButtonDropdown: React.FC<React.PropsWithChildren<ButtonDropdownProps>> = React.memo(({
children, type, size, auto, className, disabled, loading, ...props
}) => {
const ref = useRef<HTMLDivElement>(null)
const theme = useTheme()
const colors = getColor(theme.palette, type)
const sizes = getButtonSize(size, auto)
@@ -58,15 +60,11 @@ const ButtonDropdown: React.FC<React.PropsWithChildren<ButtonDropdownProps>> = R
return visible ? colors.hoverBgColor : colors.bgColor
}, [visible, colors, theme.palette])
const closeDetails = () => setVisible(false)
useEffect(() => {
document.addEventListener('click', closeDetails)
return () => document.removeEventListener('click', closeDetails)
}, [])
useClickAway(ref, () => setVisible(false))
return (
<ButtonDropdownContext.Provider value={initialValue}>
<div className={`btn-dropdown ${className}`} onClick={stopPropagation} {...props}>
<div ref={ref} className={`btn-dropdown ${className}`} onClick={stopPropagation} {...props}>
{mainItemChildren}
<details open={visible}>
<summary onClick={clickHandler}>

View File

@@ -1,6 +1,7 @@
import React, { useEffect, useMemo, useState } from 'react'
import withDefaults from '../utils/with-defaults'
import { CheckboxContext } from './checkbox-context'
import useWarning from '../utils/use-warning'
interface Props {
value: string[]
@@ -22,7 +23,7 @@ const CheckboxGroup: React.FC<React.PropsWithChildren<CheckboxGroupProps>> = Rea
const [selfVal, setSelfVal] = useState<string[]>([])
if (!value) {
value = []
console.error('[Checkbox Group]: Props "value" is required.')
useWarning('Props "value" is required.', 'Checkbox Group')
}
const updateState = (val: string, checked: boolean) => {

View File

@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react'
import { useCheckbox } from './checkbox-context'
import CheckboxGroup from './checkbox-group'
import CheckboxIcon from './checkbox.icon'
import useWarning from '../utils/use-warning'
interface CheckboxEventTarget {
checked: boolean
@@ -41,11 +42,17 @@ const Checkbox: React.FC<CheckboxProps> = React.memo(({
const isDisabled = inGroup ? disabledAll || disabled : disabled
if (inGroup && !value) {
console.error('[Checkbox]: Props "value" must be set when [Checkbox] component is in the group.')
useWarning(
'Props "value" must be set when [Checkbox] component is in the group.',
'Checkbox',
)
}
if (inGroup && checked) {
console.error('[Checkbox]: Remove props "checked" when [Checkbox] component is in the group.')
useWarning(
'Remove props "checked" when [Checkbox] component is in the group.',
'Checkbox',
)
}
if (inGroup) {

View File

@@ -3,6 +3,7 @@ import useTheme from '../styles/use-theme'
import withDefaults from '../utils/with-defaults'
import useCurrentState from '../utils/use-current-state'
import { FieldsetContext, FieldItem } from './fieldset-context'
import useWarning from '../utils/use-warning'
interface Props {
value: string
@@ -26,7 +27,7 @@ const FieldsetGroup: React.FC<React.PropsWithChildren<FieldsetGroupProps>> = Rea
const register = useCallback((newItem: FieldItem) => {
const hasItem = ref.current.find(item => item.value === newItem.value)
if (hasItem) {
console.error('[Fieldset Group]: The "value" of each "Fieldset" must be unique.')
useWarning('The "value" of each "Fieldset" must be unique.', 'Fieldset')
}
setItems([...ref.current, newItem])
}, [])

View File

@@ -6,6 +6,7 @@ import FieldsetFooter from './fieldset-footer'
import FieldsetGroup from './fieldset-group'
import { hasChild, pickChild } from '../utils/collections'
import { useFieldset } from './fieldset-context'
import useWarning from '../utils/use-warning'
interface Props {
value?: string
@@ -38,7 +39,7 @@ const Fieldset: React.FC<React.PropsWithChildren<FieldsetProps>> = React.memo(({
if (inGroup) {
if (!label) {
console.error('[Fieldset Group]: Props "label" is required when in a group.')
useWarning('Props "label" is required when in a group.', 'Fieldset Group')
}
if (!value || value === '') {
value = label

View File

@@ -4,6 +4,7 @@ import { useRadioContext } from './radio-context'
import RadioGroup from './radio-group'
import RadioDescription from './radio-description'
import { pickChild } from '../utils/collections'
import useWarning from '../utils/use-warning'
interface RadioEventTarget {
checked: boolean
@@ -43,10 +44,10 @@ const Radio: React.FC<React.PropsWithChildren<RadioProps>> = React.memo(({
if (inGroup) {
if (checked !== undefined) {
console.error('[Radio]: remove props "checked" if in the Radio.Group.')
useWarning('Remove props "checked" if in the Radio.Group.', 'Radio')
}
if (radioValue === undefined) {
console.error('[Radio]: props "value" must be deinfed if in the Radio.Group.')
useWarning('Props "value" must be deinfed if in the Radio.Group.', 'Radio')
}
useEffect(() => setSelfChecked(groupValue === radioValue), [groupValue, radioValue])
}

View File

@@ -1,4 +1,4 @@
import React from 'react'
import React, { MutableRefObject } from 'react'
import { NormalSizes } from '../utils/prop-types'
export interface SelectConfig {
@@ -8,6 +8,7 @@ export interface SelectConfig {
updateVisible?: Function
size?: NormalSizes
disableAll?: boolean
ref?: MutableRefObject<HTMLElement | null>
}
const defaultContext = {

View File

@@ -0,0 +1,31 @@
import React from 'react'
import useTheme from '../styles/use-theme'
import { useSelectContext } from './select-context'
import Dropdown from '../shared/dropdown'
interface Props {
visible: boolean
}
const SelectDropdown: React.FC<React.PropsWithChildren<Props>> = ({
visible, children,
}) => {
const theme = useTheme()
const { ref } = useSelectContext()
return (
<Dropdown parent={ref} visible={visible}>
<div className="select-dropdown">
{children}
<style jsx>{`
.select-dropdown {
border-radius: ${theme.layout.radius};
box-shadow: ${theme.expressiveness.shadowLarge};
}
`}</style>
</div>
</Dropdown>
)
}
export default SelectDropdown

View File

@@ -2,6 +2,7 @@ import React, { useMemo } from 'react'
import withDefaults from '../utils/with-defaults'
import useTheme from '../styles/use-theme'
import { useSelectContext } from './select-context'
import useWarning from '../utils/use-warning'
interface Props {
value: string
@@ -25,7 +26,7 @@ const SelectOption: React.FC<React.PropsWithChildren<SelectOptionProps>> = ({
const { updateValue, value, disableAll } = useSelectContext()
const isDisabled = useMemo(() => disabled || disableAll, [disabled, disableAll])
if (identValue === undefined) {
console.error('[Select Option]: the props "value" is required.')
useWarning('The props "value" is required.', 'Select Option')
}
const selected = useMemo(() => value ? identValue === value : false, [identValue, value])

View File

@@ -1,13 +1,13 @@
import React, { MutableRefObject, useEffect, useMemo, useRef, useState } from 'react'
import useTheme from '../styles/use-theme'
import SelectOption from './select-option'
import SelectIcon from './select-icon'
import Dropdown from '../shared/dropdown'
import { ZeitUIThemes } from '../styles/themes'
import { SelectContext, SelectConfig } from './select-context'
import React, { useMemo, useRef, useState } from 'react'
import { NormalSizes } from '../utils/prop-types'
import { getSizes } from './styles'
import useClickAway from '../utils/use-click-away'
import { pickChildByProps, pickChildrenFirst } from '../utils/collections'
import SelectIcon from './select-icon'
import SelectOption from './select-option'
import useTheme from '../styles/use-theme'
import SelectDropdown from './select-dropdown'
import { SelectContext, SelectConfig } from './select-context'
import { getSizes } from './styles'
interface Props {
disabled?: boolean
@@ -30,25 +30,6 @@ const defaultProps = {
export type SelectProps = Props & typeof defaultProps & React.HTMLAttributes<any>
const getDropdown = (
ref: MutableRefObject<HTMLDivElement | null>,
children: React.ReactNode | null,
theme: ZeitUIThemes,
visible: boolean,
) => (
<Dropdown parent={ref} visible={visible}>
<div className="select-dropdown">
{children}
<style jsx>{`
.select-dropdown {
border-radius: ${theme.layout.radius};
box-shadow: ${theme.expressiveness.shadowLarge};
}
`}</style>
</div>
</Dropdown>
)
const Select: React.FC<React.PropsWithChildren<SelectProps>> = ({
children, size, disabled, initialValue: init, placeholder,
icon: Icon, onChange, className, pure, ...props
@@ -67,10 +48,10 @@ const Select: React.FC<React.PropsWithChildren<SelectProps>> = ({
}
const initialValue: SelectConfig = useMemo(() => ({
value, visible, updateValue, updateVisible,
size, disableAll: disabled,
}), [visible, size, disabled])
value, visible, updateValue, updateVisible, size, ref,
disableAll: disabled,
}), [visible, size, disabled, ref])
const clickHandler = (event: React.MouseEvent<HTMLDivElement>) => {
event.stopPropagation()
event.nativeEvent.stopImmediatePropagation()
@@ -79,11 +60,7 @@ const Select: React.FC<React.PropsWithChildren<SelectProps>> = ({
setVisible(!visible)
}
useEffect(() => {
const closeHandler = () => setVisible(false)
document.addEventListener('click', closeHandler)
return () => document.removeEventListener('click', closeHandler)
}, [])
useClickAway(ref, () => setVisible(false))
const selectedChild = useMemo(() => {
const [, optionChildren] = pickChildByProps(children, 'value', value)
@@ -97,7 +74,7 @@ const Select: React.FC<React.PropsWithChildren<SelectProps>> = ({
<div className={`select ${className}`} ref={ref} onClick={clickHandler} {...props}>
{!value && <span className="value placeholder">{placeholder}</span>}
{value && <span className="value">{selectedChild}</span>}
{getDropdown(ref, children, theme, visible)}
<SelectDropdown visible={visible}>{children}</SelectDropdown>
{!pure && <div className="icon"><Icon /></div>}
<style jsx>{`
.select {

View File

@@ -1,10 +1,12 @@
import React, { MutableRefObject, useEffect, useState } from 'react'
import React, { MutableRefObject, useState } from 'react'
import { createPortal } from 'react-dom'
import usePortal from '../utils/use-portal'
import useResize from '../utils/use-resize'
import CSSTransition from './css-transition'
import useClickAnyWhere from '../utils/use-click-anywhere'
interface Props {
parent?: MutableRefObject<HTMLDivElement | null>
parent?: MutableRefObject<HTMLElement | null> | undefined
visible: boolean
}
@@ -22,7 +24,7 @@ const defaultRect: ReactiveDomReact = {
width: 0,
}
const getRect = (ref: MutableRefObject<HTMLDivElement | null>): ReactiveDomReact => {
const getRect = (ref: MutableRefObject<HTMLElement | null>): ReactiveDomReact => {
if (!ref || !ref.current) return defaultRect
const rect = ref.current.getBoundingClientRect()
return {
@@ -45,12 +47,14 @@ const Dropdown: React.FC<React.PropsWithChildren<Props>> = React.memo(({
setRect({ top, left, right, width: nativeWidth })
}
useEffect(() => {
useResize(updateRect)
useClickAnyWhere(() => {
const { top, left } = getRect(parent)
const shouldUpdatePosition = top !== rect.top || left !== rect.left
if (!shouldUpdatePosition) return
updateRect()
window.addEventListener('resize', updateRect)
return () => window.removeEventListener('resize', updateRect)
}, [])
})
const clickHandler = (event: React.MouseEvent<HTMLDivElement>) => {
event.stopPropagation()
event.preventDefault()

View File

@@ -1,5 +1,6 @@
import React from 'react'
import withDefaults from '../utils/with-defaults'
import useWarning from '../utils/use-warning'
interface Props {
x?: number
@@ -19,7 +20,7 @@ export type SpacerProps = Props & typeof defaultProps & React.HTMLAttributes<any
const getMargin = (num: number): string => {
if (num < 0) {
console.error('[Spacer]: "x"/"y" must be greater than or equal to 0')
useWarning('Props "x"/"y" must be greater than or equal to 0', 'Spacer')
return '0'
}
return `calc(${num * 15.25}pt + 1px * ${num - 1})`

View File

@@ -4,6 +4,7 @@ import darkTheme from '../themes/dark'
import lightTheme from '../themes/default'
import { ZeitUIThemes } from '../themes/index'
import ThemeContext from '../use-theme/theme-context'
import useWarning from '../../utils/use-warning'
type PartialTheme = Partial<ZeitUIThemes>
export type ThemeParam = PartialTheme | ((theme: PartialTheme) => PartialTheme) | undefined
@@ -17,7 +18,7 @@ const mergeTheme = (current: ZeitUIThemes, custom: ThemeParam): ZeitUIThemes =>
if (typeof custom === 'function') {
const merged = custom(current)
if (!merged || typeof merged !== 'object') {
console.error('Zeit-UI: the theme function must return object value.')
useWarning('The theme function must return object value.')
}
return merged as ZeitUIThemes
}

View File

@@ -3,6 +3,7 @@ import TabsItem from './tabs-item'
import useTheme from '../styles/use-theme'
import { TabsLabelItem, TabsConfig, TabsContext } from './tabs-context'
import useCurrentState from '../utils/use-current-state'
import useWarning from '../utils/use-warning'
interface Props {
initialValue?: string
@@ -28,7 +29,7 @@ const Tabs: React.FC<React.PropsWithChildren<TabsProps>> = ({
const register = (next: TabsLabelItem) => {
const hasItem = tabsRef.current.find(item => item.value === next.value)
if (hasItem) {
console.error('[Tabs]: The "value" of each "Tabs.Item" must be unique.')
useWarning('The "value" of each "Tabs.Item" must be unique.', 'Tabs')
}
setTabs([...tabsRef.current, next])
}

View File

@@ -0,0 +1,14 @@
import { useEffect } from 'react'
const useClickAnyWhere = (
handler: (event: Event) => void,
) => {
useEffect(() => {
const callback = (event: Event) => handler(event)
document.addEventListener('click', callback)
return () => document.removeEventListener('click', callback)
}, [handler])
}
export default useClickAnyWhere

View File

@@ -0,0 +1,19 @@
import { MutableRefObject, useEffect } from 'react'
const useClickAway = (
ref: MutableRefObject<HTMLElement | null>,
handler: (event: Event) => void,
) => {
useEffect(() => {
const callback = (event: Event) => {
const el = ref.current
if (!event || !el || el.contains((event as any).target)) return
handler(event)
}
document.addEventListener('click', callback)
return () => document.removeEventListener('click', callback)
}, [ref, handler])
}
export default useClickAway

View File

@@ -25,7 +25,7 @@ const usePortal = (selectId: string = getId()): Element | null => {
return () => {
const node = document.getElementById(id)
if (node) {
// document.body.removeChild(node)
document.body.removeChild(node)
}
}
}, [])

View File

@@ -0,0 +1,14 @@
import { useEffect } from 'react'
const useResize = (callback: Function, immediatelyInvoke: boolean = true): void => {
useEffect(() => {
const fn = () => callback()
if (immediatelyInvoke) {
fn()
}
window.addEventListener('resize', fn)
return () => window.removeEventListener('resize', fn)
}, [])
}
export default useResize

View File

@@ -0,0 +1,18 @@
const warningStack: { [key: string]: boolean } = {}
const useWarning = (message: string, component?: string) => {
const tag = component ? ` [${component}]` : ' '
const log = `[Zeit UI]${tag}: ${message}`
if (typeof console === 'undefined') return
if (warningStack[log]) return
warningStack[log] = true
if (process.env.NODE_ENV !== 'production') {
return console.error(log)
}
console.warn(log)
}
export default useWarning