From c603f0c48a015e0404221a6a360be4c916099af4 Mon Sep 17 00:00:00 2001 From: Andrey Yamanov Date: Thu, 2 Apr 2026 17:53:52 +0200 Subject: [PATCH 1/2] chore: improve docs about items prop --- .../fields/ComboBox/ComboBox.docs.mdx | 29 ++++++++ .../fields/ComboBox/ComboBox.stories.tsx | 37 ++++++++++ .../FilterListBox/FilterListBox.docs.mdx | 27 ++++++++ .../FilterListBox/FilterListBox.stories.tsx | 49 +++++++++++++ .../fields/FilterPicker/FilterPicker.docs.mdx | 34 +++++++++ .../FilterPicker/FilterPicker.stories.tsx | 69 +++++++++++++++++++ .../fields/ListBox/ListBox.docs.mdx | 50 +++++++++++--- .../fields/ListBox/ListBox.stories.tsx | 51 ++++++++++++++ src/components/fields/Picker/Picker.docs.mdx | 27 ++++++++ .../fields/Picker/Picker.stories.tsx | 44 ++++++++++++ src/components/fields/Select/Select.docs.mdx | 2 + 11 files changed, 411 insertions(+), 8 deletions(-) diff --git a/src/components/fields/ComboBox/ComboBox.docs.mdx b/src/components/fields/ComboBox/ComboBox.docs.mdx index 41b024e88..88c77b132 100644 --- a/src/components/fields/ComboBox/ComboBox.docs.mdx +++ b/src/components/fields/ComboBox/ComboBox.docs.mdx @@ -296,6 +296,33 @@ const [selectedKey, setSelectedKey] = useState(null); ``` +### With Sections (Dynamic) + + + +Sections and items can be defined dynamically using the `items` prop with a render function. Pass hierarchical data where each section contains a `children` array. + +```jsx +const categories = [ + { name: 'Fruits', children: [ + { key: 'apple', label: 'Apple' }, + { key: 'banana', label: 'Banana' }, + ]}, + { name: 'Vegetables', children: [ + { key: 'carrot', label: 'Carrot' }, + { key: 'broccoli', label: 'Broccoli' }, + ]}, +]; + + + {(category) => ( + + {(item) => {item.label}} + + )} + +``` + ### With Disabled Items @@ -442,6 +469,8 @@ const fruits = [ +For large datasets, use the `items` prop with a render function. This enables automatic virtualization — only visible items are rendered in the DOM, providing smooth scrolling even with thousands of items. Virtualization is disabled when sections are present. + ```jsx const items = Array.from({ length: 1000 }, (_, i) => ({ key: `item-${i}`, diff --git a/src/components/fields/ComboBox/ComboBox.stories.tsx b/src/components/fields/ComboBox/ComboBox.stories.tsx index ecc851758..e22681af8 100644 --- a/src/components/fields/ComboBox/ComboBox.stories.tsx +++ b/src/components/fields/ComboBox/ComboBox.stories.tsx @@ -516,6 +516,43 @@ export const WithSections = () => ( ); +export const DynamicSections = () => { + const categories = [ + { + name: 'Fruits', + children: [ + { key: 'apple', label: 'Apple' }, + { key: 'banana', label: 'Banana' }, + { key: 'cherry', label: 'Cherry' }, + ], + }, + { + name: 'Vegetables', + children: [ + { key: 'carrot', label: 'Carrot' }, + { key: 'broccoli', label: 'Broccoli' }, + { key: 'spinach', label: 'Spinach' }, + ], + }, + ]; + + return ( + + {(category: any) => ( + + {(item: any) => ( + {item.label} + )} + + )} + + ); +}; + export const WithDisabledItems = () => ( ``` +### With Sections (Dynamic) + + + +Sections and items can be defined dynamically using the `items` prop with a render function. Pass hierarchical data where each section contains a `children` array. + +```jsx +const categories = [ + { name: 'Fruits', children: [ + { key: 'apple', label: 'Apple' }, + { key: 'banana', label: 'Banana' }, + ]}, + { name: 'Vegetables', children: [ + { key: 'carrot', label: 'Carrot' }, + { key: 'broccoli', label: 'Broccoli' }, + ]}, +]; + + + {(category) => ( + + {(item) => {item.label}} + + )} + +``` + ### With Descriptions diff --git a/src/components/fields/FilterListBox/FilterListBox.stories.tsx b/src/components/fields/FilterListBox/FilterListBox.stories.tsx index 134864cf6..b90b37cc5 100644 --- a/src/components/fields/FilterListBox/FilterListBox.stories.tsx +++ b/src/components/fields/FilterListBox/FilterListBox.stories.tsx @@ -533,6 +533,55 @@ WithSections.args = { searchPlaceholder: 'Search ingredients...', }; +export const DynamicSections: StoryFn> = (args) => { + const categories = [ + { + name: 'Fruits', + children: [ + { key: 'apple', label: 'Apple' }, + { key: 'banana', label: 'Banana' }, + { key: 'cherry', label: 'Cherry' }, + ], + }, + { + name: 'Vegetables', + children: [ + { key: 'carrot', label: 'Carrot' }, + { key: 'broccoli', label: 'Broccoli' }, + { key: 'spinach', label: 'Spinach' }, + ], + }, + { + name: 'Herbs', + children: [ + { key: 'basil', label: 'Basil' }, + { key: 'cilantro', label: 'Cilantro' }, + { key: 'parsley', label: 'Parsley' }, + ], + }, + ]; + + return ( + + {(category: any) => ( + + {(item: any) => ( + {item.label} + )} + + )} + + ); +}; +DynamicSections.args = { + label: 'Choose an ingredient', + searchPlaceholder: 'Search ingredients...', +}; + export const WithSectionsAndDescriptions: StoryFn< CubeFilterListBoxProps > = (args) => ( diff --git a/src/components/fields/FilterPicker/FilterPicker.docs.mdx b/src/components/fields/FilterPicker/FilterPicker.docs.mdx index 9b9dcea8f..95d854280 100644 --- a/src/components/fields/FilterPicker/FilterPicker.docs.mdx +++ b/src/components/fields/FilterPicker/FilterPicker.docs.mdx @@ -242,6 +242,40 @@ For large datasets or dynamic content, use the `items` prop with a render functi ``` +### Dynamic Sections Pattern + +For hierarchical data with sections, use the `items` prop with nested render functions. Each section contains a `children` array of items. + +```jsx +const categories = [ + { name: 'Fruits', children: [ + { key: 'apple', label: 'Apple' }, + { key: 'banana', label: 'Banana' }, + ]}, + { name: 'Vegetables', children: [ + { key: 'carrot', label: 'Carrot' }, + { key: 'broccoli', label: 'Broccoli' }, + ]}, +]; + + + {(category) => ( + + {(item) => ( + + {item.label} + + )} + + )} + +``` + **Key Benefits:** - **Virtualization**: Automatically enabled for large lists without sections - **Performance**: Only renders visible items in the DOM diff --git a/src/components/fields/FilterPicker/FilterPicker.stories.tsx b/src/components/fields/FilterPicker/FilterPicker.stories.tsx index 5cd2a6c26..c08a68f87 100644 --- a/src/components/fields/FilterPicker/FilterPicker.stories.tsx +++ b/src/components/fields/FilterPicker/FilterPicker.stories.tsx @@ -686,6 +686,75 @@ export const WithSections: Story = { }, }; +export const DynamicSections: Story = { + args: { + label: 'Organized by Sections', + placeholder: 'Choose items...', + selectionMode: 'multiple', + searchPlaceholder: 'Search ingredients...', + width: 'max 30x', + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const trigger = canvas.getByRole('button'); + await userEvent.click(trigger); + }, + render: (args) => { + const categories = [ + { + name: 'Fruits', + children: [ + { key: 'apple', label: 'Apple' }, + { key: 'banana', label: 'Banana' }, + { key: 'cherry', label: 'Cherry' }, + ], + }, + { + name: 'Vegetables', + children: [ + { key: 'carrot', label: 'Carrot' }, + { key: 'broccoli', label: 'Broccoli' }, + { key: 'spinach', label: 'Spinach' }, + ], + }, + { + name: 'Grains', + children: [ + { key: 'rice', label: 'Rice' }, + { key: 'oats', label: 'Oats' }, + { key: 'barley', label: 'Barley' }, + ], + }, + ]; + + return ( + + {(category: any) => ( + + {(item: any) => ( + + {item.label} + + )} + + )} + + ); + }, + parameters: { + docs: { + description: { + story: + 'Sections and items can be defined dynamically using the `items` prop with a render function. Pass hierarchical data where each section contains a `children` array.', + }, + }, + }, +}; + export const CustomSummary: Story = { args: { label: 'Custom Summary Display', diff --git a/src/components/fields/ListBox/ListBox.docs.mdx b/src/components/fields/ListBox/ListBox.docs.mdx index 952608b73..43dabe810 100644 --- a/src/components/fields/ListBox/ListBox.docs.mdx +++ b/src/components/fields/ListBox/ListBox.docs.mdx @@ -327,6 +327,33 @@ Groups related items together with an optional heading. ``` +### With Sections (Dynamic) + + + +Sections and items can be defined dynamically using the `items` prop with a render function. Pass hierarchical data where each section contains a `children` array. + +```jsx +const categories = [ + { name: 'Fruits', children: [ + { key: 'apple', label: 'Apple' }, + { key: 'banana', label: 'Banana' }, + ]}, + { name: 'Vegetables', children: [ + { key: 'carrot', label: 'Carrot' }, + { key: 'broccoli', label: 'Broccoli' }, + ]}, +]; + + + {(category) => ( + + {(item) => {item.label}} + + )} + +``` + ### Multiple Selection @@ -736,15 +763,22 @@ This component supports all [Field properties](/docs/getting-started-field-prope ### Virtualization -ListBox automatically enables virtualization when: -- No sections are present (sections disable virtualization) -- Provides smooth scrolling even with large datasets -- Only visible items are rendered in the DOM -- Uses `@tanstack/react-virtual` for efficient rendering -- Automatically measures item heights for consistent scrolling -- Supports items with varying heights (descriptions, complex content) + + +For large datasets, use the `items` prop with a render function. This enables automatic virtualization — only visible items are rendered in the DOM, providing smooth scrolling even with thousands of items. + +```jsx +const items = Array.from({ length: 1000 }, (_, i) => ({ + key: `item-${i}`, + label: `Item ${i + 1}`, +})); + + + {(item) => {item.label}} + +``` -**Note**: Virtualization is transparent to the user - no configuration needed. It works automatically when you have a flat list without sections. +**Note**: Virtualization is disabled when sections are present. For very large datasets, prefer a flat list structure. ### Optimization Tips diff --git a/src/components/fields/ListBox/ListBox.stories.tsx b/src/components/fields/ListBox/ListBox.stories.tsx index 67b6c63be..d65ca197f 100644 --- a/src/components/fields/ListBox/ListBox.stories.tsx +++ b/src/components/fields/ListBox/ListBox.stories.tsx @@ -408,6 +408,57 @@ WithSections.args = { selectionMode: 'single', }; +export const DynamicSections: StoryObj>['render'] = ( + args, +) => { + const categories = [ + { + name: 'Fruits', + children: [ + { key: 'apple', label: 'Apple' }, + { key: 'banana', label: 'Banana' }, + { key: 'cherry', label: 'Cherry' }, + ], + }, + { + name: 'Vegetables', + children: [ + { key: 'carrot', label: 'Carrot' }, + { key: 'broccoli', label: 'Broccoli' }, + { key: 'spinach', label: 'Spinach' }, + ], + }, + { + name: 'Grains', + children: [ + { key: 'rice', label: 'Rice' }, + { key: 'oats', label: 'Oats' }, + { key: 'barley', label: 'Barley' }, + ], + }, + ]; + + return ( + + {(category: any) => ( + + {(item: any) => ( + {item.label} + )} + + )} + + ); +}; +DynamicSections.args = { + label: 'Select food items', + selectionMode: 'single', +}; + export const WithHeaderAndFooter: StoryObj>['render'] = ( args, ) => ( diff --git a/src/components/fields/Picker/Picker.docs.mdx b/src/components/fields/Picker/Picker.docs.mdx index e6fdb3c49..ea4792dbd 100644 --- a/src/components/fields/Picker/Picker.docs.mdx +++ b/src/components/fields/Picker/Picker.docs.mdx @@ -445,6 +445,33 @@ Organize items into logical groups with section headers. ``` +### With Sections (Dynamic) + + + +Sections and items can be defined dynamically using the `items` prop with a render function. Pass hierarchical data where each section contains a `children` array. + +```jsx +const categories = [ + { name: 'Fruits', children: [ + { key: 'apple', label: 'Apple' }, + { key: 'banana', label: 'Banana' }, + ]}, + { name: 'Vegetables', children: [ + { key: 'carrot', label: 'Carrot' }, + { key: 'broccoli', label: 'Broccoli' }, + ]}, +]; + + + {(category) => ( + + {(item) => {item.label}} + + )} + +``` + ### Different Sizes diff --git a/src/components/fields/Picker/Picker.stories.tsx b/src/components/fields/Picker/Picker.stories.tsx index 424de7176..2071a1e88 100644 --- a/src/components/fields/Picker/Picker.stories.tsx +++ b/src/components/fields/Picker/Picker.stories.tsx @@ -144,6 +144,50 @@ export const WithSections: Story = { }, }; +export const DynamicSections: Story = { + args: { + placeholder: 'Select a food', + label: 'Favorite Food', + selectionMode: 'single', + }, + render: (args) => { + const categories = [ + { + name: 'Fruits', + children: [ + { key: 'apple', label: 'Apple' }, + { key: 'banana', label: 'Banana' }, + { key: 'orange', label: 'Orange' }, + ], + }, + { + name: 'Vegetables', + children: [ + { key: 'carrot', label: 'Carrot' }, + { key: 'broccoli', label: 'Broccoli' }, + { key: 'spinach', label: 'Spinach' }, + ], + }, + ]; + + return ( + + {(category: any) => ( + + {(item: any) => ( + {item.label} + )} + + )} + + ); + }, +}; + export const WithItemsArray: Story = { args: { placeholder: 'Select a fruit', diff --git a/src/components/fields/Select/Select.docs.mdx b/src/components/fields/Select/Select.docs.mdx index 64cabf4bd..c0258c311 100644 --- a/src/components/fields/Select/Select.docs.mdx +++ b/src/components/fields/Select/Select.docs.mdx @@ -417,6 +417,8 @@ Groups related options together with an optional heading. +Sections and items can be defined dynamically using the `items` prop with a render function. + ```jsx const groups = [ { From 05b99c3039a7a05fc6577055e65430cadf88241f Mon Sep 17 00:00:00 2001 From: Andrey Yamanov Date: Fri, 3 Apr 2026 12:39:31 +0200 Subject: [PATCH 2/2] feat(LayoutPanel): resizing on mobile devices --- .changeset/fix-layout-panel-touch-resize.md | 5 +++++ src/components/content/Layout/LayoutPanel.tsx | 4 ++++ 2 files changed, 9 insertions(+) create mode 100644 .changeset/fix-layout-panel-touch-resize.md diff --git a/.changeset/fix-layout-panel-touch-resize.md b/.changeset/fix-layout-panel-touch-resize.md new file mode 100644 index 000000000..b1e784837 --- /dev/null +++ b/.changeset/fix-layout-panel-touch-resize.md @@ -0,0 +1,5 @@ +--- +'@cube-dev/ui-kit': patch +--- + +Fix Layout.Panel resize handler not working properly on touch devices by adding `touch-action: none` to prevent browser scroll interference during drag diff --git a/src/components/content/Layout/LayoutPanel.tsx b/src/components/content/Layout/LayoutPanel.tsx index 8f21a3ec7..ecb14a335 100644 --- a/src/components/content/Layout/LayoutPanel.tsx +++ b/src/components/content/Layout/LayoutPanel.tsx @@ -176,6 +176,10 @@ const ResizeHandlerElement = tasty({ horizontal: 'col-resize', disabled: 'not-allowed', }, + touchAction: { + '': 'none', + disabled: 'auto', + }, padding: 0, outline: 0, boxSizing: 'border-box',