mirror of
https://github.com/zhigang1992/react.git
synced 2026-04-29 20:55:46 +08:00
feat: initial
This commit is contained in:
22
components/fieldset/fieldset-context.ts
Normal file
22
components/fieldset/fieldset-context.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from 'react'
|
||||
|
||||
export interface FieldItem {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export interface FieldsetConfig {
|
||||
register: (item: FieldItem) => void
|
||||
currentValue: string
|
||||
inGroup: boolean
|
||||
}
|
||||
|
||||
const defaultContext = {
|
||||
inGroup: false,
|
||||
currentValue: '',
|
||||
register: () => {},
|
||||
}
|
||||
|
||||
export const FieldsetContext = React.createContext<FieldsetConfig>(defaultContext)
|
||||
|
||||
export const useFieldset = (): FieldsetConfig => React.useContext<FieldsetConfig>(FieldsetContext)
|
||||
30
components/fieldset/fieldset-footer-actions.tsx
Normal file
30
components/fieldset/fieldset-footer-actions.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from 'react'
|
||||
import withDefaults from '../utils/with-defaults'
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
className: ''
|
||||
}
|
||||
|
||||
export type FieldsetFooterActionsProps = Props & typeof defaultProps & React.HTMLAttributes<any>
|
||||
|
||||
const FieldsetFooterActions: React.FC<FieldsetFooterActionsProps> = React.memo(({
|
||||
className, children, ...props
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div className={className} {...props}>{children}</div>
|
||||
<style jsx>{`
|
||||
div {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
export default withDefaults(FieldsetFooterActions, defaultProps)
|
||||
37
components/fieldset/fieldset-footer-status.tsx
Normal file
37
components/fieldset/fieldset-footer-status.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React from 'react'
|
||||
import withDefaults from '../utils/with-defaults'
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
className: ''
|
||||
}
|
||||
|
||||
export type FieldsetFooterStatusProps = Props & typeof defaultProps & React.HTMLAttributes<any>
|
||||
|
||||
const FieldsetFooterStatus: React.FC<FieldsetFooterStatusProps> = React.memo(({
|
||||
className, children, ...props
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div className={className} {...props}>{children}</div>
|
||||
<style jsx>{`
|
||||
div {
|
||||
font-size: .875rem;
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
display: inline-flex;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
div>:global(p) {
|
||||
margin: 0;
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
export default withDefaults(FieldsetFooterStatus, defaultProps)
|
||||
53
components/fieldset/fieldset-footer.tsx
Normal file
53
components/fieldset/fieldset-footer.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React from 'react'
|
||||
import useTheme from '../styles/use-theme'
|
||||
import FieldsetFooterStatus from './fieldset-footer-status'
|
||||
import FieldsetFooterActions from './fieldset-footer-actions'
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
className: '',
|
||||
}
|
||||
|
||||
export type FieldsetFooterProps = Props & typeof defaultProps & React.HTMLAttributes<any>
|
||||
|
||||
const FieldsetFooter: React.FC<React.PropsWithChildren<FieldsetFooterProps>> = React.memo(({
|
||||
className, children, ...props
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<footer className={className} {...props}>
|
||||
{children}
|
||||
<style jsx>{`
|
||||
footer {
|
||||
background-color: ${theme.palette.accents_1};
|
||||
border-top: 1px solid ${theme.palette.border};
|
||||
border-bottom-left-radius: ${theme.layout.radius};
|
||||
border-bottom-right-radius: ${theme.layout.radius};
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
color: ${theme.palette.accents_6};
|
||||
padding: ${theme.layout.gapHalf} ${theme.layout.gap};
|
||||
font-size: .875rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
`}</style>
|
||||
</footer>
|
||||
)
|
||||
})
|
||||
|
||||
FieldsetFooter.defaultProps = defaultProps
|
||||
|
||||
type FieldsetFooterComponent<P = {}> = React.FC<P> & {
|
||||
Status: typeof FieldsetFooterStatus
|
||||
Actions: typeof FieldsetFooterActions
|
||||
}
|
||||
|
||||
type ComponentProps = Partial<typeof defaultProps> & Omit<Props, keyof typeof defaultProps>
|
||||
|
||||
export default FieldsetFooter as FieldsetFooterComponent<ComponentProps>
|
||||
121
components/fieldset/fieldset-group.tsx
Normal file
121
components/fieldset/fieldset-group.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
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'
|
||||
|
||||
interface Props {
|
||||
value: string
|
||||
className?: string
|
||||
onChange?: (value: string) => void
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
className: '',
|
||||
}
|
||||
|
||||
export type FieldsetGroupProps = Props & typeof defaultProps & React.HTMLAttributes<any>
|
||||
|
||||
const FieldsetGroup: React.FC<React.PropsWithChildren<FieldsetGroupProps>> = React.memo(({
|
||||
className, children, value, onChange, ...props
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
const [selfVal, setSelfVal] = useState<string>(value)
|
||||
const [items, setItems, ref] = useCurrentState<FieldItem[]>([])
|
||||
|
||||
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.')
|
||||
}
|
||||
setItems([...ref.current, newItem])
|
||||
}, [])
|
||||
|
||||
const providerValue = useMemo(() => ({
|
||||
currentValue: selfVal,
|
||||
inGroup: true,
|
||||
register: () => register,
|
||||
}), [selfVal])
|
||||
|
||||
const clickHandle = useCallback((nextValue: string) => {
|
||||
setSelfVal(nextValue)
|
||||
onChange && onChange(nextValue)
|
||||
}, [onChange])
|
||||
|
||||
return (
|
||||
<FieldsetContext.Provider value={providerValue}>
|
||||
<div className={` ${className}`} {...props}>
|
||||
<div className="group">
|
||||
{items.map(item => (
|
||||
<button onClick={() => clickHandle(item.value)}
|
||||
key={item.value}
|
||||
className={selfVal === item.value ? 'active' : ''}>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="group-content">{children}</div>
|
||||
<style jsx>{`
|
||||
.group {
|
||||
white-space: nowrap;
|
||||
overflow-y: hidden;
|
||||
overflow-x: auto;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
.group-content {
|
||||
border-top-left-radius: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.group-content :global(.fieldset) {
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
height: 34px;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
color: ${theme.palette.accents_3};
|
||||
background-color: ${theme.palette.accents_1};
|
||||
font-size: .875rem;
|
||||
white-space: nowrap;
|
||||
text-transform: capitalize;
|
||||
line-height: 0;
|
||||
-webkit-appearance: none;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
padding: 0 ${theme.layout.gap};
|
||||
overflow: hidden;
|
||||
transition: all 0.2s ease 0s;
|
||||
border-radius: 0;
|
||||
border: 1px solid ${theme.palette.border};
|
||||
text-decoration: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
button.active {
|
||||
border-bottom-color: transparent;
|
||||
background-color: ${theme.palette.background};
|
||||
color: ${theme.palette.foreground};
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
button:first-of-type {
|
||||
border-top-left-radius: ${theme.layout.radius};
|
||||
}
|
||||
|
||||
button:last-of-type {
|
||||
border-top-right-radius: ${theme.layout.radius};
|
||||
}
|
||||
|
||||
button+button {
|
||||
border-left: 0;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
</FieldsetContext.Provider>
|
||||
)
|
||||
})
|
||||
|
||||
export default withDefaults(FieldsetGroup, defaultProps)
|
||||
34
components/fieldset/fieldset-subtitle.tsx
Normal file
34
components/fieldset/fieldset-subtitle.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react'
|
||||
import useTheme from '../styles/use-theme'
|
||||
import withDefaults from '../utils/with-defaults'
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
className: ''
|
||||
}
|
||||
|
||||
export type FieldsetSubtitleProps = Props & typeof defaultProps & React.HTMLAttributes<HTMLHeadingElement>
|
||||
|
||||
const FieldsetSubtitle: React.FC<FieldsetSubtitleProps> = React.memo(({
|
||||
className, children, ...props
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className={className} {...props}>{children}</p>
|
||||
<style jsx>{`
|
||||
p {
|
||||
font-size: .875rem;
|
||||
line-height: 1.6;
|
||||
margin: ${theme.layout.gapHalf} 0;
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
export default withDefaults(FieldsetSubtitle, defaultProps)
|
||||
33
components/fieldset/fieldset-title.tsx
Normal file
33
components/fieldset/fieldset-title.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import React from 'react'
|
||||
import withDefaults from '../utils/with-defaults'
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
className: ''
|
||||
}
|
||||
|
||||
export type FieldsetTitleProps = Props & typeof defaultProps & React.HTMLAttributes<HTMLHeadingElement>
|
||||
|
||||
const FieldsetTitle: React.FC<FieldsetTitleProps> = React.memo(({
|
||||
className, children, ...props
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<h4 className={className} {...props}>{children}</h4>
|
||||
<style jsx>{`
|
||||
h4 {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
display: inline-flex;
|
||||
word-break: break-word;
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
export default withDefaults(FieldsetTitle, defaultProps)
|
||||
95
components/fieldset/fieldset.tsx
Normal file
95
components/fieldset/fieldset.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import React, { ReactNode, useEffect, useState } from 'react'
|
||||
import useTheme from '../styles/use-theme'
|
||||
import FieldsetTitle from './fieldset-title'
|
||||
import FieldsetSubtitle from './fieldset-subtitle'
|
||||
import FieldsetFooter from './fieldset-footer'
|
||||
import FieldsetGroup from './fieldset-group'
|
||||
import { hasChild, pickChild } from '../utils/collections'
|
||||
import { useFieldset } from './fieldset-context'
|
||||
|
||||
interface Props {
|
||||
value?: string
|
||||
label?: string
|
||||
disabled?: boolean
|
||||
title?: string | ReactNode
|
||||
subtitle?: string | ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
value: '',
|
||||
label: '',
|
||||
disabled: false,
|
||||
title: '',
|
||||
subtitle: '',
|
||||
className: '',
|
||||
}
|
||||
|
||||
export type FieldsetProps = Props & typeof defaultProps & React.FieldsetHTMLAttributes<any>
|
||||
|
||||
const Fieldset: React.FC<React.PropsWithChildren<FieldsetProps>> = React.memo(({
|
||||
className, title, subtitle, children, value, label,
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
const { inGroup, currentValue, register } = useFieldset()
|
||||
const [hidden, setHidden] = useState<boolean>(inGroup)
|
||||
const hasTitle = hasChild(children, FieldsetTitle)
|
||||
const hasSubtitle = hasChild(children, FieldsetSubtitle)
|
||||
const [withoutFooterChildren, FooterChildren] = pickChild(children, FieldsetFooter)
|
||||
|
||||
if (inGroup) {
|
||||
if (!label) {
|
||||
console.error('[Fieldset Group]: Props "label" is required when in a group.')
|
||||
}
|
||||
if (!value || value === '') {
|
||||
value = label
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const r: any = register({ value, label })
|
||||
r({ value, label })
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentValue || currentValue === '') return
|
||||
setHidden(currentValue !== value)
|
||||
}, [currentValue])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`fieldset ${className}`}>
|
||||
<div className="content">
|
||||
{withoutFooterChildren}
|
||||
{!hasTitle && <FieldsetTitle>{title}</FieldsetTitle>}
|
||||
{!hasSubtitle && <FieldsetSubtitle>{subtitle}</FieldsetSubtitle>}
|
||||
</div>
|
||||
{FooterChildren && <>{FooterChildren}</>}
|
||||
<style jsx>{`
|
||||
.fieldset {
|
||||
background-color: ${theme.palette.background};
|
||||
border: 1px solid ${theme.palette.border};
|
||||
border-radius: ${theme.layout.radius};
|
||||
overflow: hidden;
|
||||
display: ${hidden ? 'none' : 'block'};
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: ${theme.layout.gap};
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
Fieldset.defaultProps = defaultProps
|
||||
|
||||
type FieldsetComponent<P = {}> = React.FC<P> & {
|
||||
Title: typeof FieldsetTitle
|
||||
Subtitle: typeof FieldsetSubtitle
|
||||
Footer: typeof FieldsetFooter
|
||||
Group: typeof FieldsetGroup
|
||||
}
|
||||
|
||||
type ComponentProps = Partial<typeof defaultProps> & Omit<Props, keyof typeof defaultProps>
|
||||
|
||||
export default Fieldset as FieldsetComponent<ComponentProps>
|
||||
16
components/fieldset/index.ts
Normal file
16
components/fieldset/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import Fieldset from './fieldset'
|
||||
import FieldsetTitle from './fieldset-title'
|
||||
import FieldsetSubtitle from './fieldset-subtitle'
|
||||
import FieldsetFooter from './fieldset-footer'
|
||||
import FieldsetFooterStatus from './fieldset-footer-status'
|
||||
import FieldsetFooterActions from './fieldset-footer-actions'
|
||||
import FieldsetGroup from './fieldset-group'
|
||||
|
||||
FieldsetFooter.Status = FieldsetFooterStatus
|
||||
FieldsetFooter.Actions = FieldsetFooterActions
|
||||
Fieldset.Title = FieldsetTitle
|
||||
Fieldset.Subtitle = FieldsetSubtitle
|
||||
Fieldset.Footer = FieldsetFooter
|
||||
Fieldset.Group = FieldsetGroup
|
||||
|
||||
export default Fieldset
|
||||
Reference in New Issue
Block a user