Skip to content

Commit

Permalink
feat(fields): add MultiCheckbox (#37)
Browse files Browse the repository at this point in the history
* feat(elements): add Label
* feat(elements): add Field
* feat(elements): add Fieldset
* feat(elements): add Legend
* feat(fields): add MultiCheckbox
* feat(formiks): add FormikMultiCheckbox
  • Loading branch information
ivangabriele committed Nov 29, 2022
1 parent e6aeaec commit 405ce01
Show file tree
Hide file tree
Showing 18 changed files with 371 additions and 39 deletions.
9 changes: 9 additions & 0 deletions src/elements/Field.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import styled from 'styled-components'

import type { HTMLAttributes } from 'react'

export type FieldProps = HTMLAttributes<HTMLDivElement>
export const Field = styled.div`
display: flex;
flex-direction: column;
`
16 changes: 16 additions & 0 deletions src/elements/Fieldset.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import styled from 'styled-components'

import { Field } from './Field'

import type { FieldsetHTMLAttributes } from 'react'

export type FieldsetProps = FieldsetHTMLAttributes<HTMLFieldSetElement>
export function Fieldset(nativeProps: FieldsetProps) {
return <StyledField as="fieldset" {...nativeProps} />
}

const StyledField = styled(Field)`
border: 0;
margin: 0;
padding: 0;
`
11 changes: 11 additions & 0 deletions src/elements/Label.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import styled from 'styled-components'

import type { LabelHTMLAttributes } from 'react'

export type LabelProps = LabelHTMLAttributes<HTMLLabelElement>
export const Label = styled.label`
color: ${p => p.theme.color.slateGray};
font-size: 13px;
line-height: 1.4;
margin-bottom: 0.5rem;
`
18 changes: 18 additions & 0 deletions src/elements/Legend.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import styled from 'styled-components'

import { Label } from './Label'

import type { HTMLAttributes } from 'react'

export type LegendProps = HTMLAttributes<HTMLLegendElement> & {
isHidden?: boolean
}
export function Legend({ isHidden = false, ...nativeProps }: LegendProps) {
return <StyledLabel as="legend" isHidden={isHidden} {...nativeProps} />
}

export const StyledLabel = styled(Label)<{
isHidden: boolean
}>`
display: ${p => (p.isHidden ? 'none' : 'table')};
`
18 changes: 2 additions & 16 deletions src/fields/DatePicker/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import { useCallback, useEffect, useMemo, useRef } from 'react'
import styled from 'styled-components'

import { Fieldset } from '../../elements/Fieldset'
import { Legend } from '../../elements/Legend'
import { useForceUpdate } from '../../hooks/useForceUpdate'
import { getLocalizedDayjs } from '../../utils/getLocalizedDayjs'
import { getUtcizedDayjs } from '../../utils/getUtcizedDayjs'
Expand Down Expand Up @@ -225,12 +227,6 @@ export function DatePicker({
)
}

const Fieldset = styled.fieldset`
border: 0;
margin: 0;
padding: 0;
`

const Box = styled.div`
* {
font-weight: 500;
Expand All @@ -242,16 +238,6 @@ const Box = styled.div`
position: relative;
`

const Legend = styled.legend<{
isHidden: boolean
}>`
color: ${p => p.theme.color.slateGray};
display: ${p => (p.isHidden ? 'none' : 'table')};
font-weight: inherit;
margin-bottom: 0.5rem;
padding: 0;
`

const Field = styled.span<{
isEndDateField?: boolean
isTimeField?: boolean
Expand Down
18 changes: 2 additions & 16 deletions src/fields/DateRangePicker/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import { useCallback, useEffect, useMemo, useRef } from 'react'
import styled from 'styled-components'

import { Fieldset } from '../../elements/Fieldset'
import { Legend } from '../../elements/Legend'
import { useForceUpdate } from '../../hooks/useForceUpdate'
import { getLocalizedDayjs } from '../../utils/getLocalizedDayjs'
import { getUtcizedDayjs } from '../../utils/getUtcizedDayjs'
Expand Down Expand Up @@ -343,12 +345,6 @@ export function DateRangePicker({
)
}

const Fieldset = styled.fieldset`
border: 0;
margin: 0;
padding: 0;
`

const Box = styled.div`
* {
font-weight: 500;
Expand All @@ -360,16 +356,6 @@ const Box = styled.div`
position: relative;
`

const Legend = styled.legend<{
isHidden: boolean
}>`
color: ${p => p.theme.color.slateGray};
display: ${p => (p.isHidden ? 'none' : 'table')};
font-weight: inherit;
margin-bottom: 0.5rem;
padding: 0;
`

const Field = styled.span<{
isEndDateField?: boolean
isTimeField?: boolean
Expand Down
92 changes: 92 additions & 0 deletions src/fields/MultiCheckbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { equals, reject } from 'ramda'
import { useCallback, useMemo, useRef } from 'react'
import styled, { css } from 'styled-components'

import { Fieldset } from '../elements/Fieldset'
import { Legend } from '../elements/Legend'
import { Checkbox } from './Checkbox'

import type { Option } from '../types'
import type { Promisable } from 'type-fest'

export type MultiCheckboxProps = {
defaultValue?: string[]
isInline?: boolean
label?: string
name: string
onChange?: (nextValue: string[] | undefined) => Promisable<void>
options: Option[]
}
export function MultiCheckbox({
defaultValue = [],
isInline = false,
label,
name,
onChange,
options
}: MultiCheckboxProps) {
const checkedOptionValues = useRef<string[]>(defaultValue)

const key = useMemo(() => `${name}-${JSON.stringify(defaultValue)}`, [defaultValue, name])

const handleChange = useCallback(
(nextOptionValue: string, isChecked: boolean) => {
const nextCheckedOptionValues = isChecked
? [...checkedOptionValues.current, nextOptionValue]
: reject(equals(nextOptionValue))(checkedOptionValues.current)

checkedOptionValues.current = nextCheckedOptionValues

if (onChange) {
const normalizedNextValue = nextCheckedOptionValues.length ? nextCheckedOptionValues : undefined

onChange(normalizedNextValue)
}
},
[onChange]
)

return (
<Fieldset key={key}>
{label && <Legend>{label}</Legend>}

<ChecboxesBox isInline={isInline}>
{options.map((option, index) => (
<Checkbox
defaultChecked={defaultValue.includes(option.value)}
label={option.label}
name={`${name}${index}`}
onChange={(isChecked: boolean) => handleChange(option.value, isChecked)}
/>
))}
</ChecboxesBox>
</Fieldset>
)
}

const ChecboxesBox = styled.div<{
isInline: boolean
}>`
display: flex;
flex-direction: ${p => (p.isInline ? 'row' : 'column')};
> .rs-checkbox {
> .rs-checkbox-checker {
padding-left: 28px;
padding-top: 2px;
.rs-checkbox-wrapper {
left: 2px;
top: 0 !important;
}
}
}
${p =>
p.isInline &&
css`
> .rs-checkbox:not(:first-child) {
margin-left: 0.75rem;
}
`}
`
25 changes: 25 additions & 0 deletions src/formiks/FormikMultiCheckbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useField } from 'formik'
import { useCallback, useEffect } from 'react'

import { MultiCheckbox } from '../fields/MultiCheckbox'

import type { MultiCheckboxProps } from '../fields/MultiCheckbox'

export type FormikMultiCheckboxProps = Omit<MultiCheckboxProps, 'defaultValue' | 'onChange'>
export function FormikMultiCheckbox({ name, ...originalProps }: FormikMultiCheckboxProps) {
const [, , helpers] = useField(name)
// We don't include `setValues` in `useCallback()` and `useEffect()` dependencies
// both because it is useless and it will trigger infinite hook calls
const { setValue } = helpers

const handleChange = useCallback((nextValue: string[] | undefined) => {
setValue(nextValue)

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => () => setValue(undefined), [])

return <MultiCheckbox name={name} onChange={handleChange} {...originalProps} />
}
14 changes: 14 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
export * as MUI from './constants'
export { THEME } from './theme'

export { Field } from './elements/Field'
export { Fieldset } from './elements/Fieldset'
export { Label } from './elements/Label'
export { Legend } from './elements/Legend'

export { Checkbox } from './fields/Checkbox'
export { DateRangePicker } from './fields/DateRangePicker'
export { DatePicker } from './fields/DatePicker'
export { MultiCheckbox } from './fields/MultiCheckbox'
export { MultiSelect } from './fields/MultiSelect'
export { Select } from './fields/Select'
export { Textarea } from './fields/Textarea'
Expand All @@ -13,6 +19,7 @@ export { FormikCheckbox } from './formiks/FormikCheckbox'
export { FormikDatePicker } from './formiks/FormikDatePicker'
export { FormikDateRangePicker } from './formiks/FormikDateRangePicker'
export { FormikEffect } from './formiks/FormikEffect'
export { FormikMultiCheckbox } from './formiks/FormikMultiCheckbox'
export { FormikMultiSelect } from './formiks/FormikMultiSelect'
export { FormikSelect } from './formiks/FormikSelect'
export { FormikTextarea } from './formiks/FormikTextarea'
Expand All @@ -22,9 +29,15 @@ export { ThemeProvider } from './ThemeProvider'

export type { PartialTheme, Theme } from './theme'

export type { FieldProps } from './elements/Field'
export type { FieldsetProps } from './elements/Fieldset'
export type { LabelProps } from './elements/Label'
export type { LegendProps } from './elements/Legend'

export type { CheckboxProps } from './fields/Checkbox'
export type { DateRangePickerProps } from './fields/DateRangePicker'
export type { DatePickerProps } from './fields/DatePicker'
export type { MultiCheckboxProps } from './fields/MultiCheckbox'
export type { MultiSelectProps } from './fields/MultiSelect'
export type { SelectProps } from './fields/Select'
export type { TextareaProps } from './fields/Textarea'
Expand All @@ -34,6 +47,7 @@ export type { FormikCheckboxProps } from './formiks/FormikCheckbox'
export type { FormikDatePickerProps } from './formiks/FormikDatePicker'
export type { FormikDateRangePickerProps } from './formiks/FormikDateRangePicker'
export type { FormikEffectProps } from './formiks/FormikEffect'
export type { FormikMultiCheckboxProps } from './formiks/FormikMultiCheckbox'
export type { FormikMultiSelectProps } from './formiks/FormikMultiSelect'
export type { FormikSelectProps } from './formiks/FormikSelect'
export type { FormikTextareaProps } from './formiks/FormikTextarea'
Expand Down
18 changes: 18 additions & 0 deletions stories/elements/Field.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Field } from '../../src'

import type { FieldProps } from '../../src'

const args: FieldProps = {}

export default {
title: 'Elements/Field',
component: Field,

argTypes: {},

args
}

export const _Field = (props: FieldProps) => (
<Field {...props}>This is a field for form inputs but it’s basically a div.</Field>
)
20 changes: 20 additions & 0 deletions stories/elements/Fieldset.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Fieldset } from '../../src'

import type { FieldsetProps } from '../../src'

const args: FieldsetProps = {}

export default {
title: 'Elements/Fieldset',
component: Fieldset,

argTypes: {},

args
}

export const _Fieldset = (props: FieldsetProps) => (
<Fieldset {...props}>
This is am HTML {'<fieldset>'} for form inputs. It should contain a {'<Legend>'} element.
</Fieldset>
)
18 changes: 18 additions & 0 deletions stories/elements/Label.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Label } from '../../src'

import type { LabelProps } from '../../src'

const args: LabelProps = {
children: 'A form input label'
}

export default {
title: 'Elements/Label',
component: Label,

argTypes: {},

args
}

export const _Label = (props: LabelProps) => <Label {...props} />
19 changes: 19 additions & 0 deletions stories/elements/Legend.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Legend } from '../../src'

import type { LegendProps } from '../../src'

const args: LegendProps = {
children: 'A form fieldset legend',
isHidden: false
}

export default {
title: 'Elements/Legend',
component: Legend,

argTypes: {},

args
}

export const _Legend = (props: LegendProps) => <Legend {...props} />
Loading

0 comments on commit 405ce01

Please sign in to comment.