Skip to content

Commit

Permalink
feat(fields): add MultiSelect (#26)
Browse files Browse the repository at this point in the history
* build(storybook): enforce React strict mode
* feat(fields): add MultiSelect
  • Loading branch information
ivangabriele committed Nov 28, 2022
1 parent 8a85459 commit e1123b1
Show file tree
Hide file tree
Showing 10 changed files with 140 additions and 52 deletions.
11 changes: 7 additions & 4 deletions .storybook/preview.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { THEME } from '../src/theme'

import 'rsuite/dist/rsuite.min.css'
import '../src/assets/rsuite-override.css'
import { StrictMode } from 'react'

const GlobalStyle = createGlobalStyle`
p {
Expand All @@ -13,11 +14,13 @@ const GlobalStyle = createGlobalStyle`

export const decorators = [
Story => (
<ThemeProvider theme={THEME}>
<GlobalStyle />
<StrictMode>
<ThemeProvider theme={THEME}>
<GlobalStyle />

<Story />
</ThemeProvider>
<Story />
</ThemeProvider>
</StrictMode>
)
]

Expand Down
50 changes: 50 additions & 0 deletions src/fields/MultiSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useCallback, useMemo } from 'react'
import { TagPicker } from 'rsuite'
import styled from 'styled-components'

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

export type MultiSelectProps = Omit<TagPickerProps, 'as' | 'data' | 'defaultValue' | 'onChange' | 'value'> & {
defaultValue?: string[]
name: string
onChange?: (nextValue: string[] | undefined) => Promisable<void>
options: Option[]
}
export function MultiSelect({
onChange,
options,
// eslint-disable-next-line @typescript-eslint/naming-convention
searchable = false,
...originalProps
}: MultiSelectProps) {
const key = useMemo(
() => `${originalProps.name}-${JSON.stringify(originalProps.defaultValue)}`,
[originalProps.defaultValue, originalProps.name]
)

const handleChange = useCallback(
(nextValue: string[] | null) => {
if (!onChange) {
return
}

const normalizedNextValue = !nextValue || !nextValue.length ? undefined : nextValue

onChange(normalizedNextValue)
},
[onChange]
)

return <StyledTagPicker key={key} data={options} onChange={handleChange} searchable={searchable} {...originalProps} />
}

const StyledTagPicker = styled(TagPicker)`
cursor: pointer;
width: 9.25rem;
> .rs-picker-toggle {
cursor: inherit;
}
`
41 changes: 8 additions & 33 deletions src/fields/Select.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,18 @@
import { useCallback, useMemo } from 'react'
import { SelectPicker, TagPicker } from 'rsuite'
import { SelectPicker } from 'rsuite'
import styled from 'styled-components'

import type { Option } from '../types'
import type { SelectPickerProps as RsuiteSelectPickerProps, TagPickerProps } from 'rsuite'
import type { SelectPickerProps } from 'rsuite'
import type { Promisable } from 'type-fest'

