mirror of
https://github.com/zhigang1992/react.git
synced 2026-04-24 04:15:54 +08:00
Merge pull request #14 from unix/hooks
Add hooks and remove some duplicate functions
This commit is contained in:
@@ -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}>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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])
|
||||
}, [])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
31
components/select/select-dropdown.tsx
Normal file
31
components/select/select-dropdown.tsx
Normal 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
|
||||
@@ -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])
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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})`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
14
components/utils/use-click-anywhere.ts
Normal file
14
components/utils/use-click-anywhere.ts
Normal 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
|
||||
19
components/utils/use-click-away.ts
Normal file
19
components/utils/use-click-away.ts
Normal 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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
14
components/utils/use-resize.ts
Normal file
14
components/utils/use-resize.ts
Normal 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
|
||||
18
components/utils/use-warning.ts
Normal file
18
components/utils/use-warning.ts
Normal 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
|
||||
Reference in New Issue
Block a user