|
@@ -1,7 +1,9 @@
|
|
-import { ChangeEvent, FC, ReactNode, useRef } from 'react'
|
|
|
|
|
|
+import { ChangeEvent, FC, ReactNode, useEffect, useRef, useState } from 'react'
|
|
|
|
+import { useInView } from 'react-intersection-observer'
|
|
|
|
|
|
import { Counter, StyledButton } from '@/components/FilterButton/FilterButton.styles'
|
|
import { Counter, StyledButton } from '@/components/FilterButton/FilterButton.styles'
|
|
import { CheckboxGroup } from '@/components/_inputs/CheckboxGroup'
|
|
import { CheckboxGroup } from '@/components/_inputs/CheckboxGroup'
|
|
|
|
+import { PriceRangeInput } from '@/components/_inputs/PriceRangeInput'
|
|
import { DialogPopover } from '@/components/_overlays/DialogPopover'
|
|
import { DialogPopover } from '@/components/_overlays/DialogPopover'
|
|
|
|
|
|
import { RadioButtonGroup } from '../_inputs/RadioButtonGroup'
|
|
import { RadioButtonGroup } from '../_inputs/RadioButtonGroup'
|
|
@@ -13,85 +15,161 @@ export type FilterButtonOption = {
|
|
applied: boolean
|
|
applied: boolean
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+export type FilterRange = {
|
|
|
|
+ min?: number
|
|
|
|
+ max?: number
|
|
|
|
+ appliedMin?: number
|
|
|
|
+ appliedMax?: number
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+export const isFilterRange = (value: FilterRange | FilterButtonOption[]): value is FilterRange => 'min' in value
|
|
|
|
+
|
|
export type FilterButtonProps = {
|
|
export type FilterButtonProps = {
|
|
name: string
|
|
name: string
|
|
- type: 'checkbox' | 'radio'
|
|
|
|
label?: string
|
|
label?: string
|
|
icon?: ReactNode
|
|
icon?: ReactNode
|
|
className?: string
|
|
className?: string
|
|
- onChange: (selectedOptions: FilterButtonOption[]) => void
|
|
|
|
|
|
+ type: 'checkbox' | 'radio' | 'range'
|
|
|
|
+ onChange: (value: FilterRange | FilterButtonOption[]) => void
|
|
|
|
+ range?: FilterRange
|
|
options?: FilterButtonOption[]
|
|
options?: FilterButtonOption[]
|
|
}
|
|
}
|
|
|
|
|
|
export type SectionFilter = Omit<FilterButtonProps, 'onChange'>
|
|
export type SectionFilter = Omit<FilterButtonProps, 'onChange'>
|
|
|
|
|
|
-export const FilterButton: FC<FilterButtonProps> = ({ type, name, onChange, className, icon, label, options = [] }) => {
|
|
|
|
- const counter = options.filter((option) => option.applied)?.length
|
|
|
|
|
|
+export const FilterButton: FC<FilterButtonProps> = (props) => {
|
|
const triggerRef = useRef<HTMLButtonElement>(null)
|
|
const triggerRef = useRef<HTMLButtonElement>(null)
|
|
|
|
+ const firstRangeInput = useRef<HTMLInputElement | null>(null)
|
|
|
|
+ const [shouldFocus, setShouldFocus] = useState(false)
|
|
|
|
+ const { ref, inView } = useInView()
|
|
|
|
|
|
- const handleApply = () => {
|
|
|
|
- onChange(options.map((option) => ({ ...option, applied: option.selected })))
|
|
|
|
- triggerRef.current?.click()
|
|
|
|
- }
|
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
+ if (inView && shouldFocus) {
|
|
|
|
+ firstRangeInput.current?.focus()
|
|
|
|
+ setShouldFocus(false)
|
|
|
|
+ }
|
|
|
|
+ }, [inView, shouldFocus])
|
|
|
|
|
|
- const handleCheckboxSelection = (num: number) => {
|
|
|
|
- const selected = options.map((option, idx) => {
|
|
|
|
- if (num === idx) {
|
|
|
|
- return { ...option, selected: !option.selected }
|
|
|
|
- }
|
|
|
|
- return option
|
|
|
|
- })
|
|
|
|
- onChange(selected)
|
|
|
|
- }
|
|
|
|
|
|
+ if (props.type === 'checkbox' || props.type === 'radio') {
|
|
|
|
+ const { type, name, onChange, className, icon, label, options = [] } = props
|
|
|
|
|
|
- const handleRadioButtonClick = (e: ChangeEvent<Omit<HTMLInputElement, 'value'> & { value: string | boolean }>) => {
|
|
|
|
- const optionIdx = options.findIndex((option) => option.value === e.currentTarget.value)
|
|
|
|
- const selected = options.map((option, idx) => ({ ...option, selected: optionIdx === idx }))
|
|
|
|
- onChange(selected)
|
|
|
|
- }
|
|
|
|
|
|
+ const counter = options.filter((option) => option.applied)?.length
|
|
|
|
+
|
|
|
|
+ const handleApply = () => {
|
|
|
|
+ onChange(options.map((option) => ({ ...option, applied: option.selected })))
|
|
|
|
+ triggerRef.current?.click()
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ const handleCheckboxSelection = (num: number) => {
|
|
|
|
+ const selected = options.map((option, idx) => {
|
|
|
|
+ if (num === idx) {
|
|
|
|
+ return { ...option, selected: !option.selected }
|
|
|
|
+ }
|
|
|
|
+ return option
|
|
|
|
+ })
|
|
|
|
+ onChange(selected)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ const handleRadioButtonClick = (e: ChangeEvent<Omit<HTMLInputElement, 'value'> & { value: string | boolean }>) => {
|
|
|
|
+ const optionIdx = options.findIndex((option) => option.value === e.currentTarget.value)
|
|
|
|
+ const selected = options.map((option, idx) => ({ ...option, selected: optionIdx === idx }))
|
|
|
|
+ onChange(selected)
|
|
|
|
+ }
|
|
|
|
|
|
- const handleClear = () => {
|
|
|
|
- onChange(options.map((option) => ({ ...option, selected: false, applied: false })))
|
|
|
|
- triggerRef.current?.click()
|
|
|
|
|
|
+ const handleClear = () => {
|
|
|
|
+ onChange(options.map((option) => ({ ...option, selected: false, applied: false })))
|
|
|
|
+ triggerRef.current?.click()
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return (
|
|
|
|
+ <DialogPopover
|
|
|
|
+ className={className}
|
|
|
|
+ flipEnabled
|
|
|
|
+ appendTo={document.body}
|
|
|
|
+ trigger={
|
|
|
|
+ <StyledButton
|
|
|
|
+ ref={triggerRef}
|
|
|
|
+ icon={counter ? <Counter>{counter}</Counter> : icon}
|
|
|
|
+ iconPlacement="right"
|
|
|
|
+ variant="secondary"
|
|
|
|
+ >
|
|
|
|
+ {label}
|
|
|
|
+ </StyledButton>
|
|
|
|
+ }
|
|
|
|
+ primaryButton={{ text: 'Apply', onClick: handleApply }}
|
|
|
|
+ secondaryButton={{
|
|
|
|
+ text: 'Clear',
|
|
|
|
+ onClick: handleClear,
|
|
|
|
+ }}
|
|
|
|
+ >
|
|
|
|
+ {type === 'checkbox' && (
|
|
|
|
+ <CheckboxGroup
|
|
|
|
+ name={name}
|
|
|
|
+ options={options.map((option) => ({ ...option, value: option.selected }))}
|
|
|
|
+ checkedIds={options.map((option, index) => (option.selected ? index : -1)).filter((index) => index !== -1)}
|
|
|
|
+ onChange={handleCheckboxSelection}
|
|
|
|
+ />
|
|
|
|
+ )}
|
|
|
|
+ {type === 'radio' && (
|
|
|
|
+ <RadioButtonGroup
|
|
|
|
+ name={name}
|
|
|
|
+ options={options}
|
|
|
|
+ value={options.find((option) => option.selected)?.value}
|
|
|
|
+ onChange={handleRadioButtonClick}
|
|
|
|
+ />
|
|
|
|
+ )}
|
|
|
|
+ </DialogPopover>
|
|
|
|
+ )
|
|
}
|
|
}
|
|
|
|
|
|
- return (
|
|
|
|
- <DialogPopover
|
|
|
|
- className={className}
|
|
|
|
- flipEnabled
|
|
|
|
- appendTo={document.body}
|
|
|
|
- trigger={
|
|
|
|
- <StyledButton
|
|
|
|
- ref={triggerRef}
|
|
|
|
- icon={counter ? <Counter>{counter}</Counter> : icon}
|
|
|
|
- iconPlacement="right"
|
|
|
|
- variant="secondary"
|
|
|
|
- >
|
|
|
|
- {label}
|
|
|
|
- </StyledButton>
|
|
|
|
- }
|
|
|
|
- primaryButton={{ text: 'Apply', onClick: handleApply }}
|
|
|
|
- secondaryButton={{
|
|
|
|
- text: 'Clear',
|
|
|
|
- onClick: handleClear,
|
|
|
|
- }}
|
|
|
|
- >
|
|
|
|
- {type === 'checkbox' && (
|
|
|
|
- <CheckboxGroup
|
|
|
|
- name={name}
|
|
|
|
- options={options.map((option) => ({ ...option, value: option.selected }))}
|
|
|
|
- checkedIds={options.map((option, index) => (option.selected ? index : -1)).filter((index) => index !== -1)}
|
|
|
|
- onChange={handleCheckboxSelection}
|
|
|
|
- />
|
|
|
|
- )}
|
|
|
|
- {type === 'radio' && (
|
|
|
|
- <RadioButtonGroup
|
|
|
|
- name={name}
|
|
|
|
- options={options}
|
|
|
|
- value={options.find((option) => option.selected)?.value}
|
|
|
|
- onChange={handleRadioButtonClick}
|
|
|
|
|
|
+ if (props.type === 'range') {
|
|
|
|
+ const { onChange, className, icon, label, range } = props
|
|
|
|
+
|
|
|
|
+ const isApplied = Boolean(range?.appliedMax || range?.appliedMin)
|
|
|
|
+
|
|
|
|
+ const handleApply = () => {
|
|
|
|
+ onChange({ ...range, appliedMin: range?.min, appliedMax: range?.max })
|
|
|
|
+ triggerRef.current?.click()
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ const handleClear = () => {
|
|
|
|
+ onChange({ min: undefined, max: undefined, appliedMin: undefined, appliedMax: undefined })
|
|
|
|
+ triggerRef.current?.click()
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return (
|
|
|
|
+ <DialogPopover
|
|
|
|
+ className={className}
|
|
|
|
+ flipEnabled
|
|
|
|
+ onShow={() => setShouldFocus(true)}
|
|
|
|
+ appendTo={document.body}
|
|
|
|
+ trigger={
|
|
|
|
+ <StyledButton
|
|
|
|
+ ref={triggerRef}
|
|
|
|
+ icon={isApplied ? <Counter>1</Counter> : icon}
|
|
|
|
+ iconPlacement="right"
|
|
|
|
+ variant="secondary"
|
|
|
|
+ >
|
|
|
|
+ {label}
|
|
|
|
+ </StyledButton>
|
|
|
|
+ }
|
|
|
|
+ primaryButton={{ text: 'Apply', onClick: handleApply }}
|
|
|
|
+ secondaryButton={{
|
|
|
|
+ text: 'Clear',
|
|
|
|
+ onClick: handleClear,
|
|
|
|
+ }}
|
|
|
|
+ >
|
|
|
|
+ <PriceRangeInput
|
|
|
|
+ ref={(inputRef) => {
|
|
|
|
+ ref(inputRef)
|
|
|
|
+ firstRangeInput.current = inputRef
|
|
|
|
+ }}
|
|
|
|
+ value={range}
|
|
|
|
+ onChange={(value) => onChange({ ...range, ...value })}
|
|
/>
|
|
/>
|
|
- )}
|
|
|
|
- </DialogPopover>
|
|
|
|
- )
|
|
|
|
|
|
+ </DialogPopover>
|
|
|
|
+ )
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return null
|
|
}
|
|
}
|