-
Notifications
You must be signed in to change notification settings - Fork 127
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(components): Create TypingFilter component #436
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
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
|
||
}; | ||
}, []); | ||
Check warning on line 96 in packages/components/src/custom/TypingFilter/index.tsx
|
||
|
||
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
|
||
|
||
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> | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nebula-aac typo in name of file
I
is accidentally uppercase