Merge pull request #2 from unix/radio

feat(radio): add component
This commit is contained in:
witt
2020-03-19 23:28:51 +08:00
committed by GitHub
37 changed files with 490 additions and 38 deletions

View File

@@ -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'

View File

@@ -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

View File

@@ -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<RadioConfig>(defaultContext)
export const useRadioContext = (): RadioConfig => React.useContext<RadioConfig>(RadioContext)

View File

@@ -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<any>
const RadioDescription: React.FC<React.PropsWithChildren<RadioDescriptionProps>> = React.memo(({
className, children, ...props
}) => {
const theme = useTheme()
return (
<span className={className} {...props}>
{children}
<style jsx>{`
span {
color: ${theme.palette.accents_3};
font-size: .875rem;
}
`}</style>
</span>
)
})
export default withDefaults(RadioDescription, defaultProps)

View File

@@ -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<any>
const RadioGroup: React.FC<React.PropsWithChildren<RadioGroupProps>> = React.memo(({
disabled, onChange, value, children, className, initialValue, useRow, ...props
}) => {
const theme = useTheme()
const [selfVal, setSelfVal] = useState<string | undefined>(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 (
<RadioContext.Provider value={providerValue}>
<div className={`radio-group ${className}`} {...props}>
{children}
</div>
<style jsx>{`
.radio-group {
display: flex;
flex-direction: ${useRow ? 'col' : 'column'};
}
.radio-group :global(.radio) {
margin-top: ${useRow ? 0 : theme.layout.gap};
margin-left: ${useRow ? theme.layout.gap : 0};
}
.radio-group :global(.radio:first-of-type) {
margin: 0;
}
`}</style>
</RadioContext.Provider>
)
})
export default withDefaults(RadioGroup, defaultProps)

161
components/radio/radio.tsx Normal file
View File

@@ -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<any>
const Radio: React.FC<React.PropsWithChildren<RadioProps>> = React.memo(({
className, id: customId, checked, onChange, disabled,
value: radioValue, children, ...props
}) => {
const theme = useTheme()
const [selfChecked, setSelfChecked] = useState<boolean>(!!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 (
<div className={`radio ${className}`}>
<label htmlFor={customId}>
<input type="radio" value={radioValue} id={customId}
checked={selfChecked}
onChange={changeHandler}
{...props} />
<span className="name">{withoutDescChildren}</span>
{DescChildren && DescChildren}
<span className="point" />
</label>
<style jsx>{`
input {
opacity: 0;
visibility: hidden;
overflow: hidden;
width: 1px;
height: 1px;
top: -1000px;
right: -1000px;
position: fixed;
}
.radio {
display: flex;
width: initial;
align-items: flex-start;
line-height: 1.5rem;
position: relative;
}
label {
display: flex;
flex-direction: column;
justify-content: flex-start;
margin-left: 1.375rem;
color: ${isDisabled ? theme.palette.accents_4 : theme.palette.foreground};
cursor: ${isDisabled ? 'not-allowed' : 'pointer'};
}
.name {
font-size: 1rem;
font-weight: bold;
user-select: none;
}
.point {
position: absolute;
left: 0;
top: 6px;
height: .875rem;
width: .875rem;
border-radius: 50%;
border: 1px solid ${theme.palette.border};
transition: all 0.2s ease 0s;
}
.point:before {
content: '';
position: absolute;
left: -1px;
top: -1px;
height: .875rem;
width: .875rem;
border-radius: 50%;
transform: scale(${selfChecked ? 1 : 0});
transition: all 0.2s ease;
background-color: ${isDisabled ? theme.palette.accents_4 : theme.palette.foreground};
}
`}</style>
</div>
)
})
Radio.defaultProps = defaultProps
type RadioComponent<P = {}> = React.FC<P> & {
Group: typeof RadioGroup
Desc: typeof RadioDescription
Description: typeof RadioDescription
}
type ComponentProps = Partial<typeof defaultProps> & Omit<Props, keyof typeof defaultProps>
export default Radio as RadioComponent<ComponentProps>

View File

@@ -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<Toast>, (t: Toast) => void] => {
const { updateToasts, toastHovering, toasts } = useZEITUIContext()
const destoryStack = useRef<Array<string>>([])
@@ -55,7 +52,7 @@ const useToasts = (): [Array<Toast>, (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) => {

View File

@@ -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

View File

@@ -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')

View File

@@ -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 (
<small>[alias: <Code>{alias}</Code>]</small>
)
}
const AttributesTitle: React.FC<React.PropsWithChildren<AttributesTitleProps>> = React.memo(({
children,
children, alias,
}) => {
return (
<>
<h4 className="title"><Code>{children}</Code></h4>
<h4 className="title"><Code>{children}</Code>{getAlias(alias)}</h4>
<Spacer y={.6} />
<style jsx>{`
h4 {
display: inline-flex;
height: 1.6rem;
line-height: 1.2;
align-items: center;
}
h4 :global(small) {
font-size: .75em;
padding-left: 1rem;
}
`}</style>
</>
)
})

View File

@@ -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.PropsWithChildren<AttributesProps>> = 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.PropsWithChildren<AttributesProps>> = React.mem
{children}
</Card>
<Spacer y={1} />
<Link color target="_blank" className="attributes-link" href={edit} rel="nofollow">Edit this page on GitHub</Link>
<Link color target="_blank" className="attributes-link" href={link} rel="nofollow">Edit this page on GitHub</Link>
<style global jsx>{`
.attr pre {

5
now.json Normal file
View File

@@ -0,0 +1,5 @@
{
"github": {
"silent": true
}
}

View File

@@ -16,6 +16,19 @@ const Application: NextPage<AppProps> = ({ Component, pageProps }) => {
<>
<Head>
<title>React - ZEIT UI</title>
<meta name="google" content="notranslate" />
<meta name="twitter:creator" content="@echo_witt" />
<meta name="referrer" content="strict-origin" />
<meta property="og:title" content="React - ZEIT UI" />
<meta property="og:url" content="https://react.zeit-ui.co" />
<link rel="dns-prefetch" href="//react.zeit-ui.co" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="generator" content="ZEIT UI" />
<meta name="description" content="React implementation for ZEIT design." />
<meta property="og:description" content="React implementation for ZEIT design." />
<meta property="og:image" content="https://user-images.githubusercontent.com/11304944/76085431-fd036480-5fec-11ea-8412-9e581425344a.png" />
<meta property="twitter:image" content="https://user-images.githubusercontent.com/11304944/76085431-fd036480-5fec-11ea-8412-9e581425344a.png" />
<meta name="viewport" content="initial-scale=1, maximum-scale=5, minimum-scale=1, viewport-fit=cover" />
</Head>
<ZEITUIProvider theme={{ type: themeType }}>
<CSSBaseline />

View File

@@ -83,7 +83,7 @@ Avatars represent a user or a team. Stacked avatars represent a group of people.
}
`} />
<Attributes>
<Attributes edit="/pages/docs/components/avatar.mdx">
<Attributes.Title>Avatar.Props</Attributes.Title>
| Attribute | Description | Type | Accepted values | Default

View File

@@ -117,7 +117,7 @@ Display related but alternate actions for a button.
</ButtonDropdown>
`} />
<Attributes>
<Attributes edit="/pages/docs/components/button-dropdown.mdx">
<Attributes.Title>ButtonDropdown.Props</Attributes.Title>
| Attribute | Description | Type | Accepted values | Default

View File

@@ -85,7 +85,7 @@ Used to trigger an operation.
</>
`} />
<Attributes>
<Attributes edit="/pages/docs/components/button.mdx">
<Attributes.Title>Button.Props</Attributes.Title>
| Attribute | Description | Type | Accepted values | Default

View File

@@ -30,7 +30,7 @@ Display an capacity indicator.
}
`} />
<Attributes>
<Attributes edit="/pages/docs/components/capacity.mdx">
<Attributes.Title>Capacity.Props</Attributes.Title>
| Attribute | Description | Type | Accepted values | Default

View File

@@ -39,7 +39,7 @@ A common container component.
</Card>
`} />
<Attributes>
<Attributes edit="/pages/docs/components/card.mdx">
<Attributes.Title>Card.Props</Attributes.Title>
| Attribute | Description | Type | Accepted values | Default

