Skip to content

Commit

Permalink
refactor: checkbox component
Browse files Browse the repository at this point in the history
  • Loading branch information
nonzzz committed Apr 3, 2024
1 parent 705adfd commit 42a2877
Show file tree
Hide file tree
Showing 5 changed files with 293 additions and 4 deletions.
73 changes: 73 additions & 0 deletions src/client/components/checkbox/checkbox-group.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useScale, withScale } from '@geist-ui/core'
import * as stylex from '@stylexjs/stylex'
import { SCALES } from '../button'
import { CheckboxProvider } from './context'

interface Props {
value: string[]
disabled?: boolean
onChange?: (values: string[]) => void
}

export type CheckboxGroupProps = Props & Omit<React.HTMLAttributes<any>, keyof Props>

const defaultProps: Props = {
disabled: false,
value: []
}

const styles = stylex.create({
group: (scale: SCALES) => ({
width: scale.width(1, 'auto'),
height: scale.height(1, 'auto'),
padding: `${scale.pt(0)} ${scale.pr(0)} ${scale.pb(0)} ${scale.pl(0)}`,
margin: `${scale.mt(0)} ${scale.mr(0)} ${scale.mb(0)} ${scale.ml(0)}`,
':not(#__unused__) label': {
marginRight: `calc(${scale.font(1)} * 2)`,
'--checkbox-size': scale.font(1)
},
':not(#__unused__) label:last-of-type': {
marginRight: 0
}
})
})

function CheckboxGroupComponent(props: React.PropsWithChildren< CheckboxGroupProps>) {
const { children, value, disabled = false, onChange, ...rest } = props
const { SCALES } = useScale()
const [selfValue, setSelfValue] = useState<string[]>([])

const updateState = useCallback((val: string, checked: boolean) => {
const removed = selfValue.filter(v => v !== val)
const next = checked ? [...removed, val] : removed
setSelfValue(next)
onChange?.(next)
}, [selfValue, onChange])

useEffect(() => {
setSelfValue([...value])
}, [value])

const contextValue = useMemo(() => {
return {
disabledAll: disabled,
values: selfValue,
inGroup: true,
updateState
}
}, [disabled, selfValue, updateState])

return (
<CheckboxProvider value={contextValue}>
<div {...stylex.props(styles.group(SCALES))} {...rest}>
{children}
</div>
</CheckboxProvider>
)
}

CheckboxGroupComponent.defaultProps = defaultProps
CheckboxGroupComponent.displayName = 'CheckboxGroup'

export const CheckboxGroup = withScale(CheckboxGroupComponent)
169 changes: 169 additions & 0 deletions src/client/components/checkbox/checkbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import React, { useCallback, useEffect, useState } from 'react'
import { useClasses, useScale, withScale } from '@geist-ui/core'
import * as stylex from '@stylexjs/stylex'
import { SCALES } from '../button'
import { useCheckbox } from './context'

export interface CheckboxEventTarget {
checked: boolean
}

export interface CheckboxEvent {
target: CheckboxEventTarget
stopPropagation: () => void
preventDefault: () => void
nativeEvent: React.ChangeEvent
}

interface CheckboxIconProps {
checked: boolean
disabled: boolean
}

interface Props {
checked?: boolean
disabled?: boolean
value?: string
onChange?: (e: CheckboxEvent) => void
}

export type CheckboxProps = Props & Omit<React.InputHTMLAttributes<HTMLInputElement>, keyof Props>

const defaultProps: Props = {
disabled: false,
value: ''
}

const styles = stylex.create({
checkbox: (scale: SCALES, disabled) => ({
display: 'inline-flex',
justifyContent: 'center',
alignItems: 'center',
'--checkbox-size': scale.font(0.875),
cursor: disabled ? 'not-allowed' : 'pointer',
opacity: disabled ? 0.75 : 1,
lineHeight: 'var(--checkbox-size)',
width: scale.width(1, 'auto'),
height: scale.height(1, 'var(--checkbox-size)'),
padding: `${scale.pt(0)} ${scale.pr(0)} ${scale.pb(0)} ${scale.pl(0)}`,
margin: `${scale.mt(0)} ${scale.mr(0)} ${scale.mb(0)} ${scale.ml(0)}`
}),
text: (disabled) => ({
fontSize: 'var(--checkbox-size)',
lineHeight: 'var(--checkbox-size)',
paddingLeft: 'calc(var(--checkbox-size) * 0.5)',
userSelect: 'none',
cursor: disabled ? 'not-allowed' : 'pointer'
}),
input: {
opacity: 0,
outline: 'none',
position: 'absolute',
width: 0,
height: 0,
margin: 0,
padding: 0,
zIndex: -1,
fontSize: 0,
backgroundColor: 'transparent'
},
svg: (disabled) => ({
display: 'inline-flex',
width: 'calc(var(--checkbox-size) * 0.86)',
height: 'calc(var(--checkbox-size) * 0.86)',
userSelect: 'none',
opacity: disabled ? 0.4 : 1,
cursor: disabled ? 'not-allowed' : 'pointer'
})
})

