Compare commits

...

3 Commits

Author SHA1 Message Date
github-actions[bot] 8cd964dd14 ci(changesets): version packages (beta) (#4095) 2024-11-16 15:49:48 -03:00
Junior Garcia b9d5d49251 Feat/virtualization for autocomplete (#4094)
* feat: add react-window virtualization for autocomplete

* fix: wrong imports and wrong sizing

* fix: update pnpm lock

* chore: add test cases for large dataset (1000 and 10000 items)

* chore: move virtualized-listbox to listbox components folder, implement isVirtualized conditional

* feat: implement dynamic listboxheight n item height, add story

* chore: rename props, remove unnecessary line changes

* fix: maxHeight style 256px for default, conditional usage of virtualizer

* feat: migrate to tan-stack virtual. (todo: fix scroll shadow)

* feat: virtualization support

---------

Co-authored-by: Vincentius Roger Kuswara <vincentiusrkuswara@gmail.com>
2024-11-16 15:46:56 -03:00
Junior Garcia f81ad5ebba feat: new demo added to alert 2024-11-16 15:02:49 -03:00
29 changed files with 848 additions and 104 deletions
+1
View File
@@ -117,6 +117,7 @@
"poor-moose-double",
"poor-sheep-repair",
"popular-pigs-begin",
"proud-chicken-impress",
"quick-geckos-punch",
"red-camels-build",
"selfish-baboons-know",
+7
View File
@@ -0,0 +1,7 @@
---
"@nextui-org/autocomplete": patch
"@nextui-org/listbox": patch
"@nextui-org/theme": patch
---
Virtualization support added to Listbox & Autocomplete
+1 -1
View File
@@ -171,7 +171,7 @@ function TreeItem<T>(props: TreeItemProps<T>) {
</span>
{isUpdated && (
<Chip
className="ml-1 py-1 text-tiny text-default-400 bg-default-100/50"
className="ml-1 py-1 text-tiny text-default-500 dark:text-default-400 bg-default-100 dark:bg-default-100/50"
color="default"
size="sm"
variant="flat"
+15 -25
View File
@@ -143,7 +143,8 @@
"key": "autocomplete",
"title": "Autocomplete",
"keywords": "autocomplete, auto suggest, search, typeahead",
"path": "/docs/components/autocomplete.mdx"
"path": "/docs/components/autocomplete.mdx",
"updated": true
},
{
"key": "alert",
@@ -162,8 +163,7 @@
"key": "badge",
"title": "Badge",
"keywords": "badge, markers, status indication, count display",
"path": "/docs/components/badge.mdx",
"updated": true
"path": "/docs/components/badge.mdx"
},
{
"key": "breadcrumbs",
@@ -175,14 +175,14 @@
"key": "button",
"title": "Button",
"keywords": "button, interactive, action trigger, click events",
"path": "/docs/components/button.mdx"
"path": "/docs/components/button.mdx",
"updated": true
},
{
"key": "calendar",
"title": "Calendar",
"keywords": "calendar, date picker, month picker, year picker",
"path": "/docs/components/calendar.mdx",
"newPost": true
"path": "/docs/components/calendar.mdx"
},
{
"key": "card",
@@ -194,8 +194,7 @@
"key": "checkbox",
"title": "Checkbox",
"keywords": "checkbox, binary choice, selection control, toggle",
"path": "/docs/components/checkbox.mdx",
"updated": true
"path": "/docs/components/checkbox.mdx"
},
{
"key": "checkbox-group",
@@ -225,22 +224,18 @@
"key": "date-input",
"title": "Date Input",
"keywords": "date-input, time, input, timezone",
"path": "/docs/components/date-input.mdx",
"newPost": true
},
"path": "/docs/components/date-input.mdx" },
{
"key": "date-picker",
"title": "Date Picker",
"keywords": "date-picker, time, input, timezone",
"path": "/docs/components/date-picker.mdx",
"newPost": true
"path": "/docs/components/date-picker.mdx"
},
{
"key": "date-range-picker",
"title": "Date Range Picker",
"keywords": "date-range-picker, date-picker, time, input, timezone",
"path": "/docs/components/date-range-picker.mdx",
"newPost": true
"path": "/docs/components/date-range-picker.mdx"
},
{
"key": "divider",
@@ -332,8 +327,7 @@
"key": "range-calendar",
"title": "Range Calendar",
"keywords": "range calendar, date picker, month picker, year picker",
"path": "/docs/components/range-calendar.mdx",
"newPost": true
"path": "/docs/components/range-calendar.mdx"
},
{
"key": "scroll-shadow",
@@ -351,8 +345,7 @@
"key": "skeleton",
"title": "Skeleton",
"keywords": "skeleton, loading state, placeholder, content preview",
"path": "/docs/components/skeleton.mdx",
"updated": true
"path": "/docs/components/skeleton.mdx"
},
{
"key": "slider",
@@ -394,8 +387,7 @@
"key": "tabs",
"title": "Tabs",
"keywords": "tabs, section navigation, categorized content, tabbed interface",
"path": "/docs/components/tabs.mdx",
"updated": true
"path": "/docs/components/tabs.mdx"
},
{
"key": "textarea",
@@ -407,15 +399,13 @@
"key": "time-input",
"title": "Time Input",
"keywords": "timeinput, time, input, timezone",
"path": "/docs/components/time-input.mdx",
"newPost": true
"path": "/docs/components/time-input.mdx"
},
{
"key": "tooltip",
"title": "Tooltip",
"keywords": "tooltip, hint, descriptive message, hover info",
"path": "/docs/components/tooltip.mdx",
"updated": true
"path": "/docs/components/tooltip.mdx"
},
{
"key": "user",
@@ -7,6 +7,7 @@ import variants from "./variants";
import withIcon from "./with-icon";
import withAction from "./with-action";
import controlled from "./controlled";
import withoutIcon from "./without-icon";
export const alertContent = {
colors,
@@ -18,4 +19,5 @@ export const alertContent = {
withIcon,
withAction,
controlled,
withoutIcon,
};
@@ -0,0 +1,26 @@
const App = `import {Alert} from "@nextui-org/react";
export default function App() {
const title = "This is an alert";
const description = "Thanks for subscribing to our newsletter!";
return (
<div className="flex items-center justify-center w-full">
<Alert
title={title}
description={description}
hideIcon
color="success"
variant="faded"
/>
</div>
);
}`;
const react = {
"/App.jsx": App,
};
export default {
...react,
};
@@ -26,6 +26,10 @@ import customSectionsStyle from "./custom-sections-style";
import customStyles from "./custom-styles";
import customEmptyContentMessage from "./custom-empty-content-message";
import readOnly from "./read-only";
import virtualization from "./virtualization";
import virtualizationTenThousand from "./virtualization-ten-thousand";
import virtualizationMaxListboxHeight from "./virtualization-max-listbox-height";
import virtualizationCustomItemHeight from "./virtualization-custom-item-height";
export const autocompleteContent = {
usage,
@@ -56,4 +60,8 @@ export const autocompleteContent = {
customStyles,
customEmptyContentMessage,
readOnly,
virtualization,
virtualizationTenThousand,
virtualizationMaxListboxHeight,
virtualizationCustomItemHeight,
};
@@ -0,0 +1,65 @@
const App = `import {Autocomplete, AutocompleteItem} from "@nextui-org/react";
const generateItems = (n) => {
const items = [
"Cat",
"Dog",
"Elephant",
"Lion",
"Tiger",
"Giraffe",
"Dolphin",
"Penguin",
"Zebra",
"Shark",
"Whale",
"Otter",
"Crocodile",
];
const dataset = [];
for (let i = 0; i < n; i++) {
const item = items[i % items.length];
dataset.push({
label: \`\${item}\${i}\`,
value: \`\${item.toLowerCase()}\${i}\`,
description: "Sample description",
});
}
return dataset;
};
export default function App() {
const items = generateItems(1000);
return (
<div className="flex w-full flex-wrap md:flex-nowrap gap-4">
<Autocomplete
isVirtualized
label="Search from 1000 items"
className="max-w-xs"
defaultItems={items}
placeholder="Search..."
maxListboxHeight={400}
itemHeight={40}
>
{(item) => (
<AutocompleteItem key={item.value}>
{item.label}
</AutocompleteItem>
)}
</Autocomplete>
</div>
);
}`;
const react = {
"/App.jsx": App,
};
export default {
...react,
};
@@ -0,0 +1,64 @@
const App = `import {Autocomplete, AutocompleteItem} from "@nextui-org/react";
const generateItems = (n) => {
const items = [
"Cat",
"Dog",
"Elephant",
"Lion",
"Tiger",
"Giraffe",
"Dolphin",
"Penguin",
"Zebra",
"Shark",
"Whale",
"Otter",
"Crocodile",
];
const dataset = [];
for (let i = 0; i < n; i++) {
const item = items[i % items.length];
dataset.push({
label: \`\${item}\${i}\`,
value: \`\${item.toLowerCase()}\${i}\`,
description: "Sample description",
});
}
return dataset;
};
export default function App() {
const items = generateItems(1000);
return (
<div className="flex w-full flex-wrap md:flex-nowrap gap-4">
<Autocomplete
isVirtualized
label="Search from 1000 items"
className="max-w-xs"
defaultItems={items}
placeholder="Search..."
maxListboxHeight={400}
>
{(item) => (
<AutocompleteItem key={item.value}>
{item.label}
</AutocompleteItem>
)}
</Autocomplete>
</div>
);
}`;
const react = {
"/App.jsx": App,
};
export default {
...react,
};
@@ -0,0 +1,63 @@
const App = `import {Autocomplete, AutocompleteItem} from "@nextui-org/react";
const generateItems = (n) => {
const items = [
"Cat",
"Dog",
"Elephant",
"Lion",
"Tiger",
"Giraffe",
"Dolphin",
"Penguin",
"Zebra",
"Shark",
"Whale",
"Otter",
"Crocodile",
];
const dataset = [];
for (let i = 0; i < n; i++) {
const item = items[i % items.length];
dataset.push({
label: \`\${item}\${i}\`,
value: \`\${item.toLowerCase()}\${i}\`,
description: "Sample description",
});
}
return dataset;
};
export default function App() {
const items = generateItems(10000);
return (
<div className="flex w-full flex-wrap md:flex-nowrap gap-4">
<Autocomplete
isVirtualized
label="Search from 1000 items"
className="max-w-xs"
defaultItems={items}
placeholder="Search..."
>
{(item) => (
<AutocompleteItem key={item.value}>
{item.label}
</AutocompleteItem>
)}
</Autocomplete>
</div>
);
}`;
const react = {
"/App.jsx": App,
};
export default {
...react,
};
@@ -0,0 +1,63 @@
const App = `import {Autocomplete, AutocompleteItem} from "@nextui-org/react";
const generateItems = (n) => {
const items = [
"Cat",
"Dog",
"Elephant",
"Lion",
"Tiger",
"Giraffe",
"Dolphin",
"Penguin",
"Zebra",
"Shark",
"Whale",
"Otter",
"Crocodile",
];
const dataset = [];
for (let i = 0; i < n; i++) {
const item = items[i % items.length];
dataset.push({
label: \`\${item}\${i}\`,
value: \`\${item.toLowerCase()}\${i}\`,
description: "Sample description",
});
}
return dataset;
};
export default function App() {
const items = generateItems(1000);
return (
<div className="flex w-full flex-wrap md:flex-nowrap gap-4">
<Autocomplete
isVirtualized
label="Search from 1000 items"
className="max-w-xs"
defaultItems={items}
placeholder="Search..."
>
{(item) => (
<AutocompleteItem key={item.value}>
{item.label}
</AutocompleteItem>
)}
</Autocomplete>
</div>
);
}`;
const react = {
"/App.jsx": App,
};
export default {
...react,
};
+9 -1
View File
@@ -55,10 +55,18 @@ Alert comes with 6 color variants to convey different meanings.
<CodeDemo title="Radius" files={alertContent.radius} />
### With Icon
### Custom Icon
By default, Alert displays an appropriate icon based on the `color` prop. You can override this by providing a custom icon using the `icon` prop.
<CodeDemo title="Custom Icon" files={alertContent.withIcon} />
### Without Icon
You can hide the icon by setting the `hideIcon` prop to `true`.
<CodeDemo title="Without Icon" files={alertContent.withoutIcon} />
### With Action
Alert supports an `endContent` prop for additional actions.
@@ -13,7 +13,7 @@ An autocomplete combines a text input with a listbox, allowing users to filter a
---
<CarbonAd/>
<CarbonAd />
## Installation
@@ -24,11 +24,10 @@ An autocomplete combines a text input with a listbox, allowing users to filter a
npm: "npm install @nextui-org/autocomplete",
yarn: "yarn add @nextui-org/autocomplete",
pnpm: "pnpm add @nextui-org/autocomplete",
bun: "bun add @nextui-org/autocomplete"
bun: "bun add @nextui-org/autocomplete",
}}
/>
## Import
NextUI exports 3 autocomplete-related components:
@@ -84,7 +83,7 @@ the end of the label and the autocomplete will be required.
### Read Only
If you pass the `isReadOnly` property to the Autocomplete, the Listbox will open to display
If you pass the `isReadOnly` property to the Autocomplete, the Listbox will open to display
all available options, but users won't be able to select any of the listed options.
<CodeDemo title="Read Only" highlightedLines="8" files={autocompleteContent.readOnly} />
@@ -243,7 +242,10 @@ You can customize the autocomplete items by modifying the `AutocompleteItem` chi
By default, a message `No results found.` will be shown if there is no result matching a query with your filter. You can customize the empty content message by modifying the `emptyContent` in `listboxProps`.
<CodeDemo title="Custom Empty Content Message" files={autocompleteContent.customEmptyContentMessage} />
<CodeDemo
title="Custom Empty Content Message"
files={autocompleteContent.customEmptyContentMessage}
/>
### Custom Filtering
@@ -316,6 +318,36 @@ import {useInfiniteScroll} from "@nextui-org/use-infinite-scroll";
files={autocompleteContent.asyncLoadingItems}
/>
### Virtualization
Autocomplete supports virtualization, in the example below we are using the `isVirtualized` prop to enable virtualization.
<CodeDemo
title="Virtualization"
highlightedLines="41"
files={autocompleteContent.virtualization}
/>
> **Note**: The virtualization strategy is based on the [@tanstack/react-virtual](https://tanstack.com/virtual/latest) package, which provides efficient rendering of large lists by only rendering items that are visible in the viewport.
#### Ten Thousand Items
Virtualization with 10,000 items.
<CodeDemo title="Ten Thousand Items" files={autocompleteContent.virtualizationTenThousand} />
#### Max Listbox Height
The `maxListboxHeight` prop is used to set the maximum height of the listbox. This is required when using virtualization. By default, it's set to `256`.
<CodeDemo title="Max Listbox Height" files={autocompleteContent.virtualizationMaxListboxHeight} />
#### Custom Item Height
The `itemHeight` prop is used to set the height of each item in the listbox. This is required when using virtualization. By default, it's set to `32`.
<CodeDemo title="Custom Item Height" files={autocompleteContent.virtualizationCustomItemHeight} />
### With Sections
You can use the `AutocompleteSection` component to group autocomplete items.
@@ -412,57 +444,60 @@ properties to customize the popover, listbox and input components.
### Autocomplete Props
| Attribute | Type | Description | Default |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
| children\* | `ReactNode[]` | The children to render. Usually a list of `AutocompleteItem` and `AutocompleteSection` elements. | - |
| label | `ReactNode` | The content to display as the label. | - |
| name | `string` | The name of the input element, used when submitting an HTML form. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefname). | - |
| variant | `flat` \| `bordered` \| `faded` \| `underlined` | The variant of the Autocomplete. | `flat` |
| color | `default` \| `primary` \| `secondary` \| `success` \| `warning` \| `danger` | The color of the Autocomplete. | `default` |
| size | `sm` \| `md` \| `lg` | The size of the Autocomplete. | `md` |
| radius | `none` \| `sm` \| `md` \| `lg` \| `full` | The radius of the Autocomplete. | - |
| items | [`Iterable<T>`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) | The list of Autocomplete items. (controlled) | - |
| defaultItems | [`Iterable<T>`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) | The list of Autocomplete items (uncontrolled). | - |
| inputValue | `string` | The value of the Autocomplete input (controlled). | - |
| defaultInputValue | `string` | The value of the Autocomplete input (uncontrolled). | - |
| allowsCustomValue | `boolean` | Whether the Autocomplete allows a non-item matching input value to be set. | `false` |
| allowsEmptyCollection | `boolean` | Whether the autocomplete allows the menu to be open when the collection is empty. | `true` |
| shouldCloseOnBlur | `boolean` | Whether the Autocomplete should close when the input is blurred. | `true` |
| placeholder | `string` | Temporary text that occupies the text input when it is empty. | - |
| description | `ReactNode` | A description for the field. Provides a hint such as specific requirements for what to choose. | - |
| menuTrigger | `focus` \| `input` \| `manual` | The action that causes the menu to open. | `focus` |
| labelPlacement | `inside` \| `outside` \| `outside-left` | The position of the label. | `inside` |
| selectedKey | `React.Key` | The currently selected key in the collection (controlled). | - |
| defaultSelectedKey | `React.Key` | The initial selected key in the collection (uncontrolled). | - |
| disabledKeys | `all` \| `React.Key[]` | The item keys that are disabled. These items cannot be selected, focused, or otherwise interacted with. | - |
| errorMessage | `ReactNode` \| `((v: ValidationResult) => ReactNode)` | An error message to display below the field. | - |
| validate | `(value: { inputValue: string, selectedKey: React.Key }) => ValidationError true null undefined` | Validate input values when committing (e.g. on blur), and return error messages for invalid values. | - |
| validationBehavior | `native` \| `aria` | Whether to use native HTML form validation to prevent form submission when the value is missing or invalid, or mark the field as required or invalid via ARIA.| `aria` |
| startContent | `ReactNode` | Element to be rendered in the left side of the Autocomplete. | - |
| endContent | `ReactNode` | Element to be rendered in the right side of the Autocomplete. | - |
| autoFocus | `boolean` | Whether the Autocomplete should be focused on render. | `false` |
| defaultFilter | `(textValue: string, inputValue: string) => boolean` | The filter function used to determine if a option should be included in the Autocomplete list. | - |
| filterOptions | [Intl.CollatorOptions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Collator/Collator) | The options used to create the collator used for filtering. | `{ sensitivity: 'base'}` |
| isReadOnly | `boolean` | Whether the Autocomplete is read only. | `false` |
| isRequired | `boolean` | Whether the Autocomplete is required. | `false` |
| isInvalid | `boolean` | Whether the Autocomplete is invalid. | `false` |
| isDisabled | `boolean` | Whether the Autocomplete is disabled. | `false` |
| fullWidth | `boolean` | Whether the input should take up the width of its parent. | `true` |
| selectorIcon | `ReactNode` | The icon that represents the autocomplete open state. Usually a chevron icon. | - |
| clearIcon | `ReactNode` | The icon to be used in the clear button. Usually a cross icon. | - |
| showScrollIndicators | `boolean` | Whether the scroll indicators should be shown when the listbox is scrollable. | `true` |
| scrollRef | `React.RefObject<HTMLElement>` | A ref to the scrollable element. | - |
| inputProps | [InputProps](/docs/components/input#api) | Props to be passed to the Input component. | - |
| popoverProps | [PopoverProps](/docs/components/popover#api) | Props to be passed to the Popover component. | - |
| listboxProps | [ListboxProps](/docs/components/listbox#api) | Props to be passed to the Listbox component. | - |
| scrollShadowProps | [ScrollShadowProps](/docs/components/scroll-shadow#api) | Props to be passed to the ScrollShadow component. | - |
| selectorButtonProps | [ButtonProps](/docs/components/button#api) | Props to be passed to the selector button. | - |
| clearButtonProps | [ButtonProps](/docs/components/button#api) | Props to be passed to the clear button. | - |
| isClearable | `boolean` | Whether the clear button should be shown. | `true` |
| disableClearable | `boolean` | Whether the clear button should be hidden. (**Deprecated**) Use `isClearable` instead. | `false` |
| disableAnimation | `boolean` | Whether the Autocomplete should be animated. | `true` |
| disableSelectorIconRotation | `boolean` | Whether the select should disable the rotation of the selector icon. | `false` |
| classNames | `Record<"base" "listboxWrapper" "listbox" "popoverContent" "endContentWrapper" "clearButton" "selectorButton", string>` | Allows to set custom class names for the Autocomplete slots. | - |
| Attribute | Type | Description | Default |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
| children\* | `ReactNode[]` | The children to render. Usually a list of `AutocompleteItem` and `AutocompleteSection` elements. | - |
| label | `ReactNode` | The content to display as the label. | - |
| name | `string` | The name of the input element, used when submitting an HTML form. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefname). | - |
| variant | `flat` \| `bordered` \| `faded` \| `underlined` | The variant of the Autocomplete. | `flat` |
| color | `default` \| `primary` \| `secondary` \| `success` \| `warning` \| `danger` | The color of the Autocomplete. | `default` |
| size | `sm` \| `md` \| `lg` | The size of the Autocomplete. | `md` |
| radius | `none` \| `sm` \| `md` \| `lg` \| `full` | The radius of the Autocomplete. | - |
| items | [`Iterable<T>`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) | The list of Autocomplete items. (controlled) | - |
| defaultItems | [`Iterable<T>`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) | The list of Autocomplete items (uncontrolled). | - |
| inputValue | `string` | The value of the Autocomplete input (controlled). | - |
| defaultInputValue | `string` | The value of the Autocomplete input (uncontrolled). | - |
| allowsCustomValue | `boolean` | Whether the Autocomplete allows a non-item matching input value to be set. | `false` |
| allowsEmptyCollection | `boolean` | Whether the autocomplete allows the menu to be open when the collection is empty. | `true` |
| shouldCloseOnBlur | `boolean` | Whether the Autocomplete should close when the input is blurred. | `true` |
| placeholder | `string` | Temporary text that occupies the text input when it is empty. | - |
| description | `ReactNode` | A description for the field. Provides a hint such as specific requirements for what to choose. | - |
| menuTrigger | `focus` \| `input` \| `manual` | The action that causes the menu to open. | `focus` |
| labelPlacement | `inside` \| `outside` \| `outside-left` | The position of the label. | `inside` |
| selectedKey | `React.Key` | The currently selected key in the collection (controlled). | - |
| defaultSelectedKey | `React.Key` | The initial selected key in the collection (uncontrolled). | - |
| disabledKeys | `all` \| `React.Key[]` | The item keys that are disabled. These items cannot be selected, focused, or otherwise interacted with. | - |
| errorMessage | `ReactNode` \| `((v: ValidationResult) => ReactNode)` | An error message to display below the field. | - |
| validate | `(value: { inputValue: string, selectedKey: React.Key }) => ValidationError true null undefined` | Validate input values when committing (e.g. on blur), and return error messages for invalid values. | - |
| validationBehavior | `native` \| `aria` | Whether to use native HTML form validation to prevent form submission when the value is missing or invalid, or mark the field as required or invalid via ARIA. | `aria` |
| startContent | `ReactNode` | Element to be rendered in the left side of the Autocomplete. | - |
| endContent | `ReactNode` | Element to be rendered in the right side of the Autocomplete. | - |
| autoFocus | `boolean` | Whether the Autocomplete should be focused on render. | `false` |
| defaultFilter | `(textValue: string, inputValue: string) => boolean` | The filter function used to determine if a option should be included in the Autocomplete list. | - |
| filterOptions | [Intl.CollatorOptions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Collator/Collator) | The options used to create the collator used for filtering. | `{ sensitivity: 'base'}` |
| maxListboxHeight | `number` | The maximum height of the listbox in pixels. Required when using virtualization. | `256` |
| itemHeight | `number` | The fixed height of each item in pixels. Required when using virtualization. | `32` |
| isVirtualized | `boolean` | Whether to enable virtualization. By default, it's enabled when the number of items exceeds 50. | `undefined` |
| isReadOnly | `boolean` | Whether the Autocomplete is read only. | `false` |
| isRequired | `boolean` | Whether the Autocomplete is required. | `false` |
| isInvalid | `boolean` | Whether the Autocomplete is invalid. | `false` |
| isDisabled | `boolean` | Whether the Autocomplete is disabled. | `false` |
| fullWidth | `boolean` | Whether the input should take up the width of its parent. | `true` |
| selectorIcon | `ReactNode` | The icon that represents the autocomplete open state. Usually a chevron icon. | - |
| clearIcon | `ReactNode` | The icon to be used in the clear button. Usually a cross icon. | - |
| showScrollIndicators | `boolean` | Whether the scroll indicators should be shown when the listbox is scrollable. | `true` |
| scrollRef | `React.RefObject<HTMLElement>` | A ref to the scrollable element. | - |
| inputProps | [InputProps](/docs/components/input#api) | Props to be passed to the Input component. | - |
| popoverProps | [PopoverProps](/docs/components/popover#api) | Props to be passed to the Popover component. | - |
| listboxProps | [ListboxProps](/docs/components/listbox#api) | Props to be passed to the Listbox component. | - |
| scrollShadowProps | [ScrollShadowProps](/docs/components/scroll-shadow#api) | Props to be passed to the ScrollShadow component. | - |
| selectorButtonProps | [ButtonProps](/docs/components/button#api) | Props to be passed to the selector button. | - |
| clearButtonProps | [ButtonProps](/docs/components/button#api) | Props to be passed to the clear button. | - |
| isClearable | `boolean` | Whether the clear button should be shown. | `true` |
| disableClearable | `boolean` | Whether the clear button should be hidden. (**Deprecated**) Use `isClearable` instead. | `false` |
| disableAnimation | `boolean` | Whether the Autocomplete should be animated. | `true` |
| disableSelectorIconRotation | `boolean` | Whether the select should disable the rotation of the selector icon. | `false` |
| classNames | `Record<"base" "listboxWrapper" "listbox" "popoverContent" "endContentWrapper" "clearButton" "selectorButton", string>` | Allows to set custom class names for the Autocomplete slots. | - |
### Autocomplete Events
@@ -1,5 +1,19 @@
# @nextui-org/autocomplete
## 2.2.0-beta.11
### Patch Changes
- [#4094](https://github.com/nextui-org/nextui/pull/4094) [`b9d5d4925`](https://github.com/nextui-org/nextui/commit/b9d5d492519778a4bf071748ec9f2b4e25d8373f) Thanks [@jrgarciadev](https://github.com/jrgarciadev)! - Virtualization support added to Listbox & Autocomplete
- Updated dependencies [[`b9d5d4925`](https://github.com/nextui-org/nextui/commit/b9d5d492519778a4bf071748ec9f2b4e25d8373f)]:
- @nextui-org/listbox@2.2.0-beta.8
- @nextui-org/button@2.1.0-beta.7
- @nextui-org/input@2.3.0-beta.9
- @nextui-org/popover@2.2.0-beta.8
- @nextui-org/scroll-shadow@2.2.0-beta.7
- @nextui-org/spinner@2.1.0-beta.7
## 2.2.0-beta.10
### Patch Changes
@@ -1,6 +1,6 @@
{
"name": "@nextui-org/autocomplete",
"version": "2.2.0-beta.10",
"version": "2.2.0-beta.11",
"description": "An autocomplete combines a text input with a listbox, allowing users to filter a list of options to items matching a query.",
"keywords": [
"autocomplete"
@@ -110,13 +110,30 @@ interface Props<T> extends Omit<HTMLNextUIProps<"input">, keyof ComboBoxProps<T>
* Callback fired when the select menu is closed.
*/
onClose?: () => void;
/**
* Whether to enable virtualization of the listbox items.
* By default, virtualization is automatically enabled when the number of items is greater than 50.
* @default undefined
*/
isVirtualized?: boolean;
}
export type UseAutocompleteProps<T> = Props<T> &
Omit<InputProps, "children" | "value" | "isClearable" | "defaultValue" | "classNames"> &
ComboBoxProps<T> &
AsyncLoadable &
AutocompleteVariantProps;
AutocompleteVariantProps & {
/**
* The height of each item in the listbox.
* This is required for virtualized listboxes to calculate the height of each item.
*/
itemHeight?: number;
/**
* The max height of the listbox (which will be rendered in a popover).
* This is required for virtualized listboxes to set the maximum height of the listbox.
*/
maxListboxHeight?: number;
};
export function useAutocomplete<T extends object>(originalProps: UseAutocompleteProps<T>) {
const globalContext = useProviderContext();
@@ -158,6 +175,9 @@ export function useAutocomplete<T extends object>(originalProps: UseAutocomplete
clearButtonProps = {},
showScrollIndicators = true,
allowsCustomValue = false,
isVirtualized,
maxListboxHeight = 256,
itemHeight = 32,
validationBehavior = globalContext?.validationBehavior ?? "aria",
className,
classNames,
@@ -425,14 +445,25 @@ export function useAutocomplete<T extends object>(originalProps: UseAutocomplete
onClick: chain(slotsProps.inputProps.onClick, otherProps.onClick),
} as unknown as InputProps);
const getListBoxProps = () =>
({
const getListBoxProps = () => {
// Use isVirtualized prop if defined, otherwise fallback to default behavior
const shouldVirtualize = isVirtualized ?? state.collection.size > 50;
return {
state,
ref: listBoxRef,
isVirtualized: shouldVirtualize,
virtualization: shouldVirtualize
? {
maxListboxHeight,
itemHeight,
}
: undefined,
...mergeProps(slotsProps.listboxProps, listBoxProps, {
shouldHighlightOnFocus: true,
}),
} as ListboxProps);
} as ListboxProps;
};
const getPopoverProps = (props: DOMAttributes = {}) => {
const popoverProps = mergeProps(slotsProps.popoverProps, props);
@@ -479,6 +510,9 @@ export function useAutocomplete<T extends object>(originalProps: UseAutocomplete
props?.className,
),
}),
style: {
maxHeight: originalProps.maxListboxHeight ?? 256,
},
});
const getEndContentWrapperProps: PropGetter = (props: any = {}) => ({
@@ -100,6 +100,58 @@ const items = animalsData.map((item) => (
</AutocompleteItem>
));
interface LargeDatasetSchema {
label: string;
value: string;
description: string;
}
function generateLargeDataset(n: number): LargeDatasetSchema[] {
const dataset: LargeDatasetSchema[] = [];
const items = [
"Cat",
"Dog",
"Elephant",
"Lion",
"Tiger",
"Giraffe",
"Dolphin",
"Penguin",
"Zebra",
"Shark",
"Whale",
"Otter",
"Crocodile",
];
for (let i = 0; i < n; i++) {
const item = items[i % items.length];
dataset.push({
label: `${item}${i}`,
value: `${item.toLowerCase()}${i}`,
description: "Sample description",
});
}
return dataset;
}
const LargeDatasetTemplate = (args: AutocompleteProps & {numItems: number}) => {
const largeDataset = generateLargeDataset(args.numItems);
return (
<Autocomplete label={`Search from ${args.numItems} items`} {...args}>
{largeDataset.map((item, index) => (
<AutocompleteItem key={index} value={item.value}>
{item.label}
</AutocompleteItem>
))}
</Autocomplete>
);
};
const Template = (args: AutocompleteProps) => (
<Autocomplete label="Favorite Animal" {...args}>
<AutocompleteItem key="red_panda">Red Panda</AutocompleteItem>
@@ -1061,3 +1113,42 @@ export const FullyControlled = {
...defaultProps,
},
};
export const OneThousandList = {
render: LargeDatasetTemplate,
args: {
...defaultProps,
placeholder: "Search...",
numItems: 1000,
},
};
export const TenThousandList = {
render: LargeDatasetTemplate,
args: {
...defaultProps,
placeholder: "Search...",
numItems: 10000,
},
};
export const CustomMaxListboxHeight = {
render: LargeDatasetTemplate,
args: {
...defaultProps,
placeholder: "Search...",
numItems: 1000,
maxListboxHeight: 400,
},
};
export const CustomItemHeight = {
render: LargeDatasetTemplate,
args: {
...defaultProps,
placeholder: "Search...",
numItems: 1000,
maxListboxHeight: 400,
itemHeight: 40,
},
};
+9
View File
@@ -1,5 +1,14 @@
# @nextui-org/listbox
## 2.2.0-beta.8
### Patch Changes
- [#4094](https://github.com/nextui-org/nextui/pull/4094) [`b9d5d4925`](https://github.com/nextui-org/nextui/commit/b9d5d492519778a4bf071748ec9f2b4e25d8373f) Thanks [@jrgarciadev](https://github.com/jrgarciadev)! - Virtualization support added to Listbox & Autocomplete
- Updated dependencies []:
- @nextui-org/divider@2.1.0-beta.7
## 2.2.0-beta.7
### Patch Changes
+10 -9
View File
@@ -1,6 +1,6 @@
{
"name": "@nextui-org/listbox",
"version": "2.2.0-beta.7",
"version": "2.2.0-beta.8",
"description": "A listbox displays a list of options and allows a user to select one or more of them.",
"keywords": [
"listbox"
@@ -40,11 +40,12 @@
"@nextui-org/system": ">=2.3.0-beta.0"
},
"dependencies": {
"@nextui-org/aria-utils": "workspace:*",
"@nextui-org/divider": "workspace:*",
"@nextui-org/react-utils": "workspace:*",
"@nextui-org/shared-utils": "workspace:*",
"@nextui-org/divider": "workspace:*",
"@nextui-org/aria-utils": "workspace:*",
"@nextui-org/use-is-mobile": "workspace:*",
"@tanstack/react-virtual": "^3.10.9",
"@react-aria/utils": "3.25.2",
"@react-aria/listbox": "3.13.3",
"@react-stately/list": "3.10.8",
@@ -54,14 +55,14 @@
"@react-types/shared": "3.24.1"
},
"devDependencies": {
"@nextui-org/theme": "workspace:*",
"@nextui-org/system": "workspace:*",
"clean-package": "2.2.0",
"@nextui-org/avatar": "workspace:*",
"@nextui-org/chip": "workspace:*",
"@nextui-org/scroll-shadow": "workspace:*",
"@nextui-org/shared-icons": "workspace:*",
"@nextui-org/stories-utils": "workspace:*",
"@nextui-org/scroll-shadow": "workspace:*",
"@nextui-org/chip": "workspace:*",
"@nextui-org/avatar": "workspace:*",
"@nextui-org/system": "workspace:*",
"@nextui-org/theme": "workspace:*",
"clean-package": "2.2.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
+22 -3
View File
@@ -2,13 +2,26 @@ import {ForwardedRef, ReactElement, Ref} from "react";
import {forwardRef} from "@nextui-org/system";
import {mergeProps} from "@react-aria/utils";
import {UseListboxProps, useListbox} from "./use-listbox";
import {UseListboxProps, UseListboxReturn, useListbox} from "./use-listbox";
import ListboxSection from "./listbox-section";
import ListboxItem from "./listbox-item";
import VirtualizedListbox from "./virtualized-listbox";
interface Props<T> extends UseListboxProps<T> {}
export interface VirtualizationProps {
maxListboxHeight: number;
itemHeight: number;
}
interface Props<T> extends UseListboxProps<T> {
isVirtualized?: boolean;
virtualization?: VirtualizationProps;
}
function Listbox<T extends object>(props: Props<T>, ref: ForwardedRef<HTMLUListElement>) {
const {isVirtualized, ...restProps} = props;
const useListboxProps = useListbox<T>({...restProps, ref});
const {
Component,
state,
@@ -24,7 +37,13 @@ function Listbox<T extends object>(props: Props<T>, ref: ForwardedRef<HTMLUListE
disableAnimation,
getEmptyContentProps,
getListProps,
} = useListbox<T>({...props, ref});
} = useListboxProps;
if (isVirtualized) {
return (
<VirtualizedListbox {...(props as Props<T>)} {...(useListboxProps as UseListboxReturn)} />
);
}
const content = (
<Component {...getListProps()}>
@@ -0,0 +1,159 @@
import {ReactElement, useRef} from "react";
import {forwardRef} from "@nextui-org/system";
import {mergeProps} from "@react-aria/utils";
import {useVirtualizer} from "@tanstack/react-virtual";
import {isEmpty} from "@nextui-org/shared-utils";
import ListboxItem from "./listbox-item";
import ListboxSection from "./listbox-section";
import {VirtualizationProps} from "./listbox";
import {UseListboxReturn} from "./use-listbox";
interface Props extends UseListboxReturn {
isVirtualized?: boolean;
virtualization?: VirtualizationProps;
}
function VirtualizedListbox(props: Props) {
const {
Component,
state,
color,
variant,
itemClasses,
getBaseProps,
topContent,
bottomContent,
hideEmptyContent,
hideSelectedIcon,
shouldHighlightOnFocus,
disableAnimation,
getEmptyContentProps,
getListProps,
} = props;
const {virtualization} = props;
if (
!virtualization ||
(!isEmpty(virtualization) && !virtualization.maxListboxHeight && !virtualization.itemHeight)
) {
throw new Error(
"You are using a virtualized listbox. VirtualizedListbox requires 'virtualization' props with 'maxListboxHeight' and 'itemHeight' properties. This error might have originated from autocomplete components that use VirtualizedListbox. Please provide these props to use the virtualized listbox.",
);
}
const {maxListboxHeight, itemHeight} = virtualization;
const listHeight = Math.min(maxListboxHeight, itemHeight * state.collection.size);
const parentRef = useRef(null);
const rowVirtualizer = useVirtualizer({
count: state.collection.size,
getScrollElement: () => parentRef.current,
estimateSize: () => itemHeight,
});
const virtualItems = rowVirtualizer.getVirtualItems();
const renderRow = ({
index,
style: virtualizerStyle,
}: {
index: number;
style: React.CSSProperties;
}) => {
const item = [...state.collection][index];
const itemProps = {
color,
item,
state,
variant,
disableAnimation,
hideSelectedIcon,
...item.props,
};
if (item.type === "section") {
return (
<ListboxSection
key={item.key}
{...itemProps}
itemClasses={itemClasses}
style={{...virtualizerStyle, ...itemProps.style}}
/>
);
}
let listboxItem = (
<ListboxItem
key={item.key}
{...itemProps}
classNames={mergeProps(itemClasses, item.props?.classNames)}
shouldHighlightOnFocus={shouldHighlightOnFocus}
style={{...virtualizerStyle, ...itemProps.style}}
/>
);
if (item.wrapper) {
listboxItem = item.wrapper(listboxItem);
}
return listboxItem;
};
const content = (
<Component {...getListProps()}>
{!state.collection.size && !hideEmptyContent && (
<li>
<div {...getEmptyContentProps()} />
</li>
)}
<div
ref={parentRef}
style={{
height: maxListboxHeight,
overflow: "auto",
}}
>
{listHeight > 0 && itemHeight > 0 && (
<div
style={{
height: `${rowVirtualizer.getTotalSize()}px`,
width: "100%",
position: "relative",
}}
>
{virtualItems.map((virtualItem) =>
renderRow({
index: virtualItem.index,
style: {
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
},
}),
)}
</div>
)}
</div>
</Component>
);
return (
<div {...getBaseProps()}>
{topContent}
{content}
{bottomContent}
</div>
);
}
VirtualizedListbox.displayName = "NextUI.VirtualizedListbox";
// forwardRef doesn't support generic parameters, so cast the result to the correct type
export default forwardRef(VirtualizedListbox) as (props: Props) => ReactElement;
+10
View File
@@ -1,5 +1,15 @@
# @nextui-org/select
## 2.3.0-beta.11
### Patch Changes
- Updated dependencies [[`b9d5d4925`](https://github.com/nextui-org/nextui/commit/b9d5d492519778a4bf071748ec9f2b4e25d8373f)]:
- @nextui-org/listbox@2.2.0-beta.8
- @nextui-org/popover@2.2.0-beta.8
- @nextui-org/scroll-shadow@2.2.0-beta.7
- @nextui-org/spinner@2.1.0-beta.7
## 2.3.0-beta.10
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@nextui-org/select",
"version": "2.3.0-beta.10",
"version": "2.3.0-beta.11",
"description": "A select displays a collapsible list of options and allows a user to select one of them.",
"keywords": [
"select"
+49
View File
@@ -1,5 +1,54 @@
# @nextui-org/react
## 2.5.0-beta.17
### Patch Changes
- Updated dependencies [[`b9d5d4925`](https://github.com/nextui-org/nextui/commit/b9d5d492519778a4bf071748ec9f2b4e25d8373f)]:
- @nextui-org/autocomplete@2.2.0-beta.11
- @nextui-org/listbox@2.2.0-beta.8
- @nextui-org/theme@2.3.0-beta.12
- @nextui-org/select@2.3.0-beta.11
- @nextui-org/accordion@2.1.0-beta.8
- @nextui-org/alert@2.1.0-beta.10
- @nextui-org/avatar@2.1.0-beta.10
- @nextui-org/badge@2.1.0-beta.7
- @nextui-org/breadcrumbs@2.1.0-beta.8
- @nextui-org/button@2.1.0-beta.7
- @nextui-org/calendar@2.1.0-beta.8
- @nextui-org/card@2.1.0-beta.7
- @nextui-org/checkbox@2.2.0-beta.7
- @nextui-org/chip@2.1.0-beta.8
- @nextui-org/code@2.1.0-beta.7
- @nextui-org/date-input@2.2.0-beta.7
- @nextui-org/date-picker@2.2.0-beta.9
- @nextui-org/divider@2.1.0-beta.7
- @nextui-org/drawer@2.1.0-beta.9
- @nextui-org/dropdown@2.2.0-beta.8
- @nextui-org/image@2.1.0-beta.10
- @nextui-org/input@2.3.0-beta.9
- @nextui-org/kbd@2.1.0-beta.7
- @nextui-org/link@2.1.0-beta.8
- @nextui-org/menu@2.1.0-beta.7
- @nextui-org/modal@2.1.0-beta.9
- @nextui-org/navbar@2.1.0-beta.7
- @nextui-org/pagination@2.1.0-beta.8
- @nextui-org/popover@2.2.0-beta.8
- @nextui-org/progress@2.1.0-beta.7
- @nextui-org/radio@2.2.0-beta.7
- @nextui-org/ripple@2.1.0-beta.7
- @nextui-org/scroll-shadow@2.2.0-beta.7
- @nextui-org/skeleton@2.1.0-beta.7
- @nextui-org/slider@2.3.0-beta.7
- @nextui-org/snippet@2.1.0-beta.8
- @nextui-org/spacer@2.1.0-beta.7
- @nextui-org/spinner@2.1.0-beta.7
- @nextui-org/switch@2.1.0-beta.7
- @nextui-org/table@2.1.0-beta.8
- @nextui-org/tabs@2.1.0-beta.7
- @nextui-org/tooltip@2.1.0-beta.7
- @nextui-org/user@2.1.0-beta.10
## 2.5.0-beta.16
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@nextui-org/react",
"version": "2.5.0-beta.16",
"version": "2.5.0-beta.17",
"description": "🚀 Beautiful and modern React UI library.",
"author": "Junior Garcia <jrgarciadev@gmail.com>",
"homepage": "https://nextui.org",
+6
View File
@@ -1,5 +1,11 @@
# @nextui-org/theme
## 2.3.0-beta.12
### Patch Changes
- [#4094](https://github.com/nextui-org/nextui/pull/4094) [`b9d5d4925`](https://github.com/nextui-org/nextui/commit/b9d5d492519778a4bf071748ec9f2b4e25d8373f) Thanks [@jrgarciadev](https://github.com/jrgarciadev)! - Virtualization support added to Listbox & Autocomplete
## 2.3.0-beta.11
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@nextui-org/theme",
"version": "2.3.0-beta.11",
"version": "2.3.0-beta.12",
"description": "The default theme for NextUI components",
"keywords": [
"theme",
@@ -5,7 +5,7 @@ import {tv} from "../utils/tv";
const autocomplete = tv({
slots: {
base: "group inline-flex flex-column w-full",
listboxWrapper: "scroll-py-6 max-h-64 w-full",
listboxWrapper: "scroll-py-6 w-full",
listbox: "",
popoverContent: "w-full p-1 overflow-hidden",
endContentWrapper: "relative flex h-full items-center -mr-2",
+20
View File
@@ -1783,6 +1783,9 @@ importers:
'@react-types/shared':
specifier: 3.24.1
version: 3.24.1(react@18.3.1)
'@tanstack/react-virtual':
specifier: ^3.10.9
version: 3.10.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
devDependencies:
'@nextui-org/avatar':
specifier: workspace:*
@@ -7524,6 +7527,15 @@ packages:
peerDependencies:
tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20'
'@tanstack/react-virtual@3.10.9':
resolution: {integrity: sha512-OXO2uBjFqA4Ibr2O3y0YMnkrRWGVNqcvHQXmGvMu6IK8chZl3PrDxFXdGZ2iZkSrKh3/qUYoFqYe+Rx23RoU0g==}
peerDependencies:
react: ^18.2.0
react-dom: ^18.2.0
'@tanstack/virtual-core@3.10.9':
resolution: {integrity: sha512-kBknKOKzmeR7lN+vSadaKWXaLS0SZZG+oqpQ/k80Q6g9REn6zRHS/ZYdrIzHnpHgy/eWs00SujveUN/GJT2qTw==}
'@testing-library/dom@10.4.0':
resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==}
engines: {node: '>=18'}
@@ -21187,6 +21199,14 @@ snapshots:
postcss-selector-parser: 6.0.10
tailwindcss: 3.4.15(ts-node@10.9.2(@swc/core@1.9.2(@swc/helpers@0.5.15))(@types/node@20.2.5)(typescript@5.6.3))
'@tanstack/react-virtual@3.10.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@tanstack/virtual-core': 3.10.9
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@tanstack/virtual-core@3.10.9': {}
'@testing-library/dom@10.4.0':
dependencies:
'@babel/code-frame': 7.26.2