Skip to content

Commit

Permalink
feat(fields): add MultiRadio (#38)
Browse files Browse the repository at this point in the history
* feat(fields): add MultiRadio
* feat(formiks): add FormikMultiRadio
  • Loading branch information
ivangabriele committed Nov 29, 2022
1 parent 405ce01 commit de205f9
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/fields/MultiCheckbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,10 @@ export function MultiCheckbox({
const ChecboxesBox = styled.div<{
isInline: boolean
}>`
color: ${p => p.theme.color.gunMetal};
display: flex;
flex-direction: ${p => (p.isInline ? 'row' : 'column')};
font-weight: 500;
> .rs-checkbox {
> .rs-checkbox-checker {
Expand Down
91 changes: 91 additions & 0 deletions src/fields/MultiRadio.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import { Radio } from 'rsuite'
import styled, { css } from 'styled-components'

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

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

export type MultiRadioProps = {
defaultValue?: string
isInline?: boolean
label?: string
name: string
onChange?: (nextValue: string | undefined) => Promisable<void>
options: Option[]
}
export function MultiRadio({ defaultValue, isInline = false, label, name, onChange, options }: MultiRadioProps) {
const [checkedOptionValue, setCheckedOptionValue] = useState<string | undefined>(undefined)

const key = useMemo(() => `${name}-${String(checkedOptionValue)}}`, [checkedOptionValue, name])

const handleChange = useCallback(
(nextOptionValue: string, isChecked: boolean) => {
const nextCheckedOptionValue = isChecked ? nextOptionValue : undefined

setCheckedOptionValue(nextCheckedOptionValue)

if (onChange) {
onChange(nextCheckedOptionValue)
}
},
[onChange]
)

// TODO There may be a better solution.
// A key change is not enough to force radio checked check changes
// on `defaultValue` property update (even when appending `defaultValue` to `key`),
// we need to force a second re-render in order for the changes to be applied.
useEffect(() => {
setCheckedOptionValue(defaultValue)
}, [defaultValue])

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

<ChecboxesBox isInline={isInline}>
{options.map(option => (
<Radio
defaultChecked={option.value === checkedOptionValue}
name={name}
onChange={(_: any, isChecked: boolean) => handleChange(option.value, isChecked)}
>
{option.label}
</Radio>
))}
</ChecboxesBox>
</Fieldset>
)
}

const ChecboxesBox = styled.div<{
isInline: boolean
}>`
color: ${p => p.theme.color.gunMetal};
display: flex;
flex-direction: ${p => (p.isInline ? 'row' : 'column')};
font-weight: 500;
> .rs-radio {
> .rs-radio-checker {
padding-left: 28px;
padding-top: 2px;
.rs-radio-wrapper {
left: 2px;
top: 3px !important;
}
}
}
${p =>
p.isInline &&
css`
> .rs-radio:not(:first-child) {
margin-left: 0.75rem;
}
`}
`
25 changes: 25 additions & 0 deletions src/formiks/FormikMultiRadio.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 { MultiRadio } from '../fields/MultiRadio'

import type { MultiRadioProps } from '../fields/MultiRadio'

export type FormikMultiRadioProps = Omit<MultiRadioProps, 'defaultValue' | 'onChange'>
export function FormikMultiRadio({ name, ...originalProps }: FormikMultiRadioProps) {
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 <MultiRadio name={name} onChange={handleChange} {...originalProps} />
}
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export { DateRangePicker } from './fields/DateRangePicker'
export { DatePicker } from './fields/DatePicker'
export { MultiCheckbox } from './fields/MultiCheckbox'
export { MultiSelect } from './fields/MultiSelect'
export { MultiRadio } from './fields/MultiRadio'
export { Select } from './fields/Select'
export { Textarea } from './fields/Textarea'
export { TextInput } from './fields/TextInput'
Expand All @@ -21,6 +22,7 @@ export { FormikDateRangePicker } from './formiks/FormikDateRangePicker'
export { FormikEffect } from './formiks/FormikEffect'
export { FormikMultiCheckbox } from './formiks/FormikMultiCheckbox'
export { FormikMultiSelect } from './formiks/FormikMultiSelect'
export { FormikMultiRadio } from './formiks/FormikMultiRadio'
export { FormikSelect } from './formiks/FormikSelect'
export { FormikTextarea } from './formiks/FormikTextarea'
export { FormikTextInput } from './formiks/FormikTextInput'
Expand All @@ -39,6 +41,7 @@ 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 { MultiRadioProps } from './fields/MultiRadio'
export type { SelectProps } from './fields/Select'
export type { TextareaProps } from './fields/Textarea'
export type { TextInputProps } from './fields/TextInput'
Expand All @@ -49,6 +52,7 @@ 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 { FormikMultiRadioProps } from './formiks/FormikMultiRadio'
export type { FormikSelectProps } from './formiks/FormikSelect'
export type { FormikTextareaProps } from './formiks/FormikTextarea'
export type { FormikTextInputProps } from './formiks/FormikTextInput'
45 changes: 45 additions & 0 deletions stories/fields/MultiRadio.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useState } from 'react'

import { MultiRadio } from '../../src'
import { Output } from '../_components/Output'

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

const args: MultiRadioProps = {
defaultValue: undefined,
isInline: false,
label: 'Pick an option',
name: 'myMultiRadio',
options: [
{ label: 'First Option', value: 'FIRST_OPTION' },
{ label: 'Second Option', value: 'SECOND_OPTION' },
{ label: 'Third Option', value: 'THIRD_OPTION' },
{ label: 'A Very Very Long Option', value: 'A_VERY_VERY_LONG_OPTION' }
]
}

export default {
title: 'Fields/MultiRadio',
component: MultiRadio,

argTypes: {
defaultValue: {
control: 'inline-radio',
options: ['FIRST_OPTION', 'SECOND_OPTION', 'THIRD_OPTION', 'A_VERY_VERY_LONG_OPTION']
}
},

args
}

export function _MultiRadio(props: MultiRadioProps) {
const [outputValue, setOutputValue] = useState<string | undefined | '∅'>('∅')

return (
<>
<MultiRadio {...props} onChange={setOutputValue} />

{outputValue !== '∅' && <Output value={outputValue} />}
</>
)
}
53 changes: 53 additions & 0 deletions stories/formiks/FormikMultiRadio.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Formik } from 'formik'
import { useMemo, useState } from 'react'

import { FormikEffect, FormikMultiRadio } from '../../src'
import { Output } from '../_components/Output'
import { noop } from '../_utils/noop'

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

const args: FormikMultiRadioProps = {
label: 'Pick an option',
name: 'myMultiRadio',
options: [
{ label: 'First Option', value: 'FIRST_OPTION' },
{ label: 'Second Option', value: 'SECOND_OPTION' },
{ label: 'Third Option', value: 'THIRD_OPTION' },
{ label: 'A Very Very Long Option', value: 'A_VERY_VERY_LONG_OPTION' }
]
}

export default {
title: 'Formiks/FormikMultiRadio',
component: FormikMultiRadio,

argTypes: {},

args
}

export const _FormikMultiRadio = (props: FormikMultiRadioProps) => {
const [outputValue, setOutputValue] = useState<
| {
mySelect?: string
}
| '∅'
>('∅')

const key = useMemo(() => props.name, [props.name])

return (
<>
<Formik key={key} initialValues={{}} onSubmit={noop}>
<>
<FormikEffect onChange={setOutputValue} />

<FormikMultiRadio {...props} />
</>
</Formik>

{outputValue !== '∅' && <Output value={outputValue} />}
</>
)
}

0 comments on commit de205f9

Please sign in to comment.