diff --git a/components/index.ts b/components/index.ts index 698f621..885f98d 100644 --- a/components/index.ts +++ b/components/index.ts @@ -30,3 +30,4 @@ export { default as Spinner } from './spinner' export { default as ButtonDropdown } from './button-dropdown' export { default as Capacity } from './capacity' export { default as Input } from './input' +export { default as Radio } from './radio' diff --git a/components/radio/index.ts b/components/radio/index.ts new file mode 100644 index 0000000..c3ee9bc --- /dev/null +++ b/components/radio/index.ts @@ -0,0 +1,9 @@ +import Radio from './radio' +import RadioGroup from './radio-group' +import RadioDescription from './radio-description' + +Radio.Group = RadioGroup +Radio.Description = RadioDescription +Radio.Desc = RadioDescription + +export default Radio diff --git a/components/radio/radio-context.ts b/components/radio/radio-context.ts new file mode 100644 index 0000000..9a0ecb5 --- /dev/null +++ b/components/radio/radio-context.ts @@ -0,0 +1,18 @@ +import React from 'react' + +export interface RadioConfig { + updateState: (value: string) => void + disabledAll: boolean + value?: string + inGroup: boolean +} + +const defaultContext = { + disabledAll: false, + updateState: () => {}, + inGroup: false, +} + +export const RadioContext = React.createContext(defaultContext) + +export const useRadioContext = (): RadioConfig => React.useContext(RadioContext) diff --git a/components/radio/radio-description.tsx b/components/radio/radio-description.tsx new file mode 100644 index 0000000..5b36127 --- /dev/null +++ b/components/radio/radio-description.tsx @@ -0,0 +1,33 @@ +import React from 'react' +import withDefaults from '../utils/with-defaults' +import useTheme from '../styles/use-theme' + +interface Props { + className?: string +} + +const defaultProps = { + className: '', +} + +export type RadioDescriptionProps = Props & typeof defaultProps & React.HTMLAttributes + +const RadioDescription: React.FC> = React.memo(({ + className, children, ...props +}) => { + const theme = useTheme() + + return ( + + {children} + + + ) +}) + +export default withDefaults(RadioDescription, defaultProps) diff --git a/components/radio/radio-group.tsx b/components/radio/radio-group.tsx new file mode 100644 index 0000000..076a2dd --- /dev/null +++ b/components/radio/radio-group.tsx @@ -0,0 +1,71 @@ +import React, { useEffect, useMemo, useState } from 'react' +import withDefaults from '../utils/with-defaults' +import useTheme from '../styles/use-theme' +import { RadioContext } from './radio-context' + +interface Props { + value: string + initialValue?: string + disabled?: boolean + onChange?: (value: string) => void + className?: string + useRow?: boolean +} + +const defaultProps = { + disabled: false, + className: '', + useRow: false, +} + +export type RadioGroupProps = Props & typeof defaultProps & React.HTMLAttributes + +const RadioGroup: React.FC> = React.memo(({ + disabled, onChange, value, children, className, initialValue, useRow, ...props +}) => { + const theme = useTheme() + const [selfVal, setSelfVal] = useState(initialValue) + + const updateState = (nextValue: string) => { + setSelfVal(nextValue) + onChange && onChange(nextValue) + } + + const providerValue = useMemo(() => { + return { + updateState, + disabledAll: disabled, + inGroup: true, + value: selfVal, + } + },[disabled, selfVal]) + + useEffect(() => { + setSelfVal(value) + }, [value]) + + return ( + +
+ {children} +
+ +
+ ) +}) + +export default withDefaults(RadioGroup, defaultProps) diff --git a/components/radio/radio.tsx b/components/radio/radio.tsx new file mode 100644 index 0000000..a4f8126 --- /dev/null +++ b/components/radio/radio.tsx @@ -0,0 +1,161 @@ +import React, { useEffect, useMemo, useState } from 'react' +import useTheme from '../styles/use-theme' +import { useRadioContext } from './radio-context' +import RadioGroup from './radio-group' +import RadioDescription from './radio-description' +import { pickChild } from '../utils/collections' + +interface RadioEventTarget { + checked: boolean +} + +export interface RadioEvent { + target: RadioEventTarget + stopPropagation: () => void + preventDefault: () => void + nativeEvent: React.ChangeEvent +} + +interface Props { + checked?: boolean + value?: string + id?: string + className?: string + disabled?: boolean + onChange: (e: RadioEvent) => void +} + +const defaultProps = { + disabled: false, + className: '', +} + +export type RadioProps = Props & typeof defaultProps & React.InputHTMLAttributes + +const Radio: React.FC> = React.memo(({ + className, id: customId, checked, onChange, disabled, + value: radioValue, children, ...props +}) => { + const theme = useTheme() + const [selfChecked, setSelfChecked] = useState(!!checked) + const { value: groupValue, disabledAll, inGroup, updateState } = useRadioContext() + const [withoutDescChildren, DescChildren] = pickChild(children, RadioDescription) + + if (inGroup) { + if (checked !== undefined) { + console.error('[Radio]: remove props "checked" if in the Radio.Group.') + } + if (radioValue === undefined) { + console.error('[Radio]: props "value" must be deinfed if in the Radio.Group.') + } + useEffect(() => setSelfChecked(groupValue === radioValue), [groupValue, radioValue]) + } + + // const id = useMemo(() => customId || `zeit-ui-radio-${label}`, [customId, label]) + const isDisabled = useMemo(() => disabled || disabledAll, [disabled, disabledAll]) + const changeHandler = (event: React.ChangeEvent) => { + if (isDisabled) return + const selfEvent: RadioEvent = { + target: { + checked: !selfChecked, + }, + stopPropagation: event.stopPropagation, + preventDefault: event.preventDefault, + nativeEvent: event, + } + setSelfChecked(!selfChecked) + if (inGroup) { + updateState(radioValue as string) + } + onChange && onChange(selfEvent) + } + + useEffect(() => setSelfChecked(!!checked), [checked]) + + return ( +
+ + + +
+ ) +}) + + +Radio.defaultProps = defaultProps + +type RadioComponent

