or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

index.mdmulti-select.mdomnibar.mdquery-list.mdselect.mdsuggest.mdutilities.md
tile.json

query-list.mddocs/

Query Management

The QueryList component is a higher-order component that manages query string interactions with item lists. It serves as the foundation for all other select components in the library.

Capabilities

QueryList Component

Higher-order React component for managing query interactions with item lists.

/**
 * QueryList higher-order component for managing query interactions
 * @template T - Type of items in the list
 */
class QueryList<T> extends React.Component<QueryListProps<T>, QueryListState<T>> {
  /** Generic factory method for type inference */
  static ofType<U>(): new (props: QueryListProps<U>) => QueryList<U>;
  
  /** Scroll the active item into view */
  scrollActiveItemIntoView(): void;
  /** Set the query string and optionally reset active item */
  setQuery(query: string, resetActiveItem?: boolean, props?: QueryListProps<T>): void;
  /** Set the currently active item */
  setActiveItem(activeItem: T | CreateNewItem | null): void;
}

interface QueryListProps<T> extends ListItemsProps<T> {
  /** Function to render the query list UI */
  renderer: (listProps: QueryListRendererProps<T>) => React.JSX.Element;
  /** Initial item to be active when component mounts */
  initialActiveItem?: T;
  /** Props for the menu container element */
  menuProps?: React.HTMLAttributes<HTMLUListElement>;
  /** Keyboard event handler for keydown events */
  onKeyDown?: React.KeyboardEventHandler<HTMLElement>;
  /** Keyboard event handler for keyup events */
  onKeyUp?: React.KeyboardEventHandler<HTMLElement>;
  /** Whether the component is disabled */
  disabled?: boolean;
}

interface QueryListState<T> {
  /** Currently active item */
  activeItem: T | CreateNewItem | null;
  /** Items that can be created from current query */
  createNewItem: T | T[] | undefined;
  /** Items after applying filtering predicates */
  filteredItems: T[];
  /** Current query string */
  query: string;
}

interface QueryListRendererProps<T> extends Pick<QueryListState<T>, "activeItem" | "filteredItems" | "query"> {
  /** Function to handle item selection */
  handleItemSelect: (item: T, event?: React.SyntheticEvent<HTMLElement>) => void;
  /** Function to handle paste operations */
  handlePaste: (queries: string[]) => void;
  /** Keyboard handler for key down events */
  handleKeyDown: React.KeyboardEventHandler<HTMLElement>;
  /** Keyboard handler for key up events */
  handleKeyUp: React.KeyboardEventHandler<HTMLElement>;
  /** Handler for query string changes */
  handleQueryChange: React.ChangeEventHandler<HTMLInputElement>;
  /** Rendered list of items */
  itemList: React.ReactNode;
}

Usage Examples:

import React, { useState } from "react";
import { QueryList, ItemRenderer, QueryListRendererProps } from "@blueprintjs/select";
import { InputGroup, Menu } from "@blueprintjs/core";

interface Task {
  id: number;
  title: string;
  completed: boolean;
  priority: "high" | "medium" | "low";
}

const tasks: Task[] = [
  { id: 1, title: "Review pull request", completed: false, priority: "high" },
  { id: 2, title: "Update documentation", completed: false, priority: "medium" },
  { id: 3, title: "Fix bug in login flow", completed: false, priority: "high" },
  { id: 4, title: "Prepare demo presentation", completed: true, priority: "low" },
];

const renderTask: ItemRenderer<Task> = (task, { handleClick, modifiers }) => {
  const priorityColor = {
    high: "#DB3737",
    medium: "#F29D49", 
    low: "#0F9960",
  }[task.priority];

  return (
    <div
      onClick={handleClick}
      style={{
        padding: "8px 12px",
        background: modifiers.active ? "#137CBD" : "transparent",
        color: modifiers.active ? "white" : "black",
        cursor: "pointer",
        opacity: task.completed ? 0.6 : 1,
      }}
    >
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
        <span style={{ textDecoration: task.completed ? "line-through" : "none" }}>
          {task.title}
        </span>
        <div
          style={{
            width: "8px",
            height: "8px",
            borderRadius: "50%",
            backgroundColor: priorityColor,
          }}
        />
      </div>
    </div>
  );
};

