Skip to content

Commit

Permalink
Use generic for specifying select item mutability
Browse files Browse the repository at this point in the history
  • Loading branch information
ggdouglas committed Oct 30, 2024
1 parent aa64811 commit 8f5925a
Show file tree
Hide file tree
Showing 8 changed files with 90 additions and 67 deletions.
14 changes: 8 additions & 6 deletions packages/select/src/common/itemListRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import type { CreateNewItem } from "./listItemsUtils";
* An object describing how to render the list of items.
* An `itemListRenderer` receives this object as its sole argument.
*/
export interface ItemListRendererProps<T> {
export interface ItemListRendererProps<T, A extends readonly T[] = T[]> {
/**
* The currently focused item (for keyboard interactions), or `null` to
* indicate that no item is active.
Expand All @@ -35,13 +35,13 @@ export interface ItemListRendererProps<T> {
* map each item in this array through `renderItem`, with support for
* optional `noResults` and `initialContent` states.
*/
filteredItems: T[];
filteredItems: A;

/**
* Array of all items in the list.
* See `filteredItems` for a filtered array based on `query` and predicate props.
*/
items: T[];
items: A;

/**
* The current query string.
Expand Down Expand Up @@ -75,15 +75,17 @@ export interface ItemListRendererProps<T> {
}

/** Type alias for a function that renders the list of items. */
export type ItemListRenderer<T> = (itemListProps: ItemListRendererProps<T>) => React.JSX.Element | null;
export type ItemListRenderer<T, A extends readonly T[] = T[]> = (
itemListProps: ItemListRendererProps<T, A>,
) => React.JSX.Element | null;

/**
* `ItemListRenderer` helper method for rendering each item in `filteredItems`,
* with optional support for `noResults` (when filtered items is empty)
* and `initialContent` (when query is empty).
*/
export function renderFilteredItems(
props: ItemListRendererProps<any>,
export function renderFilteredItems<T, A extends readonly T[] = T[]>(
props: ItemListRendererProps<T, A>,
noResults?: React.ReactNode,
initialContent?: React.ReactNode | null,
): React.ReactNode {
Expand Down
12 changes: 6 additions & 6 deletions packages/select/src/common/listItemsProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export type ItemsEqualComparator<T> = (itemA: T, itemB: T) => boolean;
export type ItemsEqualProp<T> = ItemsEqualComparator<T> | keyof T;

/** Reusable generic props for a component that operates on a filterable, selectable list of `items`. */
export interface ListItemsProps<T> extends Props {
export interface ListItemsProps<T, A extends readonly T[] = T[]> extends Props {
/**
* The currently focused item for keyboard interactions, or `null` to
* indicate that no item is active. If omitted or `undefined`, this prop will be
Expand All @@ -44,7 +44,7 @@ export interface ListItemsProps<T> extends Props {
activeItem?: T | CreateNewItem | null;

/** Array of items in the list. */
items: T[];
items: A;

/**
* Specifies how to test if two items are equal. By default, simple strict
Expand Down Expand Up @@ -75,7 +75,7 @@ export interface ListItemsProps<T> extends Props {
*
* If `itemPredicate` is also defined, this prop takes priority and the other will be ignored.
*/
itemListPredicate?: ItemListPredicate<T>;
itemListPredicate?: ItemListPredicate<T, A>;

/**
* Customize querying of individual items.
Expand Down Expand Up @@ -110,7 +110,7 @@ export interface ListItemsProps<T> extends Props {
* and wraps them all in a `Menu` element. If the query is empty then `initialContent` is returned,
* and if there are no items that match the predicate then `noResults` is returned.
*/
itemListRenderer?: ItemListRenderer<T>;
itemListRenderer?: ItemListRenderer<T, A>;

/**
* React content to render when query is empty.
Expand Down Expand Up @@ -157,7 +157,7 @@ export interface ListItemsProps<T> extends Props {
/**
* Callback invoked when multiple items are selected at once via pasting.
*/
onItemsPaste?: (items: T[]) => void;
onItemsPaste?: (items: A) => void;

/**
* Callback invoked when the query string changes.
Expand All @@ -170,7 +170,7 @@ export interface ListItemsProps<T> extends Props {
* created, either by pressing the `Enter` key or by clicking on the "Create
* Item" option. It transforms a query string into one or many items type.
*/
createNewItemFromQuery?: (query: string) => T | T[];
createNewItemFromQuery?: (query: string) => T | A;

/**
* Custom renderer to transform the current query string into a selectable
Expand Down
2 changes: 1 addition & 1 deletion packages/select/src/common/predicate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
* A custom predicate for returning an entirely new `items` array based on the provided query.
* See usage sites in `ListItemsProps`.
*/
export type ItemListPredicate<T> = (query: string, items: T[]) => T[];
export type ItemListPredicate<T, A extends readonly T[] = T[]> = (query: string, items: A) => A;

/**
* A custom predicate for filtering items based on the provided query.
Expand Down
29 changes: 16 additions & 13 deletions packages/select/src/components/multi-select/multiSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ import { Cross } from "@blueprintjs/icons";
import { Classes, type ListItemsProps, type SelectPopoverProps } from "../../common";
import { QueryList, type QueryListRendererProps } from "../query-list/queryList";

export interface MultiSelectProps<T> extends ListItemsProps<T>, SelectPopoverProps {
export interface MultiSelectProps<T, A extends readonly T[] = T[]> extends ListItemsProps<T, A>, SelectPopoverProps {
/**
* Element which triggers the multiselect popover. Providing this prop will replace the default TagInput
* target thats rendered and move the search functionality to within the Popover.
*/
customTarget?: (selectedItems: T[], isOpen: boolean) => React.ReactNode;
customTarget?: (selectedItems: A, isOpen: boolean) => React.ReactNode;

/**
* Whether the component is non-interactive.
Expand Down Expand Up @@ -104,7 +104,7 @@ export interface MultiSelectProps<T> extends ListItemsProps<T>, SelectPopoverPro
placeholder?: string;

/** Controlled selected values. */
selectedItems: T[];
selectedItems: A;

/**
* Props to pass to the [TagInput component](##core/components/tag-input).
Expand Down Expand Up @@ -142,7 +142,10 @@ export interface MultiSelectState {
*
* @see https://blueprintjs.com/docs/#select/multi-select
*/
export class MultiSelect<T> extends AbstractPureComponent<MultiSelectProps<T>, MultiSelectState> {
export class MultiSelect<T, A extends readonly T[] = T[]> extends AbstractPureComponent<
MultiSelectProps<T, A>,
MultiSelectState
> {
public static displayName = `${DISPLAYNAME_PREFIX}.MultiSelect`;

private listboxId = Utils.uniqueId("listbox");
Expand All @@ -164,19 +167,19 @@ export class MultiSelect<T> extends AbstractPureComponent<MultiSelectProps<T>, M

public input: HTMLInputElement | null = null;

public queryList: QueryList<T> | null = null;
public queryList: QueryList<T, A> | null = null;

private refHandlers: {
input: React.RefCallback<HTMLInputElement>;
popover: React.RefObject<Popover>;
queryList: React.RefCallback<QueryList<T>>;
queryList: React.RefCallback<QueryList<T, A>>;
} = {
input: refHandler(this, "input", this.props.tagInputProps?.inputRef),
popover: React.createRef(),
queryList: (ref: QueryList<T> | null) => (this.queryList = ref),
queryList: (ref: QueryList<T, A> | null) => (this.queryList = ref),
};

public componentDidUpdate(prevProps: MultiSelectProps<T>) {
public componentDidUpdate(prevProps: MultiSelectProps<T, A>) {
if (prevProps.tagInputProps?.inputRef !== this.props.tagInputProps?.inputRef) {
setRef(prevProps.tagInputProps?.inputRef, null);
this.refHandlers.input = refHandler(this, "input", this.props.tagInputProps?.inputRef);
Expand All @@ -195,7 +198,7 @@ export class MultiSelect<T> extends AbstractPureComponent<MultiSelectProps<T>, M
const { menuProps, openOnKeyDown, popoverProps, tagInputProps, customTarget, ...restProps } = this.props;

return (
<QueryList<T>
<QueryList<T, A>
{...restProps}
menuProps={{
"aria-label": "selectable options",
Expand All @@ -211,7 +214,7 @@ export class MultiSelect<T> extends AbstractPureComponent<MultiSelectProps<T>, M
);
}

private renderQueryList = (listProps: QueryListRendererProps<T>) => {
private renderQueryList = (listProps: QueryListRendererProps<T, A>) => {
const { disabled, popoverContentProps = {}, popoverProps = {} } = this.props;
const { handleKeyDown, handleKeyUp } = listProps;

Expand Down Expand Up @@ -267,7 +270,7 @@ export class MultiSelect<T> extends AbstractPureComponent<MultiSelectProps<T>, M
// the "fill" prop. Note that we must take `isOpen` as an argument to force this render function to be called
// again after that state changes.
private getPopoverTargetRenderer =
(listProps: QueryListRendererProps<T>, isOpen: boolean) =>
(listProps: QueryListRendererProps<T, A>, isOpen: boolean) =>
// N.B. pull out `isOpen` so that it's not forwarded to the DOM, but remember not to use it directly
// since it may be stale (`renderTarget` is not re-invoked on this.state changes).
// eslint-disable-next-line react/display-name
Expand Down Expand Up @@ -304,7 +307,7 @@ export class MultiSelect<T> extends AbstractPureComponent<MultiSelectProps<T>, M
);
};

private getTagInput = (listProps: QueryListRendererProps<T>, className?: string) => {
private getTagInput = (listProps: QueryListRendererProps<T, A>, className?: string) => {
const { disabled, fill, onClear, placeholder, selectedItems, tagInputProps = {} } = this.props;

const maybeClearButton =
Expand Down Expand Up @@ -408,7 +411,7 @@ export class MultiSelect<T> extends AbstractPureComponent<MultiSelectProps<T>, M
};

private getTagInputAddHandler =
(listProps: QueryListRendererProps<T>) => (values: any[], method: TagInputAddMethod) => {
(listProps: QueryListRendererProps<T, A>) => (values: any[], method: TagInputAddMethod) => {
if (method === "paste") {
listProps.handlePaste(values);
}
Expand Down
8 changes: 4 additions & 4 deletions packages/select/src/components/omnibar/omnibar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { Search } from "@blueprintjs/icons";
import { Classes, type ListItemsProps } from "../../common";
import { QueryList, type QueryListRendererProps } from "../query-list/queryList";

export interface OmnibarProps<T> extends ListItemsProps<T> {
export interface OmnibarProps<T, A extends readonly T[] = T[]> extends ListItemsProps<T, A> {
/**
* Props to spread to the query `InputGroup`. Use `query` and
* `onQueryChange` instead of `inputProps.value` and `inputProps.onChange`
Expand Down Expand Up @@ -58,7 +58,7 @@ export interface OmnibarProps<T> extends ListItemsProps<T> {
*
* @see https://blueprintjs.com/docs/#select/omnibar
*/
export class Omnibar<T> extends React.PureComponent<OmnibarProps<T>> {
export class Omnibar<T, A extends readonly T[] = T[]> extends React.PureComponent<OmnibarProps<T, A>> {
public static displayName = `${DISPLAYNAME_PREFIX}.Omnibar`;

public static ofType<U>() {
Expand All @@ -71,7 +71,7 @@ export class Omnibar<T> extends React.PureComponent<OmnibarProps<T>> {
const initialContent = "initialContent" in this.props ? this.props.initialContent : null;

return (
<QueryList<T>
<QueryList<T, A>
{...restProps}
// Omnibar typically does not keep track of and/or show its selection state like other
// select components, so it's more of a menu than a listbox. This means that users should return
Expand All @@ -83,7 +83,7 @@ export class Omnibar<T> extends React.PureComponent<OmnibarProps<T>> {
);
}

private renderQueryList = (listProps: QueryListRendererProps<T>) => {
private renderQueryList = (listProps: QueryListRendererProps<T, A>) => {
const { inputProps = {}, isOpen, overlayProps = {} } = this.props;
const { handleKeyDown, handleKeyUp } = listProps;
const handlers = isOpen ? { onKeyDown: handleKeyDown, onKeyUp: handleKeyUp } : {};
Expand Down
Loading

0 comments on commit 8f5925a

Please sign in to comment.