View File

@@ -49,7 +49,7 @@ Displays a boolean value.
}
`} />
<Attributes>
<Attributes edit="/pages/docs/components/checkbox.mdx">
<Attributes.Title>Checkbox.Props</Attributes.Title>
| Attribute | Description | Type | Accepted values | Default

View File

@@ -38,7 +38,7 @@ export const meta = {
yarn add @zeit-ui/vue</Code>
`} />
<Attributes>
<Attributes edit="/pages/docs/components/code.mdx">
<Attributes.Title>Code.Props</Attributes.Title>
| Attribute | Description | Type | Accepted values | Default

View File

@@ -23,7 +23,7 @@ A common container component.
<Description title="Section Title" content={<p><Code>code</Code> about this section.</p>} />
`} />
<Attributes>
<Attributes edit="/pages/docs/components/description.mdx">
<Attributes.Title>Description.Props</Attributes.Title>
| Attribute | Description | Type | Accepted values | Default

View File

@@ -56,7 +56,7 @@ export const meta = {
</Display>
`} />
<Attributes>
<Attributes edit="/pages/docs/components/display.mdx">
<Attributes.Title>Display.Props</Attributes.Title>
| Attribute | Description | Type | Accepted values | Default

View File

@@ -32,7 +32,7 @@ Display an indicator of deployment status.
</>
`} />
<Attributes>
<Attributes edit="/pages/docs/components/dot.mdx">
<Attributes.Title>Dot.Props</Attributes.Title>
| Attribute | Description | Type | Accepted values | Default

View File

@@ -89,7 +89,7 @@ Display a collection of related information in a single unit.
}
`} />
<Attributes>
<Attributes edit="/pages/docs/components/fieldset.mdx">
<Attributes.Title>Fieldset.Props</Attributes.Title>
| Attribute | Description | Type | Accepted values | Default

