mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 01:18:42 +00:00
UI – Improve UX of label filter dropdown (#15199)
## Addresses #14102 - Enable closing this menu on clicking its header when open - Other small UX and code improvements around this component https://github.com/fleetdm/fleet/assets/61553566/b848b2d1-533f-4aa0-9827-e841d3d840e8 - [x] Changes file added for user-visible changes in `changes/` - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
This commit is contained in:
parent
57df2f250c
commit
3ad60e1041
3 changed files with 61 additions and 45 deletions
2
changes/14102-fix-label-filter-select
Normal file
2
changes/14102-fix-label-filter-select
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
- Fix a bug in which the manage page's label filter selection menu did not close when open and
|
||||
clicked. Added some additional UX improvements around this component.
|
||||
|
|
@ -10,7 +10,6 @@ import CustomLabelGroupHeading from "../CustomLabelGroupHeading";
|
|||
import { PLATFORM_TYPE_ICONS } from "./constants";
|
||||
import { createDropdownOptions, IEmptyOption, IGroupOption } from "./helpers";
|
||||
import CustomDropdownIndicator from "../CustomDropdownIndicator";
|
||||
import CustomValueContainer from "../CustomValueContainer";
|
||||
|
||||
// Extending the react-select module to add custom props we need for our custom
|
||||
// group heading. More info here:
|
||||
|
|
@ -36,7 +35,7 @@ const baseClass = "label-filter-select";
|
|||
* component. You will find focus and blur handlers in this component to help
|
||||
* solve the problem of changing focus between the select dropdown and the
|
||||
* label search input. */
|
||||
const OptionLabel = (data: ILabel | IEmptyOption) => {
|
||||
const formatOptionLabel = (data: ILabel | IEmptyOption) => {
|
||||
const isLabel = "display_text" in data;
|
||||
const isPlatform = isLabel && data.type === "platform";
|
||||
|
||||
|
|
@ -81,9 +80,9 @@ const LabelFilterSelect = ({
|
|||
const [labelQuery, setLabelQuery] = useState("");
|
||||
|
||||
// we need the Select to be a controlled component to enable our label input
|
||||
// to work correctly. shouldOpenMenu now becomes our single source of truth if
|
||||
// to work correctly. menuIsOpen now becomes our single source of truth if
|
||||
// we want the menu to render open or closed.
|
||||
const [shouldOpenMenu, setShouldOpenMenu] = useState(false);
|
||||
const [menuIsOpen, setMenuIsOpen] = useState(false);
|
||||
const isLabelSearchInputFocusedRef = useRef(false);
|
||||
const selectRef = useRef<
|
||||
SelectInstance<ILabel | IEmptyOption, false, IGroupOption>
|
||||
|
|
@ -97,40 +96,47 @@ const LabelFilterSelect = ({
|
|||
const handleChange = (option: ILabel | IEmptyOption | null) => {
|
||||
if (option === null) return;
|
||||
if ("type" in option) {
|
||||
setShouldOpenMenu(false);
|
||||
// typeof option === "ILabel"
|
||||
setLabelQuery("");
|
||||
selectRef.current?.blur();
|
||||
onChange(option);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLabelQueryChange = (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
const toggleMenu = () => {
|
||||
menuIsOpen && selectRef.current?.blur();
|
||||
setMenuIsOpen(!menuIsOpen);
|
||||
};
|
||||
const onChangeLabelQuery = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
// We need to stop the key presses propagation to prevent the dropdown from
|
||||
// picking up keypresses.
|
||||
event.stopPropagation();
|
||||
setLabelQuery(event.target.value);
|
||||
};
|
||||
|
||||
const handleBlurSelect = () => {
|
||||
const onBlur = () => {
|
||||
if (!isLabelSearchInputFocusedRef.current) {
|
||||
isLabelSearchInputFocusedRef.current = false;
|
||||
setShouldOpenMenu(false);
|
||||
setMenuIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFocusSelect = () => {
|
||||
setShouldOpenMenu(true);
|
||||
const onKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
setMenuIsOpen(false);
|
||||
selectRef.current?.blur();
|
||||
} else {
|
||||
setMenuIsOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClickLabelSearchInput = () => {
|
||||
const onClickLabelSearchInput = () => {
|
||||
isLabelSearchInputFocusedRef.current = true;
|
||||
};
|
||||
|
||||
const handleBlurLabelSearchInput = () => {
|
||||
const onBlurLabelSearchInput = () => {
|
||||
isLabelSearchInputFocusedRef.current = false;
|
||||
setShouldOpenMenu(false);
|
||||
setMenuIsOpen(false);
|
||||
};
|
||||
|
||||
const getOptionLabel = (option: ILabel | IEmptyOption) => {
|
||||
|
|
@ -161,35 +167,40 @@ const LabelFilterSelect = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<Select<ILabel | IEmptyOption, false, IGroupOption>
|
||||
ref={selectRef}
|
||||
name="input-filter-select"
|
||||
options={options}
|
||||
className={classes}
|
||||
classNamePrefix={baseClass}
|
||||
defaultMenuIsOpen={false}
|
||||
placeholder={"Filter by platform or label"}
|
||||
formatOptionLabel={OptionLabel}
|
||||
menuIsOpen={shouldOpenMenu}
|
||||
value={selectedLabel}
|
||||
isSearchable={false}
|
||||
getOptionLabel={getOptionLabel}
|
||||
getOptionValue={getOptionValue}
|
||||
components={{
|
||||
GroupHeading: CustomLabelGroupHeading,
|
||||
DropdownIndicator: CustomDropdownIndicator,
|
||||
ValueContainer,
|
||||
}}
|
||||
labelQuery={labelQuery}
|
||||
canAddNewLabels={canAddNewLabels}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlurSelect}
|
||||
onFocus={handleFocusSelect}
|
||||
onAddLabel={onAddLabel}
|
||||
onChangeLabelQuery={handleLabelQueryChange}
|
||||
onClickLabelSearchInput={handleClickLabelSearchInput}
|
||||
onBlurLabelSearchInput={handleBlurLabelSearchInput}
|
||||
/>
|
||||
<div onClick={toggleMenu}>
|
||||
<Select<ILabel | IEmptyOption, false, IGroupOption>
|
||||
ref={selectRef}
|
||||
name="input-filter-select"
|
||||
className={classes}
|
||||
classNamePrefix={baseClass}
|
||||
defaultMenuIsOpen={false}
|
||||
placeholder="Filter by platform or label"
|
||||
value={selectedLabel}
|
||||
isSearchable={false}
|
||||
components={{
|
||||
GroupHeading: CustomLabelGroupHeading,
|
||||
DropdownIndicator: CustomDropdownIndicator,
|
||||
ValueContainer,
|
||||
}}
|
||||
onChange={handleChange}
|
||||
closeMenuOnSelect
|
||||
{...{
|
||||
menuIsOpen,
|
||||
options,
|
||||
formatOptionLabel,
|
||||
getOptionLabel,
|
||||
getOptionValue,
|
||||
labelQuery,
|
||||
canAddNewLabels,
|
||||
onKeyDown,
|
||||
onAddLabel,
|
||||
onBlur,
|
||||
onChangeLabelQuery,
|
||||
onClickLabelSearchInput,
|
||||
onBlurLabelSearchInput,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@
|
|||
box-shadow: none;
|
||||
border: 1px solid $core-vibrant-blue;
|
||||
}
|
||||
&--is-focused,
|
||||
&--menu-is-open {
|
||||
.custom-dropdown-indicator {
|
||||
svg {
|
||||
|
|
@ -101,6 +100,10 @@
|
|||
.label-filter-select__option {
|
||||
padding: 10px 1rem;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&--is-selected,
|
||||
&:active {
|
||||
background-color: $ui-vibrant-blue-25;
|
||||
|
|
|
|||
Loading…
Reference in a new issue