adwaita-web
Version:
A GTK inspired toolkit designed to build awesome web apps
257 lines (256 loc) • 7.24 kB
JavaScript
import cx from "clsx";
import React from "react";
import { PanDown } from "../icons";
import { Button } from "./Button";
import { Input } from "./Input";
import { Label } from "./Label";
import { Menu } from "./Menu";
import { MenuButton } from "./MenuButton";
import { MenuItem } from "./MenuItem";
import { Popover } from "./Popover";
class Dropdown extends React.Component {
static defaultProps = {
size: "medium",
align: "right",
disabled: false,
options: []
};
static getDerivedStateFromProps(props) {
if ("open" in props)
return { open: props.open };
return null;
}
domNode;
trigger;
lastValue;
lastOption;
lastOptions;
lastRenderedOptions;
lastRenderedOptionsArgs;
state;
constructor(props) {
super(props);
this.domNode = document.createElement("div");
document.body.append(this.domNode);
this.lastRenderedOptions = [];
this.lastRenderedOptionsArgs = [];
this.state = {
value: void 0,
selectedOption: void 0,
open: false,
position: { top: 0, left: 0 },
inputValue: "",
previousInputValue: ""
};
}
componentWillUnmount() {
document.body.removeChild(this.domNode);
}
onInputBlur = () => {
setTimeout(this.close, 200);
};
onInputChange = (inputValue) => {
this.setState({ inputValue });
};
onInputAccept = () => {
const options = this.getRenderedOptions();
if (options.length === 0)
return;
const option = options[0];
this.select(option);
if (this.trigger) {
this.trigger.querySelector("input")?.blur();
}
};
onToggle = () => {
if (this.state.open)
this.close();
else
this.open();
};
open = () => {
if (this.props.onOpen)
this.props.onOpen();
else
this.setState({ open: true });
};
close = () => {
if (this.props.onClose)
this.props.onClose();
else
this.setState({ open: false });
if (this.props.input && this.state.inputValue) {
this.setState({ inputValue: "", previousInputValue: this.state.inputValue });
}
};
isOpen = () => {
return "open" in this.props ? this.props.open : this.state.open;
};
isControlled = () => {
return "value" in this.props;
};
select = (option) => {
if (this.isControlled()) {
if (this.props.onChange)
this.props.onChange(option ? option.value : void 0);
} else {
this.setState({ value: option?.value, selectedOption: option });
}
this.close();
};
getValue() {
if (this.isControlled())
return this.props.value;
return this.state.value;
}
getRenderedOptions() {
const { options: optionsProp, filterKey, filter } = this.props;
const open = this.isOpen();
const { inputValue, previousInputValue } = this.state;
const value = open ? inputValue : previousInputValue;
const args = [optionsProp, value, filterKey, filter];
if (args.every((a, i) => this.lastRenderedOptionsArgs[i] === a)) {
return this.lastRenderedOptions ?? [];
}
let options;
if (!value) {
options = optionsProp;
} else {
const needle = value.toLowerCase();
options = optionsProp?.filter((o) => {
const optionValue = filterKey ? o.data[filterKey] : filter ? filter(o) : typeof o.label === "string" ? o.label : o.value;
return String(optionValue).toLowerCase().includes(needle);
}) ?? [];
}
this.lastRenderedOptionsArgs = args;
this.lastRenderedOptions = options;
return options ?? [];
}
getSelectedOption() {
const value = this.getValue();
if (value === this.lastValue && this.props.options === this.lastOptions)
return this.lastOption;
const option = this.props.options.find((o) => o.value === value);
if (option) {
this.lastValue = value;
this.lastOption = option;
this.lastOptions = this.props.options;
return option;
}
return void 0;
}
render() {
const {
className,
triggerClassName,
size,
align,
placeholder,
label: labelValue,
value: _value,
options: _options,
disabled,
loading,
open: _open,
allowClear,
input,
filterKey,
filter,
onClose,
onOpen,
onChange: _onChange,
...rest
} = this.props;
const open = this.isOpen();
const value = this.getValue();
const label = labelValue || this.getSelectedOption()?.label || placeholder || value;
let trigger;
if (!input) {
const buttonClassName = cx("Dropdown Box horizontal", className, size, {
open,
hover: open
});
trigger = /* @__PURE__ */ React.createElement(Button, {
size,
className: buttonClassName,
loading,
disabled,
onClick: this.onToggle,
ref: (ref) => ref && (this.trigger = ref),
...rest
}, /* @__PURE__ */ React.createElement(Label, {
align: "left",
className: "Box__fill"
}, label), /* @__PURE__ */ React.createElement(PanDown, {
containerProps: { className: "Dropdown__arrow" }
}));
} else {
const { inputValue } = this.state;
const inputClassName = cx("Dropdown", className, size, {
open,
hover: open
});
trigger = /* @__PURE__ */ React.createElement(Input, {
className: inputClassName,
loading,
disabled,
value: inputValue,
onFocus: this.open,
onBlur: this.onInputBlur,
onChange: this.onInputChange,
onAccept: this.onInputAccept,
iconAfter: PanDown,
ref: (ref) => ref && (this.trigger = ref),
...rest
}, wrapLabel(label));
}
const options = this.getRenderedOptions();
const actualChildren = options.map((o) => /* @__PURE__ */ React.createElement(MenuButton, {
key: o.value.toString(),
selected: String(o.value) === String(value),
onClick: () => this.select(o)
}, o.label));
if (actualChildren.length === 0)
actualChildren.push(/* @__PURE__ */ React.createElement(MenuItem, {
key: "empty_item"
}, /* @__PURE__ */ React.createElement(Label, {
muted: true,
italic: true
}, "No option found")));
if (allowClear) {
actualChildren.unshift(/* @__PURE__ */ React.createElement(MenuButton, {
key: "null_item",
onClick: () => this.select(null)
}, /* @__PURE__ */ React.createElement(Label, {
muted: true,
italic: true
}, "None")));
}
const popoverClassName = cx("Dropdown__menu", size);
const menu = /* @__PURE__ */ React.createElement(Menu, {
className: popoverClassName
}, actualChildren);
return /* @__PURE__ */ React.createElement(Popover, {
className: "Dropdown__menu",
method: "click-controlled",
width: "trigger",
arrow: false,
open,
content: menu,
onClose: this.close,
shouldUpdatePlacement: open
}, trigger);
}
}
function wrapLabel(label) {
if (typeof label !== "string")
return label;
return /* @__PURE__ */ React.createElement(Label, {
align: "left",
fill: "width",
ellipsis: true
}, label);
}
export {
Dropdown
};