View File

@@ -35,7 +35,7 @@ Display image content.
</Display>
`} />
<Attributes>
<Attributes edit="/pages/docs/components/image.mdx">
<Attributes.Title>Image.Props</Attributes.Title>
| Attribute | Description | Type | Accepted values | Default

View File

@@ -101,7 +101,7 @@ Retrieve text input from a user.
`} />
<Attributes>
<Attributes edit="/pages/docs/components/input.mdx">
<Attributes.Title>Input.Props</Attributes.Title>
| Attribute | Description | Type | Accepted values | Default

View File

@@ -46,7 +46,7 @@ Display keyboard input that triggers an action.
<Keyboard small>/</Keyboard>
`} />
<Attributes>
<Attributes edit="/pages/docs/components/keyboard.mdx">
<Attributes.Title>Keyboard.Props</Attributes.Title>
| Attribute | Description | Type | Accepted values | Default

View File

@@ -123,7 +123,7 @@ Smart and simple layout components.
</>
`} />
<Attributes>
<Attributes edit="/pages/docs/components/layout.mdx">
<Attributes.Title>Row.Props</Attributes.Title>
| Attribute | Description | Type | Accepted values | Default

View File

@@ -39,7 +39,7 @@ Hyperlinks between pages.
</div>
`} />
<Attributes>
<Attributes edit="/pages/docs/components/link.mdx">
<Attributes.Title>Link.Props</Attributes.Title>
| Attribute | Description | Type | Accepted values | Default

View File

@@ -117,7 +117,7 @@ Display popup content that requires attention or provides additional information
}
`} />
<Attributes>
<Attributes edit="/pages/docs/components/modal.mdx">
<Attributes.Title>Modal.Props</Attributes.Title>
| Attribute | Description | Type | Accepted values | Default

View File