const TaskQueryList = () => {
  const [selectedTask, setSelectedTask] = useState<Task | null>(null);

  const renderer = (listProps: QueryListRendererProps<Task>) => {
    const { query, handleQueryChange, handleKeyDown, handleKeyUp, itemList } = listProps;

    return (
      <div style={{ width: "300px", border: "1px solid #ccc", borderRadius: "4px" }}>
        <InputGroup
          placeholder="Search tasks..."
          value={query}
          onChange={handleQueryChange}
          onKeyDown={handleKeyDown}
          onKeyUp={handleKeyUp}
          leftIcon="search"
        />
        <Menu style={{ maxHeight: "200px", overflow: "auto" }}>
          {itemList}
        </Menu>
      </div>
    );
  };

  const taskPredicate = (query: string, task: Task) => {
    return task.title.toLowerCase().includes(query.toLowerCase());
  };

  return (
    <div>
      <QueryList<Task>
        items={tasks}
        itemRenderer={renderTask}
        itemPredicate={taskPredicate}
        onItemSelect={setSelectedTask}
        renderer={renderer}
        noResults={<div style={{ padding: "8px", textAlign: "center" }}>No tasks found</div>}
      />
      
      {selectedTask && (
        <div style={{ marginTop: "16px", padding: "12px", background: "#f5f5f5" }}>
          <strong>Selected:</strong> {selectedTask.title}
          <br />
          <strong>Priority:</strong> {selectedTask.priority}
          <br />
          <strong>Status:</strong> {selectedTask.completed ? "Completed" : "Pending"}
        </div>
      )}
    </div>
  );
};

Custom Rendering

QueryList provides complete control over the UI through the renderer prop.

Usage Examples:

// Grid layout renderer
const GridRenderer = (listProps: QueryListRendererProps<Task>) => {
  const { query, handleQueryChange, handleKeyDown, itemList } = listProps;

  return (
    <div style={{ width: "600px" }}>
      <InputGroup
        placeholder="Search tasks..."
        value={query}
        onChange={handleQueryChange}
        onKeyDown={handleKeyDown}
        large
        leftIcon="search"
      />
      <div
        style={{
          display: "grid",
          gridTemplateColumns: "repeat(auto-fill, minmax(250px, 1fr))",
          gap: "12px",
          marginTop: "12px",
          maxHeight: "400px",
          overflow: "auto",
        }}
      >
        {itemList}
      </div>
    </div>
  );
};

// Table layout renderer
const TableRenderer = (listProps: QueryListRendererProps<Task>) => {
  const { query, handleQueryChange, itemList } = listProps;

  return (
    <div style={{ width: "100%" }}>
      <InputGroup
        placeholder="Search tasks..."
        value={query}
        onChange={handleQueryChange}
        style={{ marginBottom: "12px" }}
      />
      <table style={{ width: "100%", borderCollapse: "collapse" }}>
        <thead>
          <tr style={{ borderBottom: "2px solid #ccc" }}>
            <th style={{ padding: "8px", textAlign: "left" }}>Task</th>
            <th style={{ padding: "8px", textAlign: "left" }}>Priority</th>
            <th style={{ padding: "8px", textAlign: "left" }}>Status</th>
          </tr>
        </thead>
        <tbody>
          {itemList}
        </tbody>
      </table>
    </div>
  );
};

// Corresponding table item renderer
const renderTaskRow: ItemRenderer<Task> = (task, { handleClick, modifiers }) => (
  <tr
    onClick={handleClick}
    style={{
      backgroundColor: modifiers.active ? "#137CBD" : "transparent",
      color: modifiers.active ? "white" : "black",
      cursor: "pointer",
    }}
  >
    <td style={{ padding: "8px" }}>{task.title}</td>
    <td style={{ padding: "8px" }}>{task.priority}</td>
    <td style={{ padding: "8px" }}>{task.completed ? "✓" : "○"}</td>
  </tr>
);

Keyboard Navigation

QueryList provides built-in keyboard navigation that can be extended with custom handlers.

Usage Examples:

