diff --git a/.changeset/dull-coats-crash.md b/.changeset/dull-coats-crash.md new file mode 100644 index 0000000000..ea1827a871 --- /dev/null +++ b/.changeset/dull-coats-crash.md @@ -0,0 +1,9 @@ +--- +"@nextui-org/dropdown": patch +"@nextui-org/listbox": patch +"@nextui-org/popover": patch +"@nextui-org/modal": patch +"@nextui-org/menu": patch +--- + +Fix typecheck diff --git a/.changeset/ninety-mails-yell.md b/.changeset/ninety-mails-yell.md new file mode 100644 index 0000000000..ab7d6e5c84 --- /dev/null +++ b/.changeset/ninety-mails-yell.md @@ -0,0 +1,6 @@ +--- +"@nextui-org/menu": patch +"@nextui-org/theme": patch +--- + +Fix should not export list item internal variables type diff --git a/.changeset/ten-experts-happen.md b/.changeset/ten-experts-happen.md new file mode 100644 index 0000000000..844749c01d --- /dev/null +++ b/.changeset/ten-experts-happen.md @@ -0,0 +1,7 @@ +--- +"@nextui-org/listbox": patch +"@nextui-org/menu": patch +"@nextui-org/theme": patch +--- + +Virtualization support added to Listbox and Menu diff --git a/apps/docs/components/docs/components/code-demo/react-live-demo.tsx b/apps/docs/components/docs/components/code-demo/react-live-demo.tsx index 48e74a5aaa..527728e6ef 100644 --- a/apps/docs/components/docs/components/code-demo/react-live-demo.tsx +++ b/apps/docs/components/docs/components/code-demo/react-live-demo.tsx @@ -5,6 +5,7 @@ import * as Components from "@nextui-org/react"; import * as intlDateUtils from "@internationalized/date"; import * as reactAriaI18n from "@react-aria/i18n"; import * as reactHookForm from "react-hook-form"; +import {SandpackFiles} from "@codesandbox/sandpack-react/types"; import {BgGridContainer} from "@/components/bg-grid-container"; import {GradientBox, GradientBoxProps} from "@/components/gradient-box"; @@ -12,7 +13,7 @@ import {CopyButton} from "@/components/copy-button"; export interface ReactLiveDemoProps { code: string; - files: string[]; + files: SandpackFiles; noInline?: boolean; height?: string | number; isCentered?: boolean; @@ -47,7 +48,7 @@ export const ReactLiveDemo: React.FC = ({
)} diff --git a/apps/docs/config/routes.json b/apps/docs/config/routes.json index 5cc00218ca..4356cba05a 100644 --- a/apps/docs/config/routes.json +++ b/apps/docs/config/routes.json @@ -302,7 +302,8 @@ "key": "listbox", "title": "Listbox", "keywords": "listbox, selection, option list, multiple choice", - "path": "/docs/components/listbox.mdx" + "path": "/docs/components/listbox.mdx", + "updated": true }, { "key": "modal", diff --git a/apps/docs/content/components/listbox/index.ts b/apps/docs/content/components/listbox/index.ts index 35c8dcd9c4..2f83ec0bd9 100644 --- a/apps/docs/content/components/listbox/index.ts +++ b/apps/docs/content/components/listbox/index.ts @@ -9,6 +9,8 @@ import description from "./description"; import sections from "./sections"; import customStyles from "./custom-styles"; import topContent from "./top-content"; +import virtualization from "./virtualization"; +import virtualizationTenThousand from "./virtualization-ten-thousand"; export const listboxContent = { usage, @@ -22,4 +24,6 @@ export const listboxContent = { sections, customStyles, topContent, + virtualization, + virtualizationTenThousand, }; diff --git a/apps/docs/content/components/listbox/virtualization-ten-thousand.raw.jsx b/apps/docs/content/components/listbox/virtualization-ten-thousand.raw.jsx new file mode 100644 index 0000000000..f569bea6cc --- /dev/null +++ b/apps/docs/content/components/listbox/virtualization-ten-thousand.raw.jsx @@ -0,0 +1,57 @@ +import {Listbox, ListboxItem} 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 ( +
+ + {items.map((item, index) => ( + + {item.label} + + ))} + +
+ ); +} diff --git a/apps/docs/content/components/listbox/virtualization-ten-thousand.ts b/apps/docs/content/components/listbox/virtualization-ten-thousand.ts new file mode 100644 index 0000000000..1b8e486cb5 --- /dev/null +++ b/apps/docs/content/components/listbox/virtualization-ten-thousand.ts @@ -0,0 +1,9 @@ +import App from "./virtualization-ten-thousand.raw.jsx?raw"; + +const react = { + "/App.jsx": App, +}; + +export default { + ...react, +}; diff --git a/apps/docs/content/components/listbox/virtualization.raw.jsx b/apps/docs/content/components/listbox/virtualization.raw.jsx new file mode 100644 index 0000000000..96ec2011b8 --- /dev/null +++ b/apps/docs/content/components/listbox/virtualization.raw.jsx @@ -0,0 +1,56 @@ +import {Listbox, ListboxItem} 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 ( +
+ + {items.map((item, index) => ( + + {item.label} + + ))} + +
+ ); +} diff --git a/apps/docs/content/components/listbox/virtualization.ts b/apps/docs/content/components/listbox/virtualization.ts new file mode 100644 index 0000000000..e40cbd641f --- /dev/null +++ b/apps/docs/content/components/listbox/virtualization.ts @@ -0,0 +1,9 @@ +import App from "./virtualization.raw.jsx?raw"; + +const react = { + "/App.jsx": App, +}; + +export default { + ...react, +}; diff --git a/apps/docs/content/docs/components/listbox.mdx b/apps/docs/content/docs/components/listbox.mdx index 2c8eb66fc1..52f9b92575 100644 --- a/apps/docs/content/docs/components/listbox.mdx +++ b/apps/docs/content/docs/components/listbox.mdx @@ -151,6 +151,22 @@ function App() { } ``` +### Virtualization + +Select supports virtualization, which allows efficient rendering of large lists by only rendering items that are visible in the viewport. You can enable virtualization by setting the `isVirtualized` prop to `true`. + + + +> **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 + +Here's an example of using virtualization with 10,000 items. + + + ## Slots Listbox has 3 components with slots the base one `Listbox`, `ListboxItem` and `ListboxSection` components. @@ -328,6 +344,18 @@ You can customize the `Listbox` items style by using the `itemClasses` prop and type: "boolean", description: "Whether keyboard navigation is circular.", default: "false" + }, + { + attribute: "isVirtualized", + type: "boolean", + description: "Whether to enable virtualization.", + default: "false" + }, + { + attribute: "virtualization", + type: "Record<\"maxListboxHeight\" & \"itemHeight\", number>", + description: "Configuration for virtualization, optimizing rendering for large datasets. Required if isVirtualized is set to true.", + default: "-", }, { attribute: "hideEmptyContent", diff --git a/packages/components/dropdown/src/dropdown-trigger.tsx b/packages/components/dropdown/src/dropdown-trigger.tsx index 467270085f..0157d2e25f 100644 --- a/packages/components/dropdown/src/dropdown-trigger.tsx +++ b/packages/components/dropdown/src/dropdown-trigger.tsx @@ -4,6 +4,8 @@ import {useDropdownContext} from "./dropdown-context"; export interface DropdownTriggerProps { children?: React.ReactNode; + className?: string; + [key: string]: any; } /** diff --git a/packages/components/listbox/src/base/listbox-item-base.tsx b/packages/components/listbox/src/base/listbox-item-base.tsx index 41af3b3e58..e5283f3ff1 100644 --- a/packages/components/listbox/src/base/listbox-item-base.tsx +++ b/packages/components/listbox/src/base/listbox-item-base.tsx @@ -90,7 +90,7 @@ interface Props extends Omit, "childre } export type ListboxItemBaseProps = Props & - ListboxItemVariantProps & + Omit & Omit & FocusableProps & PressEvents; diff --git a/packages/components/listbox/src/listbox-item.tsx b/packages/components/listbox/src/listbox-item.tsx index 8e9dc351e2..8a3f248de1 100644 --- a/packages/components/listbox/src/listbox-item.tsx +++ b/packages/components/listbox/src/listbox-item.tsx @@ -3,7 +3,8 @@ import {useMemo, ReactNode} from "react"; import {UseListboxItemProps, useListboxItem} from "./use-listbox-item"; import {ListboxSelectedIcon} from "./listbox-selected-icon"; -export interface ListboxItemProps extends UseListboxItemProps {} +export interface ListboxItemProps + extends Omit, "hasDescriptionTextChild" | "hasTitleTextChild"> {} /** * @internal diff --git a/packages/components/listbox/src/use-listbox-item.ts b/packages/components/listbox/src/use-listbox-item.ts index 43be909f3c..820b32637f 100644 --- a/packages/components/listbox/src/use-listbox-item.ts +++ b/packages/components/listbox/src/use-listbox-item.ts @@ -1,4 +1,5 @@ import type {ListboxItemBaseProps} from "./base/listbox-item-base"; +import type {MenuItemVariantProps} from "@nextui-org/theme"; import {useMemo, useRef, useCallback, Fragment} from "react"; import {listboxItem} from "@nextui-org/theme"; @@ -24,7 +25,8 @@ interface Props extends ListboxItemBaseProps { } export type UseListboxItemProps = Props & - Omit, keyof Props>; + Omit, keyof Props> & + MenuItemVariantProps; export function useListboxItem(originalProps: UseListboxItemProps) { const globalContext = useProviderContext(); diff --git a/packages/components/listbox/stories/listbox.stories.tsx b/packages/components/listbox/stories/listbox.stories.tsx index 9f24e947a6..c670e57621 100644 --- a/packages/components/listbox/stories/listbox.stories.tsx +++ b/packages/components/listbox/stories/listbox.stories.tsx @@ -679,6 +679,59 @@ const CustomWithClassNamesTemplate = ({color, variant, disableAnimation, ...args ); }; +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: ListboxProps & {numItems: number}) => { + const largeDataset = generateLargeDataset(args.numItems); + + return ( +
+ + {largeDataset.map((item, index) => ( + + {item.label} + + ))} + +
+ ); +}; + export const Default = { render: Template, args: { @@ -782,3 +835,55 @@ export const CustomWithClassNames = { ...defaultProps, }, }; + +export const OneThousandList = { + render: LargeDatasetTemplate, + args: { + ...defaultProps, + numItems: 1000, + isVirtualized: true, + virtualization: { + maxListboxHeight: 400, + itemHeight: 20, + }, + }, +}; + +export const TenThousandList = { + render: LargeDatasetTemplate, + args: { + ...defaultProps, + numItems: 10000, + isVirtualized: true, + virtualization: { + maxListboxHeight: 400, + itemHeight: 20, + }, + }, +}; + +export const CustomMaxListboxHeight = { + render: LargeDatasetTemplate, + args: { + ...defaultProps, + numItems: 1000, + isVirtualized: true, + virtualization: { + maxListboxHeight: 600, + itemHeight: 20, + }, + }, +}; + +export const CustomItemHeight = { + render: LargeDatasetTemplate, + args: { + ...defaultProps, + numItems: 1000, + isVirtualized: true, + virtualization: { + itemHeight: 40, + maxListboxHeight: 600, + }, + }, +}; diff --git a/packages/components/menu/src/base/menu-item-base.tsx b/packages/components/menu/src/base/menu-item-base.tsx index e1228b0e09..693ab6d53f 100644 --- a/packages/components/menu/src/base/menu-item-base.tsx +++ b/packages/components/menu/src/base/menu-item-base.tsx @@ -89,7 +89,7 @@ interface Props extends Omit, "childre } export type MenuItemBaseProps = Props & - MenuItemVariantProps & + Omit & AriaMenuItemProps & FocusableProps & PressEvents; diff --git a/packages/components/menu/src/menu-item.tsx b/packages/components/menu/src/menu-item.tsx index fa5a6f2c19..5e22e495a3 100644 --- a/packages/components/menu/src/menu-item.tsx +++ b/packages/components/menu/src/menu-item.tsx @@ -3,7 +3,8 @@ import {useMemo, ReactNode} from "react"; import {UseMenuItemProps, useMenuItem} from "./use-menu-item"; import {MenuSelectedIcon} from "./menu-selected-icon"; -export interface MenuItemProps extends UseMenuItemProps {} +export interface MenuItemProps + extends Omit, "hasDescriptionTextChild" | "hasTitleTextChild"> {} /** * @internal diff --git a/packages/components/menu/src/menu.tsx b/packages/components/menu/src/menu.tsx index bff90fb8d1..3a74d2e23c 100644 --- a/packages/components/menu/src/menu.tsx +++ b/packages/components/menu/src/menu.tsx @@ -74,10 +74,7 @@ function Menu(props: Props, ref: ForwardedRef = Omit< - Props, - "hasChildItems" | "hasTitleTextChild" | "hasDescriptionTextChild" -> & {ref?: Ref}; +export type MenuProps = Props & {ref?: Ref}; // forwardRef doesn't support generic parameters, so cast the result to the correct type export default forwardRef(Menu) as (props: MenuProps) => ReactElement; diff --git a/packages/components/menu/src/use-menu-item.ts b/packages/components/menu/src/use-menu-item.ts index 9ed4e31253..a150e8def2 100644 --- a/packages/components/menu/src/use-menu-item.ts +++ b/packages/components/menu/src/use-menu-item.ts @@ -1,4 +1,5 @@ import type {MenuItemBaseProps} from "./base/menu-item-base"; +import type {MenuItemVariantProps} from "@nextui-org/theme"; import type {Node} from "@react-types/shared"; import {useMemo, useRef, useCallback, Fragment} from "react"; @@ -24,7 +25,8 @@ interface Props extends MenuItemBaseProps { } export type UseMenuItemProps = Props & - Omit, keyof Props>; + Omit, keyof Props> & + MenuItemVariantProps; export function useMenuItem(originalProps: UseMenuItemProps) { const globalContext = useProviderContext(); diff --git a/packages/components/popover/src/popover-trigger.tsx b/packages/components/popover/src/popover-trigger.tsx index 16c6fe0b43..2a19f0ebf3 100644 --- a/packages/components/popover/src/popover-trigger.tsx +++ b/packages/components/popover/src/popover-trigger.tsx @@ -8,6 +8,8 @@ import {usePopoverContext} from "./popover-context"; export interface PopoverTriggerProps { children?: React.ReactNode; + className?: string; + [key: string]: any; } /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c65136c311..781634e8c3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22124,7 +22124,7 @@ snapshots: doctrine: 2.1.0 eslint: 7.32.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@2.7.1(eslint-plugin-import@2.31.0)(eslint@7.32.0))(eslint@7.32.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@7.32.0))(eslint@7.32.0) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3