function CheckboxIcon(props: CheckboxIconProps) {
const { checked, disabled } = props
const c = stylex.props(styles.svg(disabled))

if (checked) {
return (
<svg viewBox="0 0 17 16" fill="none" {...c}>
<path
fill="#000"
d="M12.1429 0H3.85714C1.7269 0 0 1.79086 0 4V12C0 14.2091 1.7269 16 3.85714 16H12.1429C14.2731 16 16 14.2091 16 12V4C16 1.79086 14.2731 0 12.1429 0Z"
/>
<path d="M16 3L7.72491 11L5 8" stroke="#fff" strokeWidth="1.5" />
</svg>
)
}

return (
<svg viewBox="0 0 12 12" fill="none" {...c}>
<path
stroke="#666"
d="M8.5 0.5H3.5C1.84315 0.5 0.5 1.84315 0.5 3.5V8.5C0.5 10.1569 1.84315 11.5 3.5 11.5H8.5C10.1569 11.5 11.5 10.1569 11.5 8.5V3.5C11.5 1.84315 10.1569 0.5 8.5 0.5Z"
/>
</svg>
)
}

function CheckboxComponent(props: CheckboxProps) {
const {
checked, className: userClassName, style: userStyle,
value, disabled = false, onChange, children, ...rest } =
props
const { disabledAll, inGroup, values, updateState } = useCheckbox()
const { SCALES } = useScale()
const { className, style } = stylex.props(styles.input)
const classes = useClasses(className, userClassName)
const [selfChecked, setSelfChecked] = useState<boolean>(false)
const isDisabled = inGroup ? disabledAll || disabled : disabled

const handleChange = useCallback((e: React.ChangeEvent) => {
if (disabled) return
const evt: CheckboxEvent = {
target: {
checked: !selfChecked
},
stopPropagation: e.stopPropagation,
preventDefault: e.preventDefault,
nativeEvent: e
}
if (inGroup) {
updateState(value || '', !selfChecked)
}
setSelfChecked(pre => !pre)
onChange?.(evt)
}, [onChange, disabled, selfChecked, value, inGroup, updateState])

useEffect(() => {
if (checked === undefined) return
setSelfChecked(checked)
}, [checked])

useEffect(() => {
if (inGroup) {
if (!values.length) return setSelfChecked(false)
const next = values.includes(value || '')
if (next === selfChecked) return
setSelfChecked(next)
}
}, [value, values, selfChecked, inGroup])

return (
<label {...stylex.props(styles.checkbox(SCALES, isDisabled))}>
<CheckboxIcon checked={selfChecked} disabled={disabled} />
<input
disabled={isDisabled}
onChange={handleChange}
checked={selfChecked}
className={classes}
style={{ ...style, ...userStyle }}
type="checkbox"
{...rest}
/>
<span {...stylex.props(styles.text(isDisabled))}>{children}</span>
</label>
)
}

CheckboxComponent.defaultProps = defaultProps
CheckboxComponent.displayName = 'Checkbox'

export const Checkbox = withScale(CheckboxComponent)
24 changes: 24 additions & 0 deletions src/client/components/checkbox/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { createContext, useContext } from 'react'
import { noop } from 'foxact/noop'

export interface CheckboxContext {
disabledAll: boolean
values: string[]
inGroup: boolean,
updateState: (val: string, checked: boolean) => void
}

const initialValue: CheckboxContext = {
disabledAll: false,
values: [],
inGroup: false,
updateState: noop
}

export const CheckboxContext = createContext<CheckboxContext>(initialValue)

export function useCheckbox() {
return useContext(CheckboxContext)
}

export const CheckboxProvider = CheckboxContext.Provider
12 changes: 12 additions & 0 deletions src/client/components/checkbox/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Checkbox as _Checkbox } from './checkbox'
import { CheckboxGroup } from './checkbox-group'

export type { CheckboxEvent } from './checkbox'

export type CheckboxComponentType = typeof _Checkbox & {
Group: typeof CheckboxGroup
}

(_Checkbox as CheckboxComponentType).Group = CheckboxGroup

export const Checkbox = _Checkbox as CheckboxComponentType
19 changes: 15 additions & 4 deletions src/client/components/file-list.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { useMemo } from 'react'
import { Checkbox, Spacer } from '@geist-ui/core'
import { Spacer } from '@geist-ui/core'
import stylex from '@stylexjs/stylex'
import { noop } from 'foxact/noop'
import type { Foam, Sizes } from '../interface'
import { Checkbox } from './checkbox'
import type { CheckboxEvent } from './checkbox'
import { ModuleItem } from './module-item'

export interface FileListProps<F> {
Expand All @@ -16,6 +18,11 @@ export interface FileListProps<F> {
const styles = stylex.create({
container: {
overflow: 'hidden'
},
baseline: {
':not(#__unused__) > div': {
alignItems: 'baseline'
}
}
})

Expand Down Expand Up @@ -43,21 +50,25 @@ export function FileList<F extends Foam>(props: FileListProps<F>) {
return Array.from(scence)
}, [checkAll, scence, userFiles])

const handleChange = (e: CheckboxEvent) => {
const { checked } = e.target
onChange(checked ? userFiles.map(v => v.id) : [])
}
return (
<div {...stylex.props(styles.container)}>
<ModuleItem name={all.name} size={all.extra}>
<ModuleItem name={all.name} size={all.extra} {...stylex.props(styles.baseline)}>
<Checkbox
value={all.name}
font="14px"
scale={0.85}
checked={checkAll}
onChange={() => onChange(checkAll ? [] : userFiles.map(v => v.id))}
onChange={handleChange}
/>
</ModuleItem>
<Spacer h={0.75} />
<Checkbox.Group font="14px" scale={0.85} value={groupValues} onChange={onChange}>
{files.map(file => (
<ModuleItem name={file.name} size={file.extra} key={file.name}>
<ModuleItem name={file.name} size={file.extra} key={file.name} {...stylex.props(styles.baseline)}>
<Checkbox value={file.name} />
</ModuleItem>
))}
Expand Down

0 comments on commit 42a2877

Please sign in to comment.