const KeyboardQueryList = () => {
  const queryListRef = React.useRef<QueryList<Task>>(null);

  const handleKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {
    // Custom keyboard shortcuts
    switch (event.key) {
      case "ArrowUp":
      case "ArrowDown":
        // Let QueryList handle arrow navigation
        break;
      case "Enter":
        // Let QueryList handle selection
        break;
      case "Escape":
        // Custom: clear query
        if (queryListRef.current) {
          queryListRef.current.setQuery("", true);
        }
        event.preventDefault();
        break;
      case "Home":
        // Custom: jump to first item
        if (queryListRef.current && event.ctrlKey) {
          const firstItem = tasks[0];
          if (firstItem) {
            queryListRef.current.setActiveItem(firstItem);
          }
          event.preventDefault();
        }
        break;
      case "End":
        // Custom: jump to last item
        if (queryListRef.current && event.ctrlKey) {
          const lastItem = tasks[tasks.length - 1];
          if (lastItem) {
            queryListRef.current.setActiveItem(lastItem);
          }
          event.preventDefault();
        }
        break;
    }
  };

  const renderer = (listProps: QueryListRendererProps<Task>) => (
    <div>
      <InputGroup
        placeholder="Type to search, arrows to navigate, Enter to select, Esc to clear..."
        value={listProps.query}
        onChange={listProps.handleQueryChange}
        onKeyDown={listProps.handleKeyDown}
      />
      <div>{listProps.itemList}</div>
    </div>
  );

  return (
    <QueryList<Task>
      ref={queryListRef}
      items={tasks}
      itemRenderer={renderTask}
      onItemSelect={(task) => console.log("Selected:", task.title)}
      renderer={renderer}
      onKeyDown={handleKeyDown}
    />
  );
};

Active Item Management

QueryList provides methods for programmatically managing the active item.

Usage Examples:

const ActiveItemQueryList = () => {
  const queryListRef = React.useRef<QueryList<Task>>(null);
  const [externalActiveItem, setExternalActiveItem] = useState<Task | null>(null);

  // External controls
  const setRandomActive = () => {
    if (queryListRef.current && tasks.length > 0) {
      const randomTask = tasks[Math.floor(Math.random() * tasks.length)];
      queryListRef.current.setActiveItem(randomTask);
      setExternalActiveItem(randomTask);
    }
  };

  const clearActive = () => {
    if (queryListRef.current) {
      queryListRef.current.setActiveItem(null);
      setExternalActiveItem(null);
    }
  };

  const scrollToActive = () => {
    if (queryListRef.current) {
      queryListRef.current.scrollActiveItemIntoView();
    }
  };

  const renderer = (listProps: QueryListRendererProps<Task>) => (
    <div>
      <div style={{ marginBottom: "12px", display: "flex", gap: "8px" }}>
        <button onClick={setRandomActive}>Random Active</button>
        <button onClick={clearActive}>Clear Active</button>
        <button onClick={scrollToActive}>Scroll to Active</button>
      </div>
      
      <InputGroup
        placeholder="Search tasks..."
        value={listProps.query}
        onChange={listProps.handleQueryChange}
        onKeyDown={listProps.handleKeyDown}
      />
      
      <div style={{ maxHeight: "200px", overflow: "auto" }}>
        {listProps.itemList}
      </div>
      
      {externalActiveItem && (
        <div style={{ marginTop: "8px", fontSize: "12px", color: "#666" }}>
          Active: {externalActiveItem.title}
        </div>
      )}
    </div>
  );

  return (
    <QueryList<Task>
      ref={queryListRef}
      items={tasks}
      itemRenderer={renderTask}
      onItemSelect={(task) => console.log("Selected:", task.title)}
      onActiveItemChange={(activeItem) => {
        setExternalActiveItem(activeItem as Task | null);
      }}
      renderer={renderer}
      scrollToActiveItem
    />
  );
};

Utility Functions

QueryList includes utility functions for managing enabled items.

/**
 * Find the first enabled item in a list
 * @param items - Array of items to search
 * @param itemDisabled - Function or property key to determine if item is disabled
 * @param direction - Search direction (1 for forward, -1 for backward)
 * @param startIndex - Index to start search from
 * @returns First enabled item or null if none found
 */
function getFirstEnabledItem<T>(
  items: T[],
  itemDisabled?: keyof T | ((item: T, index: number) => boolean),
  direction?: number,
  startIndex?: number,
): T | CreateNewItem | null;

Usage Examples:

import { getFirstEnabledItem } from "@blueprintjs/select";

const itemsWithDisabled = [
  { name: "Item 1", disabled: true },
  { name: "Item 2", disabled: false },
  { name: "Item 3", disabled: false },
  { name: "Item 4", disabled: true },
];

// Find first enabled item using property key
const firstEnabled = getFirstEnabledItem(itemsWithDisabled, "disabled");
console.log(firstEnabled); // { name: "Item 2", disabled: false }

// Find first enabled item using function
const firstEnabledFn = getFirstEnabledItem(
  itemsWithDisabled,
  (item) => item.disabled,
);

// Find first enabled item starting from index 2, going backward
const firstEnabledBackward = getFirstEnabledItem(
  itemsWithDisabled,
  "disabled",
  -1, // backward direction
  2,  // start from index 2
);