-
Notifications
You must be signed in to change notification settings - Fork 100
Commit
feat(components): Create TypingFilter component
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { TextFieldProps } from '@mui/material/TextField'; | ||
import React from 'react'; | ||
import { TextField } from '../../base/TextField'; | ||
|
||
export type TypingFilterInputProps = { | ||
variant?: string; | ||
} & TextFieldProps; | ||
|
||
export const TypingFilterInput = React.forwardRef(function TypingFilterInput( | ||
props: TypingFilterInputProps, | ||
ref: React.ForwardedRef<HTMLDivElement> | ||
): JSX.Element { | ||
return <TextField ref={ref} {...props} />; | ||
}); | ||
|
||
export default TypingFilterInput; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import React from 'react'; | ||
import { Divider } from '../../base/Divider'; | ||
import { List } from '../../base/List'; | ||
import { ListItem } from '../../base/ListItem'; | ||
import { Typography } from '../../base/Typography'; | ||
import { FilterSchema, FilterStateType, FilteringEvents } from '../../utils/typing.state'; | ||
import { getCurrentFilterAndValue } from '../../utils/typing.utils'; | ||
|
||
interface TypingFiltersType { | ||
filterStateMachine: FilterStateType; | ||
dispatchFilterMachine: React.Dispatch<{ | ||
type: FilteringEvents; | ||
payload: { value: string }; | ||
}>; | ||
filterSchema: FilterSchema; | ||
} | ||
|
||
export function TypingFilters({ | ||
filterStateMachine, | ||
dispatchFilterMachine, | ||
filterSchema | ||
}: TypingFiltersType) { | ||
const selectFilter = (filter: string) => { | ||
dispatchFilterMachine({ | ||
type: FilteringEvents.SELECT_FILTER, | ||
payload: { | ||
value: filter | ||
} | ||
}); | ||
}; | ||
const { filter: currentFilter } = getCurrentFilterAndValue(filterStateMachine); | ||
|
||
const matchingFilters = currentFilter | ||
? Object.values(filterSchema).filter((filter) => filter.value.startsWith(currentFilter)) | ||
: Object.values(filterSchema); | ||
return ( | ||
<List> | ||
{matchingFilters.length == 0 && ( | ||
<ListItem> | ||
<Typography variant="body1">Sorry we dont currently support this filter</Typography> | ||
</ListItem> | ||
)} | ||
{matchingFilters.map((filter) => ( | ||
<React.Fragment key={filter}> | ||
<ListItem disableGutters onClick={() => selectFilter(filter.values)}> | ||
<Typography variant="body1">{filter.values}:</Typography> | ||
<Typography variant="body1">{filter.description}</Typography> | ||
</ListItem> | ||
<Divider light /> | ||
</React.Fragment> | ||
))} | ||
</List> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import React from 'react'; | ||
import { Divider } from '../../base/Divider'; | ||
import { List } from '../../base/List'; | ||
import { ListItem } from '../../base/ListItem'; | ||
import { Typography } from '../../base/Typography'; | ||
import { FilterSchema, FilterStateType, FilteringEvents } from '../../utils/typing.state'; | ||
import { getCurrentFilterAndValue } from '../../utils/typing.utils'; | ||
|
||
interface TypingFilterValueSuggestionsType { | ||
filterStateMachine: FilterStateType; | ||
dispatchFilterMachine: React.Dispatch<{ | ||
type: FilteringEvents; | ||
payload: { value: string }; | ||
}>; | ||
filterSchema: FilterSchema; | ||
} | ||
|
||
export function TypingFilterValueSuggestions({ | ||
filterStateMachine, | ||
dispatchFilterMachine, | ||
filterSchema | ||
}: TypingFilterValueSuggestionsType) { | ||
const selectValue = (value: string) => { | ||
dispatchFilterMachine({ | ||
type: FilteringEvents.SELECT_FILTER, | ||
payload: { | ||
value | ||
} | ||
}); | ||
}; | ||
|
||
const { filter, value } = getCurrentFilterAndValue(filterStateMachine); | ||
const currentFilter = Object.values(filterSchema).find((f) => f.values == filter); | ||
const suggestions = currentFilter?.values?.filter((v) => v.startsWith(value)) ?? []; | ||
|
||
return ( | ||
<List> | ||
{suggestions.length === 0 && ( | ||
<ListItem disableGutters> | ||
<Typography variant="body1">No results available</Typography> | ||
</ListItem> | ||
)} | ||
{suggestions.map((suggestion) => ( | ||
<React.Fragment key={suggestion}> | ||
<ListItem onClick={() => selectValue(suggestion)} disableGutters> | ||
<Typography variant="body1" component="body"> | ||
{suggestion} | ||
</Typography> | ||
</ListItem> | ||
<Divider light /> | ||
</React.Fragment> | ||
))} | ||
</List> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
import { Fade, Popper } from '@mui/material'; | ||
import { ContentFilterIcon, CrossCircleIcon } from 'packages/svg/dist'; | ||
import React from 'react'; | ||
import { ClickAwayListener } from '../../base/ClickAwayListener'; | ||
import { IconButton } from '../../base/IconButton'; | ||
import { InputAdornment } from '../../base/Input'; | ||
import { | ||
FilterSchema, | ||
FilteringEvents, | ||
FilteringState, | ||
filterReducer | ||
} from '../../utils/typing.state'; | ||
import TypingFilterInput from './TypingFIlterInput'; | ||
import { TypingFilters } from './TypingFIlters'; | ||
import { TypingFilterValueSuggestions } from './TypingFilterSuggestions'; | ||
import { getFilters } from '../../utils/typing.utils'; | ||
|
||
interface TypingFilterType { | ||
filterSchema: FilterSchema; | ||
handleFilter: (filters: object) => void; | ||
autoFilter: boolean; | ||
} | ||
|
||
export function TypingFilter({ filterSchema, handleFilter, autoFilter = false }: TypingFilterType) { | ||
const inputFieldRef = React.useRef<HTMLInputElement | null>(null); | ||
const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null); | ||
const isPopperOpen = Boolean(anchorEl); | ||
|
||
const [filterState, dispatch] = React.useReducer(filterReducer, { | ||
state: FilteringState.IDLE, | ||
context: { | ||
value: '', | ||
prevValue: [''] | ||
} | ||
}); | ||
|
||
const handleFilterChange = (e: React.ChangeEvent<HTMLInputElement>) => { | ||
if (!anchorEl) { | ||
setAnchorEl(e.currentTarget); | ||
} | ||
|
||
if (e.target.value === '') { | ||
return dispatch({ | ||
type: FilteringEvents.CLEAR | ||
}); | ||
} | ||
|
||
dispatch({ | ||
type: FilteringEvents.INPUT_CHANGE, | ||
payload: { | ||
value: e.target.value | ||
} | ||
}); | ||
}; | ||
|
||
const handleClear = () => { | ||
dispatch({ | ||
type: FilteringEvents.EXIT | ||
}); | ||
|
||
handleFilter({}); | ||
}; | ||
|
||
const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => { | ||
setAnchorEl(e.currentTarget); | ||
dispatch({ type: FilteringEvents.START }); | ||
}; | ||
|
||
const handleClickAway = (e: MouseEvent | TouchEvent) => { | ||
if (inputFieldRef.current && inputFieldRef.current.contains(e.target as Node)) { | ||
return; | ||
} | ||
|
||
setAnchorEl(null); | ||
}; | ||
|
||
React.useEffect(() => { | ||
if (!inputFieldRef.current) { | ||
return; | ||
} | ||
|
||
const handleKeyDown = (e: KeyboardEvent) => { | ||
if (e.key === 'Enter') { | ||
// Perform nullish check before accessing inputFieldRef.current.value | ||
const inputValue = inputFieldRef.current?.value ?? ''; | ||
handleFilter(getFilters(inputValue, filterSchema)); | ||
setAnchorEl(null); | ||
} | ||
}; | ||
|
||
inputFieldRef.current?.addEventListener('keydown', handleKeyDown); | ||
|
||
return () => { | ||
inputFieldRef.current?.removeEventListener('keydown', handleKeyDown); | ||
Check warning on line 94 in packages/components/src/custom/TypingFilter/index.tsx GitHub Actions / lint (16)
Check warning on line 94 in packages/components/src/custom/TypingFilter/index.tsx GitHub Actions / lint (20)
|
||
}; | ||
}, []); | ||
Check warning on line 96 in packages/components/src/custom/TypingFilter/index.tsx GitHub Actions / lint (16)
Check warning on line 96 in packages/components/src/custom/TypingFilter/index.tsx GitHub Actions / lint (20)
|
||
|
||
React.useEffect(() => { | ||
if (autoFilter && filterState.state === FilteringState.SELECTING_FILTER) { | ||
// Perform nullish check before accessing filterState.context | ||
const filterValue = filterState.context?.value ?? ''; | ||
handleFilter(getFilters(filterValue, filterSchema)); | ||
} | ||
}, [filterState.state]); | ||
Check warning on line 104 in packages/components/src/custom/TypingFilter/index.tsx GitHub Actions / lint (16)
Check warning on line 104 in packages/components/src/custom/TypingFilter/index.tsx GitHub Actions / lint (20)
|
||
|
||
return ( | ||
<React.Fragment> | ||
<TypingFilterInput | ||
ref={inputFieldRef} | ||
variant="outlined" | ||
placeholder="Filter Notifications" | ||
fullWidth | ||
size="small" | ||
value={filterState.context?.value} | ||
onChange={handleFilterChange} | ||
onFocus={handleFocus} | ||
InputProps={{ | ||
startAdornment: ( | ||
<InputAdornment position="start"> | ||
{' '} | ||
<ContentFilterIcon | ||
/* | ||
fill={(theme) => { | ||
theme.palette.iconMain; | ||
}} | ||
*/ | ||
/>{' '} | ||
</InputAdornment> | ||
), | ||
endAdornment: ( | ||
<InputAdornment position="end"> | ||
<IconButton onClick={handleClear}> | ||
{filterState.state !== FilteringState.IDLE && ( | ||
<CrossCircleIcon | ||
/*fill={(theme) => { | ||
theme.palette.iconMain; | ||
}} | ||
*/ | ||
/> | ||
)} | ||
</IconButton> | ||
</InputAdornment> | ||
) | ||
}} | ||
/> | ||
<Popper | ||
open={filterState.state != FilteringState.IDLE && isPopperOpen} | ||
anchorEl={inputFieldRef.current} | ||
placement="bottom-start" | ||
style={{ zIndex: 2000 }} | ||
transition | ||
className="mui-fixed" | ||
> | ||
{({ TransitionProps }) => { | ||
return ( | ||
<Fade {...TransitionProps} timeout={100}> | ||
<ClickAwayListener onKeydown onClickAway={handleClickAway}> | ||
<div | ||
style={{ | ||
width: inputFieldRef.current ? inputFieldRef.current.clientWidth : 0 | ||
}} | ||
> | ||
{filterState.state == FilteringState.SELECTING_FILTER && ( | ||
<TypingFilters | ||
filterStateMachine={filterState} | ||
dispatchFilterMachine={dispatch} | ||
filterSchema={filterSchema} | ||
/> | ||
)} | ||
{filterState.state == FilteringState.SELECTING_VALUE && ( | ||
<TypingFilterValueSuggestions | ||
filterStateMachine={filterState} | ||
dispatchFilterMachine={dispatch} | ||
filterSchema={filterSchema} | ||
/> | ||
)} | ||
</div> | ||
</ClickAwayListener> | ||
</Fade> | ||
); | ||
}} | ||
</Popper> | ||
</React.Fragment> | ||
); | ||
} |