UNPKG

adwaita-web

Version:

A GTK inspired toolkit designed to build awesome web apps

257 lines (256 loc) 7.24 kB
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 };