= React.FC

& { + Group: typeof RadioGroup + Desc: typeof RadioDescription + Description: typeof RadioDescription +} + +type ComponentProps = Partial & Omit + +export default Radio as RadioComponent diff --git a/components/toast/use-toast.tsx b/components/toast/use-toast.tsx index eff4471..ee2874c 100644 --- a/components/toast/use-toast.tsx +++ b/components/toast/use-toast.tsx @@ -3,6 +3,7 @@ import { NormalTypes } from '../utils/prop-types' import useCurrentState from '../utils/use-current-state' import { useZEITUIContext } from '../utils/use-zeit-ui-context' import { ToastWithID } from './toast-container' +import { getId } from '../utils/collections' export interface ToastAction { name: string @@ -21,10 +22,6 @@ const defaultToast = { delay: 2000, } -const generateId = () => { - return Math.random().toString(32).slice(2, 10) -} - const useToasts = (): [Array, (t: Toast) => void] => { const { updateToasts, toastHovering, toasts } = useZEITUIContext() const destoryStack = useRef>([]) @@ -55,7 +52,7 @@ const useToasts = (): [Array, (t: Toast) => void] => { const setToast = (toast: Toast): void => { - const id = `toast-${generateId()}` + const id = `toast-${getId()}` const delay = toast.delay || defaultToast.delay const cancel = (id: string, delay: number) => { diff --git a/components/utils/collections.ts b/components/utils/collections.ts index 4b68711..82a4494 100644 --- a/components/utils/collections.ts +++ b/components/utils/collections.ts @@ -1,5 +1,9 @@ import React, { ReactNode } from 'react' +export const getId = () => { + return Math.random().toString(32).slice(2, 10) +} + export const hasChild = ( children: ReactNode | undefined, child: React.ElementType @@ -18,7 +22,7 @@ export const pickChild = ( ): [ReactNode | undefined, ReactNode | undefined] => { let target: ReactNode[] = [] const withoutTargetChildren = React.Children.map(children, item => { - if (!React.isValidElement(item)) return null + if (!React.isValidElement(item)) return item if (item.type === targetChild) { target.push(item) return null diff --git a/components/utils/use-portal.ts b/components/utils/use-portal.ts index fe9605d..f5b10fd 100644 --- a/components/utils/use-portal.ts +++ b/components/utils/use-portal.ts @@ -1,9 +1,6 @@ import { useEffect, useState } from 'react' import useSSR from '../utils/use-ssr' - -const getId = () => { - return Math.random().toString(32).slice(2, 10) -} +import { getId } from './collections' const createElement = (id: string): HTMLElement => { const el = document.createElement('div') diff --git a/lib/components/attributes/attributes-title.tsx b/lib/components/attributes/attributes-title.tsx index 8071ac9..ec3e639 100644 --- a/lib/components/attributes/attributes-title.tsx +++ b/lib/components/attributes/attributes-title.tsx @@ -2,15 +2,37 @@ import React from 'react' import { Spacer, Code } from 'components' export interface AttributesTitleProps { + alias?: string +} + +const getAlias = (alias?: string) => { + if (!alias) return null + return ( + [alias: {alias}] + ) } const AttributesTitle: React.FC> = React.memo(({ - children, + children, alias, }) => { return ( <> -

{children}

+

{children}{getAlias(alias)}

+ + ) }) diff --git a/lib/components/attributes/attributes.tsx b/lib/components/attributes/attributes.tsx index 5fef74c..1037907 100644 --- a/lib/components/attributes/attributes.tsx +++ b/lib/components/attributes/attributes.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useMemo } from 'react' import { Card, Link, Spacer, useTheme } from 'components' import AttributesTitle from './attributes-title' @@ -10,6 +10,9 @@ const Attributes: React.FC> = React.mem edit, children, }) => { const theme = useTheme() + const link = useMemo(() => { + return `https://github.com/zeit-ui/react/blob/master${edit || '/pages'}` + }, []) return ( <> @@ -19,7 +22,7 @@ const Attributes: React.FC> = React.mem {children} - Edit this page on GitHub + Edit this page on GitHub