From 51b53b334918d15cf95908ab666c6d90c3963d08 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Tue, 27 Jul 2021 08:53:34 -0400 Subject: [PATCH] Improve 's UX (#3490) - Display previously selected namespaces at the top of the option list when first opened - Don't resort after every selection - Automatically close the option list if no longer in multi-select mode and have toggled at least one namespace Signed-off-by: Sebastian Malton --- .../+namespaces/namespace-select-filter.tsx | 95 +++++++++++++++---- .../+namespaces/namespace-select.tsx | 7 +- src/renderer/components/input/input.tsx | 4 +- 3 files changed, 84 insertions(+), 22 deletions(-) diff --git a/src/renderer/components/+namespaces/namespace-select-filter.tsx b/src/renderer/components/+namespaces/namespace-select-filter.tsx index 514749ed44..07b492ebd8 100644 --- a/src/renderer/components/+namespaces/namespace-select-filter.tsx +++ b/src/renderer/components/+namespaces/namespace-select-filter.tsx @@ -22,16 +22,16 @@ import "./namespace-select-filter.scss"; import React from "react"; -import { observer } from "mobx-react"; +import { disposeOnUnmount, observer } from "mobx-react"; import { components, PlaceholderProps } from "react-select"; +import { action, computed, makeObservable, observable, reaction } from "mobx"; import { Icon } from "../icon"; import { NamespaceSelect } from "./namespace-select"; import { namespaceStore } from "./namespace.store"; import type { SelectOption, SelectProps } from "../select"; -import { isLinux, isMac, isWindows } from "../../../common/vars"; -import { observable } from "mobx"; +import { isMac } from "../../../common/vars"; const Placeholder = observer((props: PlaceholderProps) => { const getPlaceholder = (): React.ReactNode => { @@ -60,6 +60,41 @@ export class NamespaceSelectFilter extends React.Component { static isMultiSelection = observable.box(false); static isMenuOpen = observable.box(false); + private selected = observable.set(); + private didToggle = false; + + constructor(props: SelectProps) { + super(props); + makeObservable(this); + } + + @computed get isMultiSelection() { + return NamespaceSelectFilter.isMultiSelection.get(); + } + + set isMultiSelection(val: boolean) { + NamespaceSelectFilter.isMultiSelection.set(val); + } + + @computed get isMenuOpen() { + return NamespaceSelectFilter.isMenuOpen.get(); + } + + set isMenuOpen(val: boolean) { + NamespaceSelectFilter.isMenuOpen.set(val); + } + + componentDidMount() { + disposeOnUnmount(this, [ + reaction(() => this.isMenuOpen, newVal => { + if (newVal) { // rising edge of selection + this.selected.replace(namespaceStore.selectedNamespaces); + this.didToggle = false; + } + }), + ]); + } + formatOptionLabel({ value: namespace, label }: SelectOption) { if (namespace) { const isSelected = namespaceStore.hasContext(namespace); @@ -76,37 +111,58 @@ export class NamespaceSelectFilter extends React.Component { return label; } - onChange([{ value: namespace }]: SelectOption[]) { - if (NamespaceSelectFilter.isMultiSelection.get() && namespace) { - namespaceStore.toggleContext(namespace); - } else if (!NamespaceSelectFilter.isMultiSelection.get() && namespace) { - namespaceStore.toggleSingle(namespace); + @action + onChange = ([{ value: namespace }]: SelectOption[]) => { + if (namespace) { + if (this.isMultiSelection) { + this.didToggle = true; + namespaceStore.toggleContext(namespace); + } else { + namespaceStore.toggleSingle(namespace); + } } else { namespaceStore.toggleAll(true); // "All namespaces" clicked } + }; + + private isSelectionKey(e: React.KeyboardEvent): boolean { + if (isMac) { + return e.key === "Meta"; + } + + return e.key === "Control"; // windows or linux } - onKeyDown = (e: React.KeyboardEvent) => { - if (isMac && e.metaKey || (isWindows || isLinux) && e.ctrlKey) { - NamespaceSelectFilter.isMultiSelection.set(true); + @action + onKeyDown = (e: React.KeyboardEvent) => { + if (this.isSelectionKey(e)) { + this.isMultiSelection = true; } }; - onKeyUp = (e: React.KeyboardEvent) => { - if (isMac && e.key === "Meta" || (isWindows || isLinux) && e.key === "Control") { - NamespaceSelectFilter.isMultiSelection.set(false); + @action + onKeyUp = (e: React.KeyboardEvent) => { + if (this.isSelectionKey(e)) { + this.isMultiSelection = false; + } + + if (!this.isMultiSelection && this.didToggle) { + this.isMenuOpen = false; } }; + @action onClick = () => { - if (!NamespaceSelectFilter.isMultiSelection.get()) { - NamespaceSelectFilter.isMenuOpen.set(!NamespaceSelectFilter.isMenuOpen.get()); + if (!this.isMenuOpen) { + this.isMenuOpen = true; + } else if (!this.isMultiSelection) { + this.isMenuOpen = !this.isMenuOpen; } }; reset = () => { - NamespaceSelectFilter.isMultiSelection.set(false); - NamespaceSelectFilter.isMenuOpen.set(false); + this.isMultiSelection = false; + this.isMenuOpen = false; }; render() { @@ -114,7 +170,7 @@ export class NamespaceSelectFilter extends React.Component {
{ onBlur={this.reset} formatOptionLabel={this.formatOptionLabel} className="NamespaceSelectFilter" + sort={(left, right) => +this.selected.has(right.value) - +this.selected.has(left.value)} />
); diff --git a/src/renderer/components/+namespaces/namespace-select.tsx b/src/renderer/components/+namespaces/namespace-select.tsx index c1fe06c258..3a662ef66c 100644 --- a/src/renderer/components/+namespaces/namespace-select.tsx +++ b/src/renderer/components/+namespaces/namespace-select.tsx @@ -32,6 +32,7 @@ import { kubeWatchApi } from "../../api/kube-watch-api"; interface Props extends SelectProps { showIcons?: boolean; + sort?: (a: SelectOption, b: SelectOption) => number; showAllNamespacesOption?: boolean; // show "All namespaces" option on the top (default: false) customizeOptions?(options: SelectOption[]): SelectOption[]; } @@ -59,9 +60,13 @@ export class NamespaceSelect extends React.Component { } @computed.struct get options(): SelectOption[] { - const { customizeOptions, showAllNamespacesOption } = this.props; + const { customizeOptions, showAllNamespacesOption, sort } = this.props; let options: SelectOption[] = namespaceStore.items.map(ns => ({ value: ns.getName() })); + if (sort) { + options.sort(sort); + } + if (showAllNamespacesOption) { options.unshift({ label: "All Namespaces", value: "" }); } diff --git a/src/renderer/components/input/input.tsx b/src/renderer/components/input/input.tsx index e1d39d9835..e5276ffcb5 100644 --- a/src/renderer/components/input/input.tsx +++ b/src/renderer/components/input/input.tsx @@ -115,8 +115,8 @@ export class Input extends React.Component { } getValue(): string { - const { trim, value, defaultValue = "" } = this.props; - const rawValue = value ?? this.input?.value ?? defaultValue; + const { trim, value, defaultValue } = this.props; + const rawValue = value ?? this.input?.value ?? defaultValue ?? ""; return trim ? rawValue.trim() : rawValue; }