Skip to content

Commit 8f5925a

Browse files
committed
Use generic for specifying select item mutability
1 parent aa64811 commit 8f5925a

File tree

8 files changed

+90
-67
lines changed

8 files changed

+90
-67
lines changed

packages/select/src/common/itemListRenderer.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import type { CreateNewItem } from "./listItemsUtils";
2020
* An object describing how to render the list of items.
2121
* An `itemListRenderer` receives this object as its sole argument.
2222
*/
23-
export interface ItemListRendererProps<T> {
23+
export interface ItemListRendererProps<T, A extends readonly T[] = T[]> {
2424
/**
2525
* The currently focused item (for keyboard interactions), or `null` to
2626
* indicate that no item is active.
@@ -35,13 +35,13 @@ export interface ItemListRendererProps<T> {
3535
* map each item in this array through `renderItem`, with support for
3636
* optional `noResults` and `initialContent` states.
3737
*/
38-
filteredItems: T[];
38+
filteredItems: A;
3939

4040
/**
4141
* Array of all items in the list.
4242
* See `filteredItems` for a filtered array based on `query` and predicate props.
4343
*/
44-
items: T[];
44+
items: A;
4545

4646
/**
4747
* The current query string.
@@ -75,15 +75,17 @@ export interface ItemListRendererProps<T> {
7575
}
7676

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

8082
/**
8183
* `ItemListRenderer` helper method for rendering each item in `filteredItems`,
8284
* with optional support for `noResults` (when filtered items is empty)
8385
* and `initialContent` (when query is empty).
8486
*/
85-
export function renderFilteredItems(
86-
props: ItemListRendererProps<any>,
87+
export function renderFilteredItems<T, A extends readonly T[] = T[]>(
88+
props: ItemListRendererProps<T, A>,
8789
noResults?: React.ReactNode,
8890
initialContent?: React.ReactNode | null,
8991
): React.ReactNode {

packages/select/src/common/listItemsProps.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export type ItemsEqualComparator<T> = (itemA: T, itemB: T) => boolean;
3434
export type ItemsEqualProp<T> = ItemsEqualComparator<T> | keyof T;
3535

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

4646
/** Array of items in the list. */
47-
items: T[];
47+
items: A;
4848

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

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

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

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

175175
/**
176176
* Custom renderer to transform the current query string into a selectable

packages/select/src/common/predicate.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
* A custom predicate for returning an entirely new `items` array based on the provided query.
1919
* See usage sites in `ListItemsProps`.
2020
*/
21-
export type ItemListPredicate<T> = (query: string, items: T[]) => T[];
21+
export type ItemListPredicate<T, A extends readonly T[] = T[]> = (query: string, items: A) => A;
2222

2323
/**
2424
* A custom predicate for filtering items based on the provided query.

packages/select/src/components/multi-select/multiSelect.tsx

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,12 @@ import { Cross } from "@blueprintjs/icons";
3939
import { Classes, type ListItemsProps, type SelectPopoverProps } from "../../common";
4040
import { QueryList, type QueryListRendererProps } from "../query-list/queryList";
4141

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

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

106106
/** Controlled selected values. */
107-
selectedItems: T[];
107+
selectedItems: A;
108108

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

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

165168
public input: HTMLInputElement | null = null;
166169

167-
public queryList: QueryList<T> | null = null;
170+
public queryList: QueryList<T, A> | null = null;
168171

169172
private refHandlers: {
170173
input: React.RefCallback<HTMLInputElement>;
171174
popover: React.RefObject<Popover>;
172-
queryList: React.RefCallback<QueryList<T>>;
175+
queryList: React.RefCallback<QueryList<T, A>>;
173176
} = {
174177
input: refHandler(this, "input", this.props.tagInputProps?.inputRef),
175178
popover: React.createRef(),
176-
queryList: (ref: QueryList<T> | null) => (this.queryList = ref),
179+
queryList: (ref: QueryList<T, A> | null) => (this.queryList = ref),
177180
};
178181

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

197200
return (
198-
<QueryList<T>
201+
<QueryList<T, A>
199202
{...restProps}
200203
menuProps={{
201204
"aria-label": "selectable options",
@@ -211,7 +214,7 @@ export class MultiSelect<T> extends AbstractPureComponent<MultiSelectProps<T>, M
211214
);
212215
}
213216

214-
private renderQueryList = (listProps: QueryListRendererProps<T>) => {
217+
private renderQueryList = (listProps: QueryListRendererProps<T, A>) => {
215218
const { disabled, popoverContentProps = {}, popoverProps = {} } = this.props;
216219
const { handleKeyDown, handleKeyUp } = listProps;
217220

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

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

310313
const maybeClearButton =
@@ -408,7 +411,7 @@ export class MultiSelect<T> extends AbstractPureComponent<MultiSelectProps<T>, M
408411
};
409412

410413
private getTagInputAddHandler =
411-
(listProps: QueryListRendererProps<T>) => (values: any[], method: TagInputAddMethod) => {
414+
(listProps: QueryListRendererProps<T, A>) => (values: any[], method: TagInputAddMethod) => {
412415
if (method === "paste") {
413416
listProps.handlePaste(values);
414417
}

packages/select/src/components/omnibar/omnibar.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { Search } from "@blueprintjs/icons";
2323
import { Classes, type ListItemsProps } from "../../common";
2424
import { QueryList, type QueryListRendererProps } from "../query-list/queryList";
2525

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

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

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

86-
private renderQueryList = (listProps: QueryListRendererProps<T>) => {
86+
private renderQueryList = (listProps: QueryListRendererProps<T, A>) => {
8787
const { inputProps = {}, isOpen, overlayProps = {} } = this.props;
8888
const { handleKeyDown, handleKeyUp } = listProps;
8989
const handlers = isOpen ? { onKeyDown: handleKeyDown, onKeyUp: handleKeyUp } : {};

0 commit comments

Comments
 (0)