From caefbe4040af97b817d6d5a4133932724af46c84 Mon Sep 17 00:00:00 2001 From: Sebastian Sebald Date: Thu, 10 Oct 2024 14:32:18 +0200 Subject: [PATCH] refa(listbox): Allow sections in Combobox + Autocomplete, adjust Section API (#4187) * Combobox.Item -> Combobox.Option * refa header * add comments * fix types * autocomplete.item -> autocomplete.option * add sections to autocomplete * add tests * demos * Create bright-masks-love.md * fix types * update * update changeset --- .changeset/bright-masks-love.md | 19 ++ .../autocomplete-appearance.demo.tsx | 18 +- .../autocomplete/autocomplete-async.demo.tsx | 2 +- .../autocomplete-section.demo.tsx | 78 ++++++ .../form/autocomplete/autocomplete.mdx | 253 +++++++++--------- .../combobox/combobox-appearance.demo.tsx | 14 + .../form/combobox/combobox-async.demo.tsx | 2 +- .../form/combobox/combobox-basic.demo.tsx | 14 - .../combobox/combobox-controlled.demo.tsx | 10 +- .../combobox/combobox-menu-trigger.demo.tsx | 16 +- .../form/combobox/combobox-section.demo.tsx | 78 ++++++ .../components/form/combobox/combobox.mdx | 43 +-- .../form/select/select-section.demo.tsx | 68 +++-- .../content/components/form/select/select.mdx | 14 +- .../multiselect-basic.demo.tsx | 4 +- .../src/Autocomplete/Autocomplete.stories.tsx | 56 ++-- .../src/Autocomplete/Autocomplete.test.tsx | 154 ++++++----- .../src/Autocomplete/Autocomplete.tsx | 13 +- .../src/ComboBox/ComboBox.stories.tsx | 62 +++-- .../components/src/ComboBox/ComboBox.test.tsx | 146 +++++----- packages/components/src/ComboBox/ComboBox.tsx | 13 +- .../src/ListBox/ListBox.stories.tsx | 10 +- .../components/src/ListBox/ListBoxSection.tsx | 24 +- .../src/Multiselect/Multiselect.tsx | 4 +- .../OverlayContainerProvider.test.tsx | 4 +- .../src/SearchField/SearchField.test.tsx | 2 +- .../components/src/Select/Select.stories.tsx | 7 +- .../components/src/Select/Select.test.tsx | 24 +- .../src/SelectList/SelectList.test.tsx | 2 +- packages/system/src/types/theme.ts | 2 +- .../src/components/ListBox.styles.ts | 2 +- .../src/components/ListBox.styles.ts | 2 +- .../src/components/ListBox.styles.ts | 2 +- 33 files changed, 736 insertions(+), 426 deletions(-) create mode 100644 .changeset/bright-masks-love.md create mode 100644 docs/content/components/form/autocomplete/autocomplete-section.demo.tsx create mode 100644 docs/content/components/form/combobox/combobox-appearance.demo.tsx delete mode 100644 docs/content/components/form/combobox/combobox-basic.demo.tsx create mode 100644 docs/content/components/form/combobox/combobox-section.demo.tsx diff --git a/.changeset/bright-masks-love.md b/.changeset/bright-masks-love.md new file mode 100644 index 0000000000..95761e4763 --- /dev/null +++ b/.changeset/bright-masks-love.md @@ -0,0 +1,19 @@ +--- +"@marigold/docs": patch +"@marigold/components": patch +"@marigold/system": patch +"@marigold/theme-b2b": patch +--- + +refa(listbox): Allow sections in `` and ``, adjust Section API in `` +- Updated the docs for ``, `` and `` with section stories +- Renamed the part of the `` accordingly (from `sectionTitle` to `header`) + + **BREAKING CHANGE:** We changed the API of the `
` component that is used in ``, `` and `` are now called ` User subbmitted: "{submitted}" diff --git a/docs/content/components/form/autocomplete/autocomplete-async.demo.tsx b/docs/content/components/form/autocomplete/autocomplete-async.demo.tsx index 7de18f6093..57613d18dd 100644 --- a/docs/content/components/form/autocomplete/autocomplete-async.demo.tsx +++ b/docs/content/components/form/autocomplete/autocomplete-async.demo.tsx @@ -53,7 +53,7 @@ export default () => { onSubmit={handleSubmit} > {(item: any) => ( - {item.name} + {item.name} )} {result === null ? null : result.length > 0 ? ( diff --git a/docs/content/components/form/autocomplete/autocomplete-section.demo.tsx b/docs/content/components/form/autocomplete/autocomplete-section.demo.tsx new file mode 100644 index 0000000000..81a1d3840c --- /dev/null +++ b/docs/content/components/form/autocomplete/autocomplete-section.demo.tsx @@ -0,0 +1,78 @@ +import { Autocomplete } from '@marigold/components'; + +export default () => ( + + {options.map(item => ( + + {item.genres.map(genre => ( + {genre} + ))} + + ))} + +); + +const options = [ + { + category: 'Pop and Dance', + genres: [ + 'Pop', + 'Synth-pop', + 'Electropop', + 'Dance-pop', + 'Teen pop', + 'Disco', + ], + }, + { + category: 'Rock and Alternative', + genres: [ + 'Rock', + 'Hard rock', + 'Punk rock', + 'Alternative rock', + 'Indie rock', + 'Grunge', + 'Psychedelic rock', + ], + }, + { + category: 'Hip-Hop and R&B', + genres: ['Hip-Hop', 'Rap', 'Trap', 'R&B', 'Neo-soul'], + }, + { + category: 'Electronic and Experimental', + genres: ['EDM', 'House', 'Techno', 'Dubstep', 'Ambient', 'Industrial'], + }, + { + category: 'Jazz and Blues', + genres: [ + 'Jazz', + 'Smooth jazz', + 'Bebop', + 'Blues', + 'Delta blues', + 'Chicago blues', + ], + }, + { + category: 'Classical and Orchestral', + genres: ['Classical', 'Baroque', 'Opera', 'Symphony', 'Chamber music'], + }, + { + category: 'Folk and Country', + genres: ['Folk', 'Country', 'Bluegrass', 'Americana'], + }, + { + category: 'Latin and World', + genres: ['Reggaeton', 'Salsa', 'Bossa Nova', 'Flamenco', 'Afrobeats'], + }, + { + category: 'Metal and Hard Music', + genres: ['Heavy metal', 'Thrash metal', 'Death metal', 'Doom metal'], + }, + { + category: 'Reggae and Caribbean', + genres: ['Reggae', 'Ska', 'Dancehall', 'Soca'], + }, +]; diff --git a/docs/content/components/form/autocomplete/autocomplete.mdx b/docs/content/components/form/autocomplete/autocomplete.mdx index 156554dc42..73e7da7698 100644 --- a/docs/content/components/form/autocomplete/autocomplete.mdx +++ b/docs/content/components/form/autocomplete/autocomplete.mdx @@ -64,136 +64,149 @@ It is also suitable for fields where people know what they’re looking for, e.g Use an `` instead of a long list. -### Text Label +### Text label The label of the `` provides a clear and concise description of its purpose, making it easier for users to interact with the interface and understand it. Always display a label unless the `` is next to another component which already has a label. - - - - Use a clear and concise text label to clarify the meaning. - - - Use a clear and concise text label to clarify the meaning. - - - - - Avoid using the Autocomplete without a label unless the Autocomplete is part of a complex scenario and its context is already set. - - - Avoid using the Autocomplete without a label unless the Autocomplete is part of a complex scenario and its - context - is already set. - - - - - ### Item Content - - Consider the width of the Autocomplete and keep the names of the options short and compact so that they fit. Long - item - names that occupy multiple lines are hard to perceive and should be avoided. - - - - - Keep the description of the options as short as possible to improve readability. - - - Keep the description of the options as short as possible to improve readability. - - - - - Avoid using lengthy option descriptions because the text can get truncated and users will find it difficult to read. - - - Avoid using lengthy option descriptions because the text can get truncated and users will find it difficult to - read. - - - - - ## Props - + + + + Use a clear and concise text label to clarify the meaning. + + + Use a clear and concise text label to clarify the meaning. + + + + + Avoid using the Autocomplete without a label unless the Autocomplete is part of a complex scenario and its context is already set. + + + Avoid using the Autocomplete without a label unless the Autocomplete is + part of a complex scenario and its context is already set. + + + + +### Item content + +Consider the width of the Autocomplete and keep the names of the options short and compact so that they fit. Long +item +names that occupy multiple lines are hard to perceive and should be avoided. + + + + + Keep the description of the options as short as possible to improve readability. + + + Keep the description of the options as short as possible to improve + readability. + + + + + Avoid using lengthy option descriptions because the text can get truncated and users will find it difficult to read. + + + Avoid using lengthy option descriptions because the text can get truncated + and users will find it difficult to read. + + + + +### With sections + +When related options are present, organizing them into sections enhances clarity and usability. Grouping options provides additional context and helps users navigate choices more easily. This approach reduces complexity and allows for additional guidance when needed, ensuring a more intuitive experience. + +This can be achieved by wrapping the options in the `` component. A header is required for each section, which is set using the `header` prop. + + + +## Props + + ### Autocomplete -### Autocomplete.Item +### Autocomplete.Option - ## Alternative components - -
    -
  • - [Combobox](/components/form/combobox): A text field that allows the user to - select values from a provided items array. Useful when there are mote than - 15 options. -
  • -
  • - [Radio](/components/form/radio): Component which allows to select only one - option from a list. Use it if you have less than 5 options. -
  • -
  • - [Select](/components/form/select): A component with which you can choose exactly one option from a list with - predefined options. -
  • -
- - ## Related - - - - - - ), - }, - ]} - /> +### Autocomplete.Section + + + +## Alternative components + +
    +
  • + [Combobox](/components/form/combobox): A text field that allows the user to + select values from a provided items array. Useful when there are mote than + 15 options. +
  • +
  • + [Radio](/components/form/radio): Component which allows to select only one + option from a list. Use it if you have less than 5 options. +
  • +
  • + [Select](/components/form/select): A component with which you can choose + exactly one option from a list with predefined options. +
  • +
+ +## Related + + + + + + ), + }, + ]} +/> diff --git a/docs/content/components/form/combobox/combobox-appearance.demo.tsx b/docs/content/components/form/combobox/combobox-appearance.demo.tsx new file mode 100644 index 0000000000..2df82956c4 --- /dev/null +++ b/docs/content/components/form/combobox/combobox-appearance.demo.tsx @@ -0,0 +1,14 @@ +import { ComboBox } from '@marigold/components'; + +export default () => ( + + Red Panda + Cat + Dog + Aardvark + Kangaroo + Snake + Vegan + Margrita + +); diff --git a/docs/content/components/form/combobox/combobox-async.demo.tsx b/docs/content/components/form/combobox/combobox-async.demo.tsx index 729f72f5ed..6410ceb5b6 100644 --- a/docs/content/components/form/combobox/combobox-async.demo.tsx +++ b/docs/content/components/form/combobox/combobox-async.demo.tsx @@ -22,7 +22,7 @@ export default () => { items={list.items} > {(item: { name: string }) => ( - {item.name} + {item.name} )}
); diff --git a/docs/content/components/form/combobox/combobox-basic.demo.tsx b/docs/content/components/form/combobox/combobox-basic.demo.tsx deleted file mode 100644 index 94f2c6676e..0000000000 --- a/docs/content/components/form/combobox/combobox-basic.demo.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { ComboBox } from '@marigold/components'; - -export default () => ( - - Red Panda - Cat - Dog - Aardvark - Kangaroo - Snake - Vegan - Margrita - -); diff --git a/docs/content/components/form/combobox/combobox-controlled.demo.tsx b/docs/content/components/form/combobox/combobox-controlled.demo.tsx index 5b7f91e761..1588a211b3 100644 --- a/docs/content/components/form/combobox/combobox-controlled.demo.tsx +++ b/docs/content/components/form/combobox/combobox-controlled.demo.tsx @@ -11,11 +11,11 @@ export default () => { defaultSelectedKey={3} label="Animals" > - Red Panda - Cat - Dog - Aardvark - Kangaroo + Red Panda + Cat + Dog + Aardvark + Kangaroo currentValue: "{currentValue}" diff --git a/docs/content/components/form/combobox/combobox-menu-trigger.demo.tsx b/docs/content/components/form/combobox/combobox-menu-trigger.demo.tsx index 2aa3dbddab..b289ef6cc2 100644 --- a/docs/content/components/form/combobox/combobox-menu-trigger.demo.tsx +++ b/docs/content/components/form/combobox/combobox-menu-trigger.demo.tsx @@ -2,13 +2,13 @@ import { ComboBox } from '@marigold/components'; export default () => ( - Red Panda - Cat - Dog - Aardvark - Kangaroo - Snake - Vegan - Margrita + Red Panda + Cat + Dog + Aardvark + Kangaroo + Snake + Vegan + Margrita ); diff --git a/docs/content/components/form/combobox/combobox-section.demo.tsx b/docs/content/components/form/combobox/combobox-section.demo.tsx new file mode 100644 index 0000000000..2176d978a2 --- /dev/null +++ b/docs/content/components/form/combobox/combobox-section.demo.tsx @@ -0,0 +1,78 @@ +import { ComboBox } from '@marigold/components'; + +export default () => ( + + {options.map(item => ( + + {item.genres.map(genre => ( + {genre} + ))} + + ))} + +); + +const options = [ + { + category: 'Pop and Dance', + genres: [ + 'Pop', + 'Synth-pop', + 'Electropop', + 'Dance-pop', + 'Teen pop', + 'Disco', + ], + }, + { + category: 'Rock and Alternative', + genres: [ + 'Rock', + 'Hard rock', + 'Punk rock', + 'Alternative rock', + 'Indie rock', + 'Grunge', + 'Psychedelic rock', + ], + }, + { + category: 'Hip-Hop and R&B', + genres: ['Hip-Hop', 'Rap', 'Trap', 'R&B', 'Neo-soul'], + }, + { + category: 'Electronic and Experimental', + genres: ['EDM', 'House', 'Techno', 'Dubstep', 'Ambient', 'Industrial'], + }, + { + category: 'Jazz and Blues', + genres: [ + 'Jazz', + 'Smooth jazz', + 'Bebop', + 'Blues', + 'Delta blues', + 'Chicago blues', + ], + }, + { + category: 'Classical and Orchestral', + genres: ['Classical', 'Baroque', 'Opera', 'Symphony', 'Chamber music'], + }, + { + category: 'Folk and Country', + genres: ['Folk', 'Country', 'Bluegrass', 'Americana'], + }, + { + category: 'Latin and World', + genres: ['Reggaeton', 'Salsa', 'Bossa Nova', 'Flamenco', 'Afrobeats'], + }, + { + category: 'Metal and Hard Music', + genres: ['Heavy metal', 'Thrash metal', 'Death metal', 'Doom metal'], + }, + { + category: 'Reggae and Caribbean', + genres: ['Reggae', 'Ska', 'Dancehall', 'Soca'], + }, +]; diff --git a/docs/content/components/form/combobox/combobox.mdx b/docs/content/components/form/combobox/combobox.mdx index c2d7a76cf7..578524aad3 100644 --- a/docs/content/components/form/combobox/combobox.mdx +++ b/docs/content/components/form/combobox/combobox.mdx @@ -1,33 +1,20 @@ --- title: ComboBox caption: A text-field that allows the user to select values from a provided items array. +badge: updated --- The `` component combines a text input with a listbox, allowing users to filter a list of options to items matching a query or adding a new value. Its purpose is to make interaction with software more intuitive by presenting options in a concise, readable manner instead of requiring users to remember cryptic commands or navigate through complex hierarchies -## Import - -```tsx -import { ComboBox } from '@marigold/components'; -``` - ## Appearance - - -## Props - -### ComboBox - - - -### ComboBox.Item + - + -## Examples +## Usage ### Controlled Usage with custom Filter @@ -37,6 +24,14 @@ This is especially helpful if you need to customize the filtering. For example, +### With sections + +When related options are present, organizing them into sections enhances clarity and usability. Grouping options provides additional context and helps users navigate choices more easily. This approach reduces complexity and allows for additional guidance when needed, ensuring a more intuitive experience. + +This can be achieved by wrapping the options in the `` component. A header is required for each section, which is set using the `header` prop. + + + ### Working with asynchronous Data The ComboBox component supports working with asynchronous data. In the example below, the [`useAsyncList`](/hooks/useAsyncList) hook is used to handle the loading and filtering of data from the server. @@ -54,3 +49,17 @@ Opening the suggestion popover can be triggered through various interactions. Th The below examples will display the suggestions when the input field is focused. + +## Props + +### ComboBox + + + +### ComboBox.Option + + + +### ComboBox.Section + + diff --git a/docs/content/components/form/select/select-section.demo.tsx b/docs/content/components/form/select/select-section.demo.tsx index 5fc54b1704..9e27e84965 100644 --- a/docs/content/components/form/select/select-section.demo.tsx +++ b/docs/content/components/form/select/select-section.demo.tsx @@ -1,38 +1,34 @@ -import { Header, Select } from '@marigold/components'; +import { Select } from '@marigold/components'; -export default () => { - const items = [ - { category: 'Comedy', genres: ['Kabarett', 'Satire', 'Stand Up Comedy'] }, - { - category: 'Classic', - genres: ['Chor', 'Kammermusik', 'Kantate', 'Klavierkonzert'], - }, - { - category: 'Hardcore', - genres: [ - 'Hardcore Punk', - 'Jazzcore', - 'Mathcore', - 'Melodic Hardcore', - 'Metalcore', - ], - }, - { - category: 'Metal', - genres: ['Black Metal', 'Death Metal', 'Heavy Metal', 'Nu Metal'], - }, - ]; +export default () => ( + +); - return ( - - ); -}; +const options = [ + { + category: 'Pop and Dance', + genres: ['Pop', 'Electropop', 'Dance-pop', 'Teen pop', 'Disco'], + }, + { + category: 'Rock and Alternative', + genres: [ + 'Hard rock', + 'Punk rock', + 'Alternative rock', + 'Indie rock', + 'Grunge', + ], + }, + { + category: 'Hip-Hop and R&B', + genres: ['Hip-Hop', 'Rap', 'Trap', 'R&B'], + }, +]; diff --git a/docs/content/components/form/select/select.mdx b/docs/content/components/form/select/select.mdx index 6b394e3b94..9eb159425c 100644 --- a/docs/content/components/form/select/select.mdx +++ b/docs/content/components/form/select/select.mdx @@ -71,9 +71,11 @@ When there are too many options (e.g., more than 15-20), usability can suffer. U -### Select with sections +### With sections -Use sections when it is helpful to group different options from each other, this can be for example to categorize related options together. For that you habe to use the `` component. A clear line appears to separate different options, and you can set the title of `` by using `
` component inside ``. +When related options are present, organizing them into sections enhances clarity and usability. Grouping options provides additional context and helps users navigate choices more easily. This approach reduces complexity and allows for additional guidance when needed, ensuring a more intuitive experience. + +This can be achieved by wrapping the options in the `` component. A header is required for each section, which is set using the `header` prop. @@ -99,14 +101,14 @@ Keep in mind that you always should write information why a certain option is di -### Select.Section - - - ### Select.Option +### Select.Section + + + ## Alternative components
    diff --git a/docs/content/recipes/multiselect-recipes/multiselect-basic.demo.tsx b/docs/content/recipes/multiselect-recipes/multiselect-basic.demo.tsx index ebe3246679..90e03ceaea 100644 --- a/docs/content/recipes/multiselect-recipes/multiselect-basic.demo.tsx +++ b/docs/content/recipes/multiselect-recipes/multiselect-basic.demo.tsx @@ -85,9 +85,9 @@ const Multiselect = ({ label, children, ...props }: MultiSelectProps) => { {...props} > {unselected.map((item: MultiSelectItemProps) => ( - + {item.children} - + ))} diff --git a/packages/components/src/Autocomplete/Autocomplete.stories.tsx b/packages/components/src/Autocomplete/Autocomplete.stories.tsx index a61a461aed..d3cbd0778e 100644 --- a/packages/components/src/Autocomplete/Autocomplete.stories.tsx +++ b/packages/components/src/Autocomplete/Autocomplete.stories.tsx @@ -75,16 +75,38 @@ type Story = StoryObj; export const Basic: Story = { render: args => ( - + Harry Potter best series ever - - + + Lord of the Rings - - Star Wars - Star Trek - Firefly + + Star Wars + Star Trek + Firefly + + ), +}; + +export const WithSections: Story = { + render: args => ( + + + Lettuce + Tomato + Onion + + + Ham + Tuna + Tofu + + + Mayonaise + Mustard + Ranch + ), }; @@ -107,22 +129,22 @@ export const Controlled: Story = { onSubmit={(key, val) => setSubmitted([key, val])} disabledKeys={['star-trek']} > - + Harry Potter - - + Lord of the Rings - - + + Star Wars - - + + Star Trek - - Firefly + + Firefly
    current: {current}
    @@ -159,7 +181,7 @@ export const Async: Story = {
             {...args}
           >
             {(item: any) => (
    -          {item.name}
    +          {item.name}
             )}
           
         );
    diff --git a/packages/components/src/Autocomplete/Autocomplete.test.tsx b/packages/components/src/Autocomplete/Autocomplete.test.tsx
    index c5a7360133..f6d3fc0a07 100644
    --- a/packages/components/src/Autocomplete/Autocomplete.test.tsx
    +++ b/packages/components/src/Autocomplete/Autocomplete.test.tsx
    @@ -58,7 +58,7 @@ const theme: Theme = {
           list: cva(),
           option: cva(),
           section: cva(),
    -      sectionTitle: cva(),
    +      header: cva(),
         },
         Button: cva(),
       },
    @@ -85,10 +85,10 @@ afterEach(cleanup);
     test('renders an input', () => {
       render(
         
    -      Spinach
    -      Carrots
    -      Broccoli
    -      Garlic
    +      Spinach
    +      Carrots
    +      Broccoli
    +      Garlic
         
       );
     
    @@ -101,10 +101,10 @@ test('renders an input', () => {
     test('renders a label', () => {
       render(
         
    -      Spinach
    -      Carrots
    -      Broccoli
    -      Garlic
    +      Spinach
    +      Carrots
    +      Broccoli
    +      Garlic
         
       );
     
    @@ -116,8 +116,8 @@ test('renders a label', () => {
     test('supports disabled', () => {
       render(
         
    -      Spinach
    -      Carrots
    +      Spinach
    +      Carrots
         
       );
     
    @@ -128,8 +128,8 @@ test('supports disabled', () => {
     test('supports required', () => {
       render(
         
    -      Spinach
    -      Carrots
    +      Spinach
    +      Carrots
         
       );
     
    @@ -140,8 +140,8 @@ test('supports required', () => {
     test('supports readonly', () => {
       render(
         
    -      Spinach
    -      Carrots
    +      Spinach
    +      Carrots
         
       );
     
    @@ -156,8 +156,8 @@ test('uses field structure', () => {
           description="Some helpful text"
           errorMessage="Whoopsie"
         >
    -      Spinach
    -      Carrots
    +      Spinach
    +      Carrots
         
       );
     
    @@ -174,10 +174,10 @@ test('uses field structure', () => {
     test('opens the suggestions on user input', async () => {
       render(
         
    -      Spinach
    -      Carrots
    -      Broccoli
    -      Garlic
    +      Spinach
    +      Carrots
    +      Broccoli
    +      Garlic
         
       );
     
    @@ -191,10 +191,10 @@ test('opens the suggestions on user input', async () => {
     test('opens the suggestions on focus', async () => {
       render(
         
    -      Spinach
    -      Carrots
    -      Broccoli
    -      Garlic
    +      Spinach
    +      Carrots
    +      Broccoli
    +      Garlic
         
       );
     
    @@ -208,10 +208,10 @@ test('opens the suggestions on focus', async () => {
     test('opens the suggestions on arrow down (manual)', async () => {
       render(
         
    -      Spinach
    -      Carrots
    -      Broccoli
    -      Garlic
    +      Spinach
    +      Carrots
    +      Broccoli
    +      Garlic
         
       );
     
    @@ -225,10 +225,10 @@ test('opens the suggestions on arrow down (manual)', async () => {
     test('shows suggestions based on user input', async () => {
       render(
         
    -      Spinach
    -      Carrots
    -      Broccoli
    -      Garlic
    +      Spinach
    +      Carrots
    +      Broccoli
    +      Garlic
         
       );
     
    @@ -245,10 +245,10 @@ test('shows suggestions based on user input', async () => {
     test('supports disabling suggestions', async () => {
       render(
         
    -      Spinach
    -      Carrots
    -      Broccoli
    -      Garlic
    +      Spinach
    +      Carrots
    +      Broccoli
    +      Garlic
         
       );
     
    @@ -259,13 +259,37 @@ test('supports disabling suggestions', async () => {
       expect(spinach).toHaveAttribute('aria-disabled', 'true');
     });
     
    +test('supports sections', async () => {
    +  render(
    +    
    +      
    +        one
    +        two
    +      
    +      
    +        three
    +        four
    +      
    +    
    +  );
    +
    +  const input = screen.getAllByLabelText('Label')[0];
    +  await user.type(input, 'o');
    +
    +  const s1 = await screen.findByText('Section 1');
    +  const s2 = await screen.findByText('Section 2');
    +
    +  expect(s1).toBeVisible();
    +  expect(s2).toBeVisible();
    +});
    +
     test('supporst showing a help text', () => {
       render(
         
    -      Spinach
    -      Carrots
    -      Broccoli
    -      Garlic
    +      Spinach
    +      Carrots
    +      Broccoli
    +      Garlic
         
       );
     
    @@ -275,10 +299,10 @@ test('supporst showing a help text', () => {
     test('supporst showing an error', () => {
       render(
         
    -      Spinach
    -      Carrots
    -      Broccoli
    -      Garlic
    +      Spinach
    +      Carrots
    +      Broccoli
    +      Garlic
         
       );
     
    @@ -288,10 +312,10 @@ test('supporst showing an error', () => {
     test('supports default value', () => {
       render(
         
    -      Spinach
    -      Carrots
    -      Broccoli
    -      Garlic
    +      Spinach
    +      Carrots
    +      Broccoli
    +      Garlic
         
       );
     
    @@ -304,10 +328,10 @@ test('can be controlled', async () => {
         return (
           <>
             
    -          Spinach
    -          Carrots
    -          Broccoli
    -          Garlic
    +          Spinach
    +          Carrots
    +          Broccoli
    +          Garlic
             
             {value}
           
    @@ -325,10 +349,10 @@ test('can be controlled', async () => {
     test('supports autocompletion', async () => {
       render(
         
    -      Spinach
    -      Carrots
    -      Broccoli
    -      Garlic
    +      Spinach
    +      Carrots
    +      Broccoli
    +      Garlic
         
       );
     
    @@ -344,10 +368,10 @@ test('supports autocompletion', async () => {
     test('supports clear input value', async () => {
       render(
         
    -      Spinach
    -      Carrots
    -      Broccoli
    -      Garlic
    +      Spinach
    +      Carrots
    +      Broccoli
    +      Garlic
         
       );
     
    @@ -365,10 +389,10 @@ test('supports submit handler', async () => {
     
       render(
         
    -      Spinach
    -      Carrots
    -      Broccoli
    -      Garlic
    +      Spinach
    +      Carrots
    +      Broccoli
    +      Garlic
         
       );
     
    diff --git a/packages/components/src/Autocomplete/Autocomplete.tsx b/packages/components/src/Autocomplete/Autocomplete.tsx
    index f2fbbdfa6e..13479ffe6e 100644
    --- a/packages/components/src/Autocomplete/Autocomplete.tsx
    +++ b/packages/components/src/Autocomplete/Autocomplete.tsx
    @@ -163,7 +163,15 @@ interface AutocompleteComponent
       extends ForwardRefExoticComponent<
         AutocompleteProps & RefAttributes
       > {
    -  Item: typeof ListBox.Item;
    +  /**
    +   * Options for the Combobox.
    +   */
    +  Option: typeof ListBox.Item;
    +
    +  /**
    +   * Section for the Combobox, to put options in.
    +   */
    +  Section: typeof ListBox.Section;
     }
     
     // Component
    @@ -209,6 +217,7 @@ const _Autocomplete = forwardRef(
       }
     ) as AutocompleteComponent;
     
    -_Autocomplete.Item = ListBox.Item;
    +_Autocomplete.Option = ListBox.Item;
    +_Autocomplete.Section = ListBox.Section;
     
     export { _Autocomplete as Autocomplete };
    diff --git a/packages/components/src/ComboBox/ComboBox.stories.tsx b/packages/components/src/ComboBox/ComboBox.stories.tsx
    index feb0c2491f..31ce2b7034 100644
    --- a/packages/components/src/ComboBox/ComboBox.stories.tsx
    +++ b/packages/components/src/ComboBox/ComboBox.stories.tsx
    @@ -3,6 +3,7 @@ import { Meta, StoryObj } from '@storybook/react';
     import { Key, useState } from 'react';
     import { useAsyncList } from '@react-stately/data';
     import { Stack } from '../Stack';
    +import { Text } from '../Text';
     import { ComboBox } from './ComboBox';
     
     const meta = {
    @@ -99,7 +100,7 @@ const meta = {
         width: 'full',
         menuTrigger: 'input',
         placeholder: undefined,
    -    label: '',
    +    label: 'Label',
       },
     } satisfies Meta;
     
    @@ -109,14 +110,14 @@ export const Basic: StoryObj = {
       render: args => {
         return (
           
    -        Red Panda
    -        Cat
    -        Dog
    -        Aardvark
    -        Kangaroo
    -        Snake
    -        Vegan
    -        Margrita
    +        Red Panda
    +        Cat
    +        Dog
    +        Aardvark
    +        Kangaroo
    +        Snake
    +        Vegan
    +        Margrita
           
         );
       },
    @@ -136,11 +137,11 @@ export const Controlled: StoryObj = {
               label="Animals"
               {...args}
             >
    -          Red Panda
    -          Cat
    -          Dog
    -          Aardvark
    -          Kangaroo
    +          Red Panda
    +          Cat
    +          Dog
    +          Aardvark
    +          Kangaroo
             
             
               current: {current}, selected: {id?.toString()}
    @@ -174,11 +175,40 @@ export const AsyncLoading: StoryObj = {
             {...args}
           >
             {(item: { name: string }) => (
    -          
    +          
                 {item.name}
    -          
    +          
             )}
           
         );
       },
     };
    +
    +export const Sections: StoryObj = {
    +  render: args => (
    +    
    +      
    +        
    +          Harry Potter
    +          About the boy who lived
    +        
    +        
    +          Lord of the Rings
    +          In the lands of Middle earth
    +        
    +      
    +      
    +        
    +          Start Wars
    +          
    +            A long time ago, in a galaxy far, far away
    +          
    +        
    +        
    +          Star Trek
    +          What is this
    +        
    +      
    +    
    +  ),
    +};
    diff --git a/packages/components/src/ComboBox/ComboBox.test.tsx b/packages/components/src/ComboBox/ComboBox.test.tsx
    index cabe2bfebb..aad6faf132 100644
    --- a/packages/components/src/ComboBox/ComboBox.test.tsx
    +++ b/packages/components/src/ComboBox/ComboBox.test.tsx
    @@ -46,7 +46,7 @@ const theme: Theme = {
           list: cva(),
           option: cva(),
           section: cva(),
    -      sectionTitle: cva(),
    +      header: cva(),
         },
         Popover: cva(['mt-0.5'], {
           variants: {
    @@ -86,10 +86,10 @@ const { render } = setup({ theme });
     test('renders an input', () => {
       render(
         
    -      Spinach
    -      Carrots
    -      Broccoli
    -      Garlic
    +      Spinach
    +      Carrots
    +      Broccoli
    +      Garlic
         
       );
     
    @@ -102,10 +102,10 @@ test('renders an input', () => {
     test('supports width classname', () => {
       render(
         
    -      Spinach
    -      Carrots
    -      Broccoli
    -      Garlic
    +      Spinach
    +      Carrots
    +      Broccoli
    +      Garlic
         
       );
     
    @@ -122,10 +122,10 @@ test('supports classnames', () => {
           variant="one"
           size="small"
         >
    -      Spinach
    -      Carrots
    -      Broccoli
    -      Garlic
    +      Spinach
    +      Carrots
    +      Broccoli
    +      Garlic
         
       );
     
    @@ -145,8 +145,8 @@ test('supports classnames', () => {
     test('supports disabled', () => {
       render(
         
    -      Spinach
    -      Carrots
    +      Spinach
    +      Carrots
         
       );
     
    @@ -157,8 +157,8 @@ test('supports disabled', () => {
     test('supports required', () => {
       render(
         
    -      Spinach
    -      Carrots
    +      Spinach
    +      Carrots
         
       );
     
    @@ -169,8 +169,8 @@ test('supports required', () => {
     test('supports readonly', () => {
       render(
         
    -      Spinach
    -      Carrots
    +      Spinach
    +      Carrots
         
       );
     
    @@ -185,8 +185,8 @@ test('uses field structure', () => {
           description="Some helpful text"
           errorMessage="Whoopsie"
         >
    -      Spinach
    -      Carrots
    +      Spinach
    +      Carrots
         
       );
     
    @@ -203,10 +203,10 @@ test('uses field structure', () => {
     test('opens the suggestions on user input', async () => {
       render(
         
    -      Spinach
    -      Carrots
    -      Broccoli
    -      Garlic
    +      Spinach
    +      Carrots
    +      Broccoli
    +      Garlic
         
       );
     
    @@ -220,10 +220,10 @@ test('opens the suggestions on user input', async () => {
     test('opens the suggestions on focus', async () => {
       render(
         
    -      Spinach
    -      Carrots
    -      Broccoli
    -      Garlic
    +      Spinach
    +      Carrots
    +      Broccoli
    +      Garlic
         
       );
     
    @@ -237,10 +237,10 @@ test('opens the suggestions on focus', async () => {
     test('opens the suggestions on arrow down (manual)', async () => {
       render(
         
    -      Spinach
    -      Carrots
    -      Broccoli
    -      Garlic
    +      Spinach
    +      Carrots
    +      Broccoli
    +      Garlic
         
       );
     
    @@ -254,10 +254,10 @@ test('opens the suggestions on arrow down (manual)', async () => {
     test('shows suggestions based on user input', async () => {
       render(
         
    -      Spinach
    -      Carrots
    -      Broccoli
    -      Garlic
    +      Spinach
    +      Carrots
    +      Broccoli
    +      Garlic
         
       );
     
    @@ -274,10 +274,10 @@ test('shows suggestions based on user input', async () => {
     test('supports disabling suggestions', async () => {
       render(
         
    -      Spinach
    -      Carrots
    -      Broccoli
    -      Garlic
    +      Spinach
    +      Carrots
    +      Broccoli
    +      Garlic
         
       );
     
    @@ -291,10 +291,10 @@ test('supports disabling suggestions', async () => {
     test('supporst showing a help text', () => {
       render(
         
    -      Spinach
    -      Carrots
    -      Broccoli
    -      Garlic
    +      Spinach
    +      Carrots
    +      Broccoli
    +      Garlic
         
       );
     
    @@ -309,10 +309,10 @@ test('supporst showing an error', () => {
           error
           errorMessage="Error!"
         >
    -      Spinach
    -      Carrots
    -      Broccoli
    -      Garlic
    +      Spinach
    +      Carrots
    +      Broccoli
    +      Garlic
         
       );
     
    @@ -322,10 +322,10 @@ test('supporst showing an error', () => {
     test('supports default value', () => {
       render(
         
    -      Spinach
    -      Carrots
    -      Broccoli
    -      Garlic
    +      Spinach
    +      Carrots
    +      Broccoli
    +      Garlic
         
       );
     
    @@ -333,16 +333,40 @@ test('supports default value', () => {
       expect(textField).toHaveValue('garlic');
     });
     
    +test('supports sections', async () => {
    +  render(
    +    
    +      
    +        one
    +        two
    +      
    +      
    +        three
    +        four
    +      
    +    
    +  );
    +
    +  const input = screen.getAllByLabelText('Label')[0];
    +  await user.type(input, 'o');
    +
    +  const s1 = await screen.findByText('Section 1');
    +  const s2 = await screen.findByText('Section 2');
    +
    +  expect(s1).toBeVisible();
    +  expect(s2).toBeVisible();
    +});
    +
     test('can be controlled', async () => {
       const Controlled = () => {
         const [value, setValue] = React.useState('');
         return (
           <>
             
    -          Spinach
    -          Carrots
    -          Broccoli
    -          Garlic
    +          Spinach
    +          Carrots
    +          Broccoli
    +          Garlic
             
             {value}
           
    @@ -360,10 +384,10 @@ test('can be controlled', async () => {
     test('supports autocompletion', async () => {
       render(
         
    -      Spinach
    -      Carrots
    -      Broccoli
    -      Garlic
    +      Spinach
    +      Carrots
    +      Broccoli
    +      Garlic
         
       );
     
    diff --git a/packages/components/src/ComboBox/ComboBox.tsx b/packages/components/src/ComboBox/ComboBox.tsx
    index 8ff71630c3..9cbacab72c 100644
    --- a/packages/components/src/ComboBox/ComboBox.tsx
    +++ b/packages/components/src/ComboBox/ComboBox.tsx
    @@ -91,7 +91,15 @@ interface ComboBoxComponent
       extends ForwardRefExoticComponent<
         ComboBoxProps & RefAttributes
       > {
    -  Item: typeof ListBox.Item;
    +  /**
    +   * Options for the Combobox.
    +   */
    +  Option: typeof ListBox.Item;
    +
    +  /**
    +   * Section for the Combobox, to put options in.
    +   */
    +  Section: typeof ListBox.Section;
     }
     
     // Component
    @@ -143,6 +151,7 @@ const _ComboBox = forwardRef(
       }
     ) as ComboBoxComponent;
     
    -_ComboBox.Item = ListBox.Item;
    +_ComboBox.Option = ListBox.Item;
    +_ComboBox.Section = ListBox.Section;
     
     export { _ComboBox as ComboBox };
    diff --git a/packages/components/src/ListBox/ListBox.stories.tsx b/packages/components/src/ListBox/ListBox.stories.tsx
    index de311a4ad9..7a05c08948 100644
    --- a/packages/components/src/ListBox/ListBox.stories.tsx
    +++ b/packages/components/src/ListBox/ListBox.stories.tsx
    @@ -1,7 +1,6 @@
     /* eslint-disable react-hooks/rules-of-hooks */
     import type { Meta, StoryObj } from '@storybook/react';
     import React from 'react';
    -import { Header } from '../Header';
     import { ListBox } from './ListBox';
     
     const meta = {
    @@ -31,20 +30,17 @@ export const Basic: Story = {
     export const WithSections: Story = {
       render: args => (
         
    -      
    -        
    Veggies
    + Lettuce Tomato Onion - -
    Protein
    + Ham Tuna Tofu - -
    Condiments
    + Mayonaise Mustard Ranch diff --git a/packages/components/src/ListBox/ListBoxSection.tsx b/packages/components/src/ListBox/ListBoxSection.tsx index e25f38b669..1ce9e2b6fc 100644 --- a/packages/components/src/ListBox/ListBoxSection.tsx +++ b/packages/components/src/ListBox/ListBoxSection.tsx @@ -1,18 +1,28 @@ +import type { ReactNode } from 'react'; import type RAC from 'react-aria-components'; -import { Section } from 'react-aria-components'; +import { Header, Section } from 'react-aria-components'; import { cn } from '@marigold/system'; import { useListBoxContext } from './Context'; export interface SectionProps - extends Omit, 'className' | 'style'> {} + extends Omit, 'className' | 'style' | 'children'> { + /** + * Section header to display. + */ + header: ReactNode; + /** + * Children of the section. + */ + children: ReactNode; +} -const _Section = (props: SectionProps) => { +const _Section = ({ header, children, ...props }: SectionProps) => { const { classNames } = useListBoxContext(); return ( -
    +
    +
    {header}
    + {children} +
    ); }; diff --git a/packages/components/src/Multiselect/Multiselect.tsx b/packages/components/src/Multiselect/Multiselect.tsx index e06c0cb20c..910f0936e4 100644 --- a/packages/components/src/Multiselect/Multiselect.tsx +++ b/packages/components/src/Multiselect/Multiselect.tsx @@ -111,9 +111,9 @@ export const Multiselect = ({ {...props} > {unselected.map((item: MultiSelectItemProps) => ( - + {item.children} - + ))} diff --git a/packages/components/src/Provider/OverlayContainerProvider.test.tsx b/packages/components/src/Provider/OverlayContainerProvider.test.tsx index 91c298a932..66d9c71d21 100644 --- a/packages/components/src/Provider/OverlayContainerProvider.test.tsx +++ b/packages/components/src/Provider/OverlayContainerProvider.test.tsx @@ -37,7 +37,7 @@ const theme: Theme = { list: cva(), option: cva(), section: cva(), - sectionTitle: cva(), + header: cva(), }, Field: cva(), }, @@ -62,7 +62,7 @@ test('renders portal container', async () => { - -
    Fantasy
    + Harry Potter About the boy who lived @@ -199,8 +197,7 @@ export const Sections: StoryObj = { In the lands of Middle earth - -
    Sci-Fi
    + Start Wars diff --git a/packages/components/src/Select/Select.test.tsx b/packages/components/src/Select/Select.test.tsx index 7db1fefbac..9e3e107adc 100644 --- a/packages/components/src/Select/Select.test.tsx +++ b/packages/components/src/Select/Select.test.tsx @@ -9,7 +9,6 @@ import { import userEvent from '@testing-library/user-event'; import React from 'react'; import { Theme, cva, useSmallScreen } from '@marigold/system'; -import { Header } from '../Header'; import { Text } from '../Text'; import { setup } from '../test.utils'; import { Select } from './Select'; @@ -68,7 +67,7 @@ const theme: Theme = { list: cva(), option: cva(), section: cva(), - sectionTitle: cva(), + header: cva(), }, }, }; @@ -363,16 +362,14 @@ test('supports default value via "defaultSelectedKey"', () => { expect(three).toHaveAttribute('aria-selected', 'true'); }); -test('supports sections', () => { +test('supports sections', async () => { render( - -
    Section 1
    + one two @@ -410,7 +406,7 @@ test('supports styling classnames with variants and sizes from theme', () => { test('supports applying styles to the caret icon', () => { render( - -
    Section 1
    + one two @@ -469,8 +464,7 @@ test('renders as tray', () => { render(