type SelectCommonProps = {
export type SelectProps = Omit<SelectPickerProps<any>, 'as' | 'data' | 'defaultValue' | 'onChange' | 'value'> & {
defaultValue?: string
name: string
onChange?: (nextValue: string | undefined) => Promisable<void>
options: Option[]
}
export type MultiSelectProps = Omit<TagPickerProps, 'as' | 'data' | 'onChange' | 'value'> &
SelectCommonProps & {
isMulti: true
onChange?: (values: string[] | undefined) => Promisable<void>
}
export type SingleSelectProps = Omit<RsuiteSelectPickerProps<any>, 'as' | 'data' | 'onChange' | 'value'> &
SelectCommonProps & {
isMulti?: false
onChange?: (value: string | undefined) => Promisable<void>
}
export type SelectProps = MultiSelectProps | SingleSelectProps
export function Select({
isMulti = false,
onChange,
options,
// eslint-disable-next-line @typescript-eslint/naming-convention
Expand All @@ -35,23 +25,18 @@ export function Select({
)

const handleChange = useCallback(
(valueOrValues: string | string[] | null) => {
(nextValue: string | null) => {
if (!onChange) {
return
}

const normalizedValueOrValues =
!valueOrValues || (Array.isArray(valueOrValues) && !valueOrValues.length) ? undefined : valueOrValues
const normalizedNextValue = nextValue ?? undefined

;(onChange as (valueOrValues: string | string[] | undefined) => Promisable<void>)(normalizedValueOrValues)
onChange(normalizedNextValue)
},
[onChange]
)

if (isMulti) {
return <StyledTagPicker data={options} onChange={handleChange} searchable={searchable} {...originalProps} />
}

return (
<StyledSelectPicker
key={key}
Expand All @@ -67,14 +52,4 @@ export function Select({

const StyledSelectPicker = styled(SelectPicker)`
display: inline-flex;
width: 9.25rem;
`

const StyledTagPicker = styled(TagPicker)`
cursor: pointer;
width: 9.25rem;
> .rs-picker-toggle {
cursor: inherit;
}
`
9 changes: 6 additions & 3 deletions src/formiks/FormikCheckbox.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useField } from 'formik'
import { useCallback, useMemo } from 'react'
import { useCallback, useEffect, useMemo } from 'react'

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

Expand All @@ -12,15 +12,18 @@ export function FormikCheckbox({ name, ...originalProps }: FormikCheckboxProps)
const [field, , helpers] = useField(name)

const value = useMemo(() => field.value, [field.value])
// We don't include `setValues` in `useCallback()` and `useEffect()` dependencies
// both because it is useless and it will trigger infinite hook calls
const setValue = useMemo(() => helpers.setValue, [helpers.setValue])

// We don't include `setValues` in `useCallback()` dependencok calls
// both because it is useless and it will trigger infinite ho
const handleChange = useCallback((isChecked: boolean) => {
setValue(isChecked)

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

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

return <Checkbox defaultChecked={value} name={name} onChange={handleChange} {...originalProps} />
}
2 changes: 1 addition & 1 deletion src/formiks/FormikDatePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function FormikDatePicker({ name, ...originalProps }: FormikDatePickerPro
const [, , helpers] = useField(name)
const { setValue } = helpers

// We don't include `setValues` in `useEffect()` dependencies
// We don't include `setValues` in `useEffect()` dependencies
// both because it is useless and it will trigger infinite hook calls
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => () => setValue(undefined), [])
Expand Down
4 changes: 2 additions & 2 deletions src/formiks/FormikSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ export function FormikSelect({ name, ...originalProps }: FormikSelectProps) {
// both because it is useless and it will trigger infinite hook calls
const { setValue } = helpers

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

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export { THEME } from './theme'
export { Checkbox } from './fields/Checkbox'
export { DateRangePicker } from './fields/DateRangePicker'
export { DatePicker } from './fields/DatePicker'
export { MultiSelect } from './fields/MultiSelect'
export { Select } from './fields/Select'

export { FormikCheckbox } from './formiks/FormikCheckbox'
Expand All @@ -19,6 +20,7 @@ export type { PartialTheme, Theme } from './theme'
export type { CheckboxProps } from './fields/Checkbox'
export type { DateRangePickerProps } from './fields/DateRangePicker'
export type { DatePickerProps } from './fields/DatePicker'
export type { MultiSelectProps } from './fields/MultiSelect'
export type { SelectProps } from './fields/Select'

export type { FormikCheckboxProps } from './formiks/FormikCheckbox'
Expand Down
47 changes: 47 additions & 0 deletions stories/fields/MultiSelect.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useState } from 'react'

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

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

const args: MultiSelectProps = {
defaultValue: undefined,
placeholder: 'Pick some options',
name: 'myMultiSelect',
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/MultiSelect',
component: MultiSelect,

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

args
}

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

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

{outputValue !== '∅' && <Output value={outputValue} />}
</>
)
}
22 changes: 15 additions & 7 deletions stories/fields/Select.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,36 @@ import { Output } from '../_components/Output'
import type { SelectProps } from '../../src'

const args: SelectProps = {
defaultValue: 'FIRST_OPTION',
isMulti: false,
placeholder: 'Pick an option',
defaultValue: undefined,
placeholder: 'Pick some options',
name: 'mySelect',
options: [
{ label: 'First Option', value: 'FIRST_OPTION' },
{ label: 'Second Option', value: 'SECOND_OPTION' },
{ label: 'Third Option', value: 'THIRD_OPTION' }
{ label: 'Third Option', value: 'THIRD_OPTION' },
{ label: 'A Very Very Long Option', value: 'A_VERY_VERY_LONG_OPTION' }
]
}

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

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

args
}

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

return (
<>
Expand Down
4 changes: 2 additions & 2 deletions stories/formiks/FormikSelect.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ import { noop } from '../_utils/noop'
import type { FormikSelectProps } from '../../src'

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

Expand Down

0 comments on commit e1123b1

Please sign in to comment.