Skip to content

Commit

Permalink
fix(design system): update available options if they change
Browse files Browse the repository at this point in the history
  • Loading branch information
g-saracca committed Jun 28, 2024
1 parent ef92eb0 commit 09afbbe
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ForwardedRef, forwardRef, useEffect, useId, useReducer } from 'react'
import { useState, useEffect, useMemo, useId, useReducer, forwardRef, ForwardedRef } from 'react'
import { Dropdown as DropdownBS } from 'react-bootstrap'
import {
selectAdvancedReducer,
Expand All @@ -7,11 +7,12 @@ import {
selectAllOptions,
deselectAllOptions,
searchOptions,
getSelectAdvancedInitialState
getSelectAdvancedInitialState,
updateOptions
} from './selectAdvancedReducer'
import { SelectAdvancedToggle } from './SelectAdvancedToggle'
import { SelectAdvancedMenu } from './SelectAdvancedMenu'
import { debounce } from './utils'
import { areArraysEqual, debounce } from './utils'
import { useIsFirstRender } from './useIsFirstRender'

export const DEFAULT_LOCALES = {
Expand All @@ -23,7 +24,7 @@ export const SELECT_MENU_SEARCH_DEBOUNCE_TIME = 400
export type SelectAdvancedProps =
| {
isMultiple?: false
initialOptions: string[]
options: string[]
onChange?: (selected: string) => void
defaultValue?: string
isSearchable?: boolean
Expand All @@ -36,7 +37,7 @@ export type SelectAdvancedProps =
}
| {
isMultiple: true
initialOptions: string[]
options: string[]
onChange?: (selected: string[]) => void
defaultValue?: string[]
isSearchable?: boolean
Expand All @@ -51,7 +52,7 @@ export type SelectAdvancedProps =
export const SelectAdvanced = forwardRef(
(
{
initialOptions,
options: propsOption,
onChange,
defaultValue,
isMultiple,
Expand All @@ -63,9 +64,9 @@ export const SelectAdvanced = forwardRef(
}: SelectAdvancedProps,
ref: ForwardedRef<HTMLInputElement | null>
) => {
const dynamicInitialOptions = isMultiple
? initialOptions
: [locales?.select ?? DEFAULT_LOCALES.select, ...initialOptions]
const dynamicInitialOptions = useMemo(() => {
return isMultiple ? propsOption : [locales?.select ?? DEFAULT_LOCALES.select, ...propsOption]
}, [isMultiple, propsOption, locales])

const [{ selected, filteredOptions, searchValue, options }, dispatch] = useReducer(
selectAdvancedReducer,
Expand All @@ -79,12 +80,46 @@ export const SelectAdvanced = forwardRef(

const isFirstRender = useIsFirstRender()
const menuId = useId()
const [lastOnChangeValue, setLastOnChangeValue] = useState<string | string[]>(
isMultiple ? [] : ''
)

useEffect(() => {
if (!isFirstRender && onChange) {
// Dont call onChange if the selected options (string[]) remain the same
if (isMultiple) {
const selectedOptionsRemainTheSame = areArraysEqual(
selected as string[],
defaultValue && (lastOnChangeValue as string[])?.length === 0
? defaultValue
: (lastOnChangeValue as string[])
)

if (selectedOptionsRemainTheSame) return
}
// Dont call onChange if the selected option (string) remain the same
if (!isMultiple) {
const compareAgainst =
defaultValue && (lastOnChangeValue as string) === ''
? defaultValue
: (lastOnChangeValue as string)
const selectedOptionRemainTheSame = selected === compareAgainst

if (selectedOptionRemainTheSame) return
}

isMultiple ? onChange(selected as string[]) : onChange(selected as string)
setLastOnChangeValue(selected)
}
}, [isMultiple, selected, isFirstRender, onChange])
}, [isMultiple, selected, isFirstRender, onChange, lastOnChangeValue, defaultValue])

useEffect(() => {
const optionsRemainTheSame = propsOption.every((option) => options.includes(option))

if (optionsRemainTheSame) return

dispatch(updateOptions(dynamicInitialOptions))
}, [dynamicInitialOptions, propsOption, options, isFirstRender, dispatch])

const handleSearch = debounce((e: React.ChangeEvent<HTMLInputElement>): void => {
const { value } = e.target
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ type SelectAdvancedActions =
type: 'SEARCH'
payload: string
}
| {
type: 'UPDATE_OPTIONS'
payload: string[]
}

export const selectAdvancedReducer = (
state: SelectAdvancedState,
Expand Down Expand Up @@ -97,6 +101,11 @@ export const selectAdvancedReducer = (
filteredOptions: filterOptions(state, action),
searchValue: action.payload
}
case 'UPDATE_OPTIONS':
return {
...state,
options: action.payload
}
default:
return state
}
Expand Down Expand Up @@ -140,3 +149,10 @@ export const searchOptions = /* istanbul ignore next */ (value: string): SelectA
type: 'SEARCH',
payload: value
})

export const updateOptions = /* istanbul ignore next */ (
options: string[]
): SelectAdvancedActions => ({
type: 'UPDATE_OPTIONS',
payload: options
})
21 changes: 21 additions & 0 deletions packages/design-system/src/lib/components/select-advanced/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,24 @@ export function debounce<T extends (...args: any[]) => unknown>(
timeoutId = setTimeout(() => fn(...args), delay)
}
}

export function areArraysEqual(arr1: string[], arr2: string[]): boolean {
if (arr1.length === 0 && arr2.length === 0) {
return true
}

if (arr1.length !== arr2.length) {
return false
}

const sortedArr1 = arr1.slice().sort()
const sortedArr2 = arr2.slice().sort()

for (let i = 0; i < sortedArr1.length; i++) {
if (sortedArr1[i] !== sortedArr2[i]) {
return false
}
}

return true
}
4 changes: 2 additions & 2 deletions packages/design-system/src/lib/stories/form/Form.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ export const SelectAdvanced: Story = {
</Form.Group.Label>
<Col sm={9}>
<Form.Group.SelectAdvanced
initialOptions={['Reading', 'Swimming', 'Running', 'Cycling', 'Cooking', 'Gardening']}
options={['Reading', 'Swimming', 'Running', 'Cycling', 'Cooking', 'Gardening']}
inputButtonId="basic-form-select-advanced"
/>
</Col>
Expand All @@ -240,7 +240,7 @@ export const SelectAdvancedMultiple: Story = {
<Col sm={9}>
<Form.Group.SelectAdvanced
isMultiple
initialOptions={['Reading', 'Swimming', 'Running', 'Cycling', 'Cooking', 'Gardening']}
options={['Reading', 'Swimming', 'Running', 'Cycling', 'Cooking', 'Gardening']}
inputButtonId="basic-form-select-advanced"
/>
</Col>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { Meta, StoryObj } from '@storybook/react'
import { SelectAdvanced } from '../../components/select-advanced/SelectAdvanced'
import { Button } from '../../components/button/Button'
import { CanvasFixedHeight } from '../CanvasFixedHeight'
import { useState } from 'react'

/**
* ## Description
Expand All @@ -21,22 +23,22 @@ const exampleOptions = ['Option 1', 'Option 2', 'Option 3', 'Option 4']
export const Single: Story = {
render: () => (
<CanvasFixedHeight height={250}>
<SelectAdvanced initialOptions={exampleOptions} />
<SelectAdvanced options={exampleOptions} />
</CanvasFixedHeight>
)
}
export const Multiple: Story = {
render: () => (
<CanvasFixedHeight height={250}>
<SelectAdvanced isMultiple initialOptions={exampleOptions} />
<SelectAdvanced isMultiple options={exampleOptions} />
</CanvasFixedHeight>
)
}

export const SingleWithDefaultValue: Story = {
render: () => (
<CanvasFixedHeight height={250}>
<SelectAdvanced initialOptions={exampleOptions} defaultValue={exampleOptions[2]} />
<SelectAdvanced options={exampleOptions} defaultValue={exampleOptions[2]} />
</CanvasFixedHeight>
)
}
Expand All @@ -45,7 +47,7 @@ export const MultipleWithDefaultValues: Story = {
<CanvasFixedHeight height={250}>
<SelectAdvanced
isMultiple
initialOptions={exampleOptions}
options={exampleOptions}
defaultValue={[exampleOptions[0], exampleOptions[2]]}
/>
</CanvasFixedHeight>
Expand All @@ -55,39 +57,76 @@ export const MultipleWithDefaultValues: Story = {
export const SingleNotSearchable: Story = {
render: () => (
<CanvasFixedHeight height={250}>
<SelectAdvanced initialOptions={exampleOptions} isSearchable={false} />
<SelectAdvanced options={exampleOptions} isSearchable={false} />
</CanvasFixedHeight>
)
}

export const MultipleNotSearchable: Story = {
render: () => (
<CanvasFixedHeight height={250}>
<SelectAdvanced isMultiple initialOptions={exampleOptions} isSearchable={false} />
<SelectAdvanced isMultiple options={exampleOptions} isSearchable={false} />
</CanvasFixedHeight>
)
}

export const Invalid: Story = {
render: () => (
<CanvasFixedHeight height={250}>
<SelectAdvanced initialOptions={exampleOptions} isInvalid />
<SelectAdvanced options={exampleOptions} isInvalid />
</CanvasFixedHeight>
)
}

export const Disabled: Story = {
render: () => (
<CanvasFixedHeight height={250}>
<SelectAdvanced initialOptions={exampleOptions} isDisabled />
<SelectAdvanced options={exampleOptions} isDisabled />
</CanvasFixedHeight>
)
}

export const WithDifferentSelectWord: Story = {
render: () => (
<CanvasFixedHeight height={250}>
<SelectAdvanced initialOptions={exampleOptions} locales={{ select: 'Selezionare...' }} />
<SelectAdvanced options={exampleOptions} locales={{ select: 'Selezionare...' }} />
</CanvasFixedHeight>
)
}

const SimulateChangeOfAvailableOptions = () => {
const [availableOptions, setAvailableOptions] = useState(['Tag 1', 'Tag 2', 'Tag 3'])

return (
<>
<Button
onClick={() =>
setAvailableOptions((current) => [...current, `Tag ${Date.now().toString().slice(-4)}`])
}>
Add one more option
</Button>
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
<SelectAdvanced
isMultiple
options={availableOptions}
defaultValue={[availableOptions[0], availableOptions[2]]}
onChange={(selected) => console.log(selected)}
/>
<SelectAdvanced
isMultiple
options={availableOptions}
defaultValue={[availableOptions[1]]}
onChange={(selected) => console.log(selected)}
/>
</div>
</>
)
}

export const ChangeOfAvailablesOptionsCase: Story = {
render: () => (
<CanvasFixedHeight height={250}>
<SimulateChangeOfAvailableOptions />
</CanvasFixedHeight>
)
}

0 comments on commit 09afbbe

Please sign in to comment.