@grafana/ui
Version:
Grafana Components Library
196 lines (193 loc) • 7.15 kB
JavaScript
import { jsxs, jsx } from 'react/jsx-runtime';
import { css } from '@emotion/css';
import { isEqual } from 'lodash';
import { PureComponent, createRef } from 'react';
import ReactDOM from 'react-dom';
import { FixedSizeList } from 'react-window';
import { ThemeContext } from '@grafana/data';
import { CompletionItemKind } from '../../types/completion.mjs';
import { flattenGroupItems, calculateLongestLabel, calculateListSizes } from '../../utils/typeahead.mjs';
import { TypeaheadInfo } from './TypeaheadInfo.mjs';
import { TypeaheadItem } from './TypeaheadItem.mjs';
const modulo = (a, n) => a - n * Math.floor(a / n);
class Typeahead extends PureComponent {
constructor() {
super(...arguments);
this.listRef = createRef();
this.state = {
hoveredItem: null,
typeaheadIndex: null,
allItems: [],
listWidth: -1,
listHeight: -1,
itemHeight: -1
};
this.componentDidMount = () => {
if (this.props.menuRef) {
this.props.menuRef(this);
}
document.addEventListener("selectionchange", this.handleSelectionChange);
const allItems = flattenGroupItems(this.props.groupedItems);
const longestLabel = calculateLongestLabel(allItems);
const { listWidth, listHeight, itemHeight } = calculateListSizes(this.context, allItems, longestLabel);
this.setState({
listWidth,
listHeight,
itemHeight,
allItems
});
};
this.componentWillUnmount = () => {
document.removeEventListener("selectionchange", this.handleSelectionChange);
};
this.handleSelectionChange = () => {
this.forceUpdate();
};
this.componentDidUpdate = (prevProps, prevState) => {
if (this.state.typeaheadIndex !== null && prevState.typeaheadIndex !== this.state.typeaheadIndex && this.listRef && this.listRef.current) {
if (this.state.typeaheadIndex === 1) {
this.listRef.current.scrollToItem(0);
return;
}
this.listRef.current.scrollToItem(this.state.typeaheadIndex);
}
if (isEqual(prevProps.groupedItems, this.props.groupedItems) === false) {
const allItems = flattenGroupItems(this.props.groupedItems);
const longestLabel = calculateLongestLabel(allItems);
const { listWidth, listHeight, itemHeight } = calculateListSizes(this.context, allItems, longestLabel);
this.setState({ listWidth, listHeight, itemHeight, allItems, typeaheadIndex: null });
}
};
this.onMouseEnter = (index) => {
this.setState({
hoveredItem: index
});
};
this.onMouseLeave = () => {
this.setState({
hoveredItem: null
});
};
this.moveMenuIndex = (moveAmount) => {
const itemCount = this.state.allItems.length;
if (itemCount) {
const typeaheadIndex = this.state.typeaheadIndex || 0;
let newTypeaheadIndex = modulo(typeaheadIndex + moveAmount, itemCount);
if (this.state.allItems[newTypeaheadIndex].kind === CompletionItemKind.GroupTitle) {
newTypeaheadIndex = modulo(newTypeaheadIndex + moveAmount, itemCount);
}
this.setState({
typeaheadIndex: newTypeaheadIndex
});
return;
}
};
this.insertSuggestion = () => {
if (this.props.onSelectSuggestion && this.state.typeaheadIndex !== null) {
this.props.onSelectSuggestion(this.state.allItems[this.state.typeaheadIndex]);
}
};
}
get menuPosition() {
if (!window.getSelection) {
return "";
}
const selection = window.getSelection();
const node = selection && selection.anchorNode;
if (node && node.parentElement) {
const rect = node.parentElement.getBoundingClientRect();
const scrollX = window.scrollX;
const scrollY = window.scrollY;
return `position: absolute; display: flex; top: ${rect.top + scrollY + rect.height + 6}px; left: ${rect.left + scrollX - 2}px`;
}
return "";
}
render() {
const { prefix, isOpen = false, origin } = this.props;
const { allItems, listWidth, listHeight, itemHeight, hoveredItem, typeaheadIndex } = this.state;
const styles = getStyles(this.context);
const showDocumentation = hoveredItem || typeaheadIndex;
const documentationItem = allItems[hoveredItem ? hoveredItem : typeaheadIndex || 0];
return /* @__PURE__ */ jsxs(Portal, { origin, isOpen, style: this.menuPosition, children: [
/* @__PURE__ */ jsx("ul", { role: "menu", className: styles.typeahead, "data-testid": "typeahead", children: /* @__PURE__ */ jsx(
FixedSizeList,
{
ref: this.listRef,
itemCount: allItems.length,
itemSize: itemHeight,
itemKey: (index) => {
const item = allItems && allItems[index];
const key = item ? `${index}-${item.label}` : `${index}`;
return key;
},
width: listWidth,
height: listHeight,
children: ({ index, style }) => {
const item = allItems && allItems[index];
if (!item) {
return null;
}
return /* @__PURE__ */ jsx(
TypeaheadItem,
{
onClickItem: () => this.props.onSelectSuggestion ? this.props.onSelectSuggestion(item) : {},
isSelected: typeaheadIndex === null ? false : allItems[typeaheadIndex] === item,
item,
prefix,
style,
onMouseEnter: () => this.onMouseEnter(index),
onMouseLeave: this.onMouseLeave
}
);
}
}
) }),
showDocumentation && /* @__PURE__ */ jsx(TypeaheadInfo, { height: listHeight, item: documentationItem })
] });
}
}
Typeahead.contextType = ThemeContext;
class Portal extends PureComponent {
constructor(props) {
super(props);
const { index = 0, origin = "query", style } = props;
this.node = document.createElement("div");
this.node.setAttribute("style", style);
this.node.classList.add(`slate-typeahead-${origin}-${index}`);
document.body.appendChild(this.node);
}
componentWillUnmount() {
document.body.removeChild(this.node);
}
render() {
if (this.props.isOpen) {
this.node.setAttribute("style", this.props.style);
this.node.classList.add(`slate-typeahead--open`);
return ReactDOM.createPortal(this.props.children, this.node);
} else {
this.node.classList.remove(`slate-typeahead--open`);
}
return null;
}
}
const getStyles = (theme) => ({
typeahead: css({
position: "relative",
zIndex: theme.zIndex.typeahead,
borderRadius: theme.shape.radius.default,
border: `1px solid ${theme.components.panel.borderColor}`,
maxHeight: "66vh",
overflowY: "scroll",
overflowX: "hidden",
outline: "none",
listStyle: "none",
background: theme.components.panel.background,
color: theme.colors.text.primary,
boxShadow: theme.shadows.z2,
strong: {
color: theme.v1.palette.yellow
}
})
});
export { Typeahead };
//# sourceMappingURL=Typeahead.mjs.map