@@ -77,7 +77,7 @@ Display text that requires attention or provides additional information.
</>
`} />
<Attributes>
<Attributes edit="/pages/docs/components/note.mdx">
<Attributes.Title>Note.Props</Attributes.Title>
| Attribute | Description | Type | Accepted values | Default

View File

@@ -0,0 +1,118 @@
import { Layout, Playground, Attributes } from 'lib/components'
import { Radio, Spacer, Code } from 'components'
import { useState } from 'react'
export const meta = {
title: 'radio',
description: 'avatar',
}
## Radio
Provides single user input from a selection of options.
<Playground
scope={{ Radio }}
code={`
<Radio checked={false}>Option 1</Radio>
`} />
<Playground
title="Group"
scope={{ Radio, useState }}
code={`
() => {
const [state, setState] = useState('1')
const handler = val => {
setState(val)
console.log(val)
}
return (
<>
<Radio.Group value={state} onChange={handler}>
<Radio value="1">Option 1</Radio>
<Radio value="2">Option 2</Radio>
</Radio.Group>
</>
)
}
`} />
<Playground
title="Description"
desc="`Description` can be combined with other `Components`."
scope={{ Radio, Code }}
code={`
<Radio.Group value="1" onChange={val => console.log(val)}>
<Radio value="1">
Option 1
<Radio.Description>Description for Option1</Radio.Description>
</Radio>
<Radio value="2">
Option 2
<Radio.Desc><Code>Description</Code> for Option2</Radio.Desc>
</Radio>
</Radio.Group>
`} />
<Playground
title="Disabled"
scope={{ Radio, useState, Code }}
code={`
<Radio.Group value="1" disabled>
<Radio value="1">Option 1</Radio>
<Radio value="2">Option 2</Radio>
</Radio.Group>
`} />
<Playground
title="Row"
desc="Horizontal arrangement."
scope={{ Radio, Code }}
code={`
<Radio.Group value="1" useRow>
<Radio value="1">
Option 1
<Radio.Desc>Description for Option1</Radio.Desc>
</Radio>
<Radio value="2">
Option 2
<Radio.Desc>Description for Option2</Radio.Desc>
</Radio>
</Radio.Group>
`} />
<Attributes edit="/pages/docs/components/radio.mdx">
<Attributes.Title>Radio.Props</Attributes.Title>
| Attribute | Description | Type | Accepted values | Default
| ---------- | ---------- | ---- | -------------- | ------ |
| **checked** | selected or not (in single) | `boolean` | - | `false` |
| **value** | unique ident value (in group) | `string` | - | - |
| **id** | native attr | `string` | - | - |
| **disabled** | disable current radio | `boolean` | - | `false` |
| **onChange** | change event | `(e: RadioEvent) => void` | - | - |
| ... | native props | `InputHTMLAttributes` | `'name', 'alt', 'className', ...` | - |
<Attributes.Title>Radio.Group.Props</Attributes.Title>
| Attribute | Description | Type | Accepted values | Default
| ---------- | ---------- | ---- | -------------- | ------ |
| **initialValue** | initial value | `string` | - | - |
| **value** | selected child radio | `string` | - | - |
| **useRow** | horizontal layout | `boolean` | - | `false` |
| **disabled** | disable all radios | `boolean` | - | `false` |
| **onChange** | change event | `(value: string) => void` | - | - |
| ... | native props | `HTMLAttributes` | `'name', 'id', 'className', ...` | - |
<Attributes.Title alias="Radio.Desc">Radio.Description.Props</Attributes.Title>
| Attribute | Description | Type | Accepted values | Default
| ---------- | ---------- | ---- | -------------- | ------ |
| ... | native props | `HTMLAttributes` | `'name', 'id', 'className', ...` | - |
</Attributes>
export default ({ children }) => <Layout meta={meta}>{children}</Layout>

View File

@@ -40,8 +40,8 @@ Provide empty space.
</Container>
`} />
<Attributes>
<Attributes.Title>Avatar.Props</Attributes.Title>
<Attributes edit="/pages/docs/components/spacer.mdx">
<Attributes.Title>Spacer.Props</Attributes.Title>
| Attribute | Description | Type | Accepted values | Default
| ---------- | ---------- | ---- | -------------- | ------ |

View File

@@ -31,7 +31,7 @@ Indicate an action running in the background.
</>
`} />
<Attributes>
<Attributes edit="/pages/docs/components/spinner.mdx">
<Attributes.Title>Spinner.Props</Attributes.Title>
| Attribute | Description | Type | Accepted values | Default

View File

@@ -29,7 +29,7 @@ Display a unique keyword, optionally as a part of a set.
</div>
`} />
<Attributes>
<Attributes edit="/pages/docs/components/tag.mdx">
<Attributes.Title>Tag.Props</Attributes.Title>
| Attribute | Description | Type | Accepted values | Default

View File

@@ -78,7 +78,7 @@ Display text using well-defined typographic styles.
</>
`} />
<Attributes>
<Attributes edit="/pages/docs/components/text.mdx">
<Attributes.Title>Text.Props</Attributes.Title>
| Attribute | Description | Type | Accepted values | Default

View File

@@ -89,7 +89,7 @@ Display an important message globally.
}
`} />
<Attributes>
<Attributes edit="/pages/docs/components/toast.mdx">
<Attributes.Title>useToasts</Attributes.Title>
```ts