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:
Jacob Shandling 2023-11-20 12:42:55 -08:00 committed by GitHub
parent 57df2f250c
commit 3ad60e1041
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 61 additions and 45 deletions

View 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.

View file

@ -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>
);
};

View file

@ -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;