From ca7f01c2eb9aa1bd357ec3d295b4d4de6f693c43 Mon Sep 17 00:00:00 2001 From: Braeden Smith Date: Mon, 4 May 2026 12:16:12 -0400 Subject: [PATCH] [select] Accept ReadonlyArray for items and selectedItems props Widen `items`, `selectedItems`, `filteredItems`, and related callback parameters in `@blueprintjs/select` to accept `ReadonlyArray` so consumers can pass readonly arrays without copying. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../examples/select-examples/selectExample.tsx | 4 ++-- packages/docs-theme/src/components/navigator.tsx | 2 +- packages/select/src/common/itemListRenderer.ts | 4 ++-- packages/select/src/common/listItemsProps.ts | 4 ++-- packages/select/src/common/predicate.ts | 2 +- .../src/components/multi-select/multiSelect.tsx | 4 ++-- .../src/components/query-list/queryList.test.tsx | 4 +++- .../src/components/query-list/queryList.tsx | 15 +++++++++------ 8 files changed, 22 insertions(+), 17 deletions(-) diff --git a/packages/docs-app/src/examples/select-examples/selectExample.tsx b/packages/docs-app/src/examples/select-examples/selectExample.tsx index 8023e260b1c..fbee5f2f0ed 100644 --- a/packages/docs-app/src/examples/select-examples/selectExample.tsx +++ b/packages/docs-app/src/examples/select-examples/selectExample.tsx @@ -195,7 +195,7 @@ const getGroup = (item: Film) => { return /[0-9]/.test(firstLetter) ? "0-9" : firstLetter; }; -const getGroupedItems = (filteredItems: Film[]) => { +const getGroupedItems = (filteredItems: ReadonlyArray) => { return filteredItems.reduce< Array<{ group: string; index: number; items: Film[]; key: number }> >((acc, item, index) => { @@ -212,7 +212,7 @@ const getGroupedItems = (filteredItems: Film[]) => { }, []); }; -const groupedItemListPredicate = (query: string, items: Film[]) => { +const groupedItemListPredicate = (query: string, items: ReadonlyArray) => { return items .filter((item, index) => filterFilm(query, item, index)) .sort((a, b) => getGroup(a).localeCompare(getGroup(b))); diff --git a/packages/docs-theme/src/components/navigator.tsx b/packages/docs-theme/src/components/navigator.tsx index a52f80e33d4..d6aa7c06249 100644 --- a/packages/docs-theme/src/components/navigator.tsx +++ b/packages/docs-theme/src/components/navigator.tsx @@ -90,7 +90,7 @@ export class Navigator extends PureComponent { } private filterMatches: ItemListPredicate = (query, items) => - filter(items, query, { + filter([...items], query, { key: "route", maxInners: items.length / 5, maxResults: 10, diff --git a/packages/select/src/common/itemListRenderer.ts b/packages/select/src/common/itemListRenderer.ts index 0d6ddea2a9f..7e8eaf08f8a 100644 --- a/packages/select/src/common/itemListRenderer.ts +++ b/packages/select/src/common/itemListRenderer.ts @@ -35,13 +35,13 @@ export interface ItemListRendererProps { * map each item in this array through `renderItem`, with support for * optional `noResults` and `initialContent` states. */ - filteredItems: T[]; + filteredItems: ReadonlyArray; /** * Array of all items in the list. * See `filteredItems` for a filtered array based on `query` and predicate props. */ - items: T[]; + items: ReadonlyArray; /** * The current query string. diff --git a/packages/select/src/common/listItemsProps.ts b/packages/select/src/common/listItemsProps.ts index 2beb4431b38..37cb9876c41 100644 --- a/packages/select/src/common/listItemsProps.ts +++ b/packages/select/src/common/listItemsProps.ts @@ -44,7 +44,7 @@ export interface ListItemsProps extends Props { activeItem?: T | CreateNewItem | null; /** Array of items in the list. */ - items: T[]; + items: ReadonlyArray; /** * Specifies how to test if two items are equal. By default, simple strict @@ -170,7 +170,7 @@ export interface ListItemsProps 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 | ReadonlyArray; /** * Custom renderer to transform the current query string into a selectable diff --git a/packages/select/src/common/predicate.ts b/packages/select/src/common/predicate.ts index 8011cbe0338..7eadbe69ece 100644 --- a/packages/select/src/common/predicate.ts +++ b/packages/select/src/common/predicate.ts @@ -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 = (query: string, items: T[]) => T[]; +export type ItemListPredicate = (query: string, items: ReadonlyArray) => T[]; /** * A custom predicate for filtering items based on the provided query. diff --git a/packages/select/src/components/multi-select/multiSelect.tsx b/packages/select/src/components/multi-select/multiSelect.tsx index 44168ff00f9..cb69eb51938 100644 --- a/packages/select/src/components/multi-select/multiSelect.tsx +++ b/packages/select/src/components/multi-select/multiSelect.tsx @@ -44,7 +44,7 @@ export interface MultiSelectProps extends ListItemsProps, SelectPopoverPro * 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: ReadonlyArray, isOpen: boolean) => React.ReactNode; /** * Whether the component is non-interactive. @@ -104,7 +104,7 @@ export interface MultiSelectProps extends ListItemsProps, SelectPopoverPro placeholder?: string; /** Controlled selected values. */ - selectedItems: T[]; + selectedItems: ReadonlyArray; /** * Props to pass to the [TagInput component](##core/components/tag-input). diff --git a/packages/select/src/components/query-list/queryList.test.tsx b/packages/select/src/components/query-list/queryList.test.tsx index f2f3dba4eb2..f189389b028 100644 --- a/packages/select/src/components/query-list/queryList.test.tsx +++ b/packages/select/src/components/query-list/queryList.test.tsx @@ -77,7 +77,9 @@ describe("", () => { }); it("itemListPredicate filters entire list by query", () => { - const predicate = sinon.spy((query: string, films: Film[]) => films.filter(f => f.year === +query)); + const predicate = sinon.spy((query: string, films: ReadonlyArray) => + films.filter(f => f.year === +query), + ); shallow( {...testProps} itemListPredicate={predicate} query="1994" />); expect(predicate.callCount).toBe(1); diff --git a/packages/select/src/components/query-list/queryList.tsx b/packages/select/src/components/query-list/queryList.tsx index b973c3f2fa4..aa1d489e0b3 100644 --- a/packages/select/src/components/query-list/queryList.tsx +++ b/packages/select/src/components/query-list/queryList.tsx @@ -148,10 +148,10 @@ export interface QueryListState { * this element will be used to hide the "Create Item" option if its value * matches the current `query`. */ - createNewItem: T | T[] | undefined; + createNewItem: T | ReadonlyArray | undefined; /** The original `items` array filtered by `itemListPredicate` or `itemPredicate`. */ - filteredItems: T[]; + filteredItems: ReadonlyArray; /** The current query string. */ query: string; @@ -619,7 +619,7 @@ export class QueryList extends AbstractComponent, QueryList * @param createNewItem Checks if this item would match the current query. Cannot check this.state.createNewItem * every time since state may not have been updated yet. */ - private isCreateItemRendered(createNewItem?: T | T[]): boolean { + private isCreateItemRendered(createNewItem?: T | ReadonlyArray): boolean { return ( this.canCreateItems() && this.state.query !== "" && @@ -638,7 +638,7 @@ export class QueryList extends AbstractComponent, QueryList return this.props.createNewItemFromQuery != null && this.props.createNewItemRenderer != null; } - private wouldCreatedItemMatchSomeExistingItem(createNewItem?: T | T[]) { + private wouldCreatedItemMatchSomeExistingItem(createNewItem?: T | ReadonlyArray) { // search only the filtered items, not the full items list, because we // only need to check items that match the current query. return this.state.filteredItems.some(item => { @@ -688,7 +688,10 @@ function getMatchingItem(query: string, { items, itemPredicate }: QueryListPr return undefined; } -function getFilteredItems(query: string, { items, itemPredicate, itemListPredicate }: QueryListProps) { +function getFilteredItems( + query: string, + { items, itemPredicate, itemListPredicate }: QueryListProps, +): ReadonlyArray { if (Utils.isFunction(itemListPredicate)) { // note that implementations can reorder the items here return itemListPredicate(query, items); @@ -727,7 +730,7 @@ function isItemDisabled(item: T | null, index: number, itemDisabled?: ListIte * @param startIndex which index to begin moving from */ export function getFirstEnabledItem( - items: T[], + items: ReadonlyArray, itemDisabled?: keyof T | ((item: T, index: number) => boolean), direction = 1, startIndex = items.length - 1,