Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions packages/assets/src/scss/inputs/_dropdown.scss
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,55 @@ $item-gap: calculateRem(4px);
white-space: nowrap;
}

&__group {
display: flex;
flex-direction: column;
gap: $item-gap;
list-style: none;
margin: 0;
padding: 0;

&:not(:first-child) {
margin-top: $item-gap;
}
}

&__group-label {
display: flex;
align-items: center;
min-height: $item-height;
min-width: 0;
padding: calculateRem(4px) calculateRem(8px);
box-sizing: border-box;
font-size: $text-font-size-m;
font-weight: $text-font-weight-semi;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: default;
user-select: none;
}

&__group-items {
display: flex;
flex-direction: column;
gap: $item-gap;
list-style: none;
margin: 0;
padding: 0;
padding-inline-start: calculateRem(16px);
}

&__no-results {
display: flex;
align-items: center;
min-height: $item-height;
padding: 0 calculateRem(16px) calculateRem(12px);
box-sizing: border-box;
color: $color-neutral-160;
font-size: $text-font-size-m;
}

&--error {
#{$self}__placeholder {
color: $color-error-90;
Expand Down
6 changes: 5 additions & 1 deletion packages/components/assets/ibexa_design_system.en.xliff
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,18 @@
<source>Select an item</source>
<target>Select an item</target>
</trans-unit>
<trans-unit id="ids.dropdown.placeholder.all">
<trans-unit id="ids.dropdown.placeholder.all" resname="ids.dropdown.placeholder.all">
<source>All</source>
<target>All</target>
</trans-unit>
<trans-unit id="ids.dropdown.search.placeholder" resname="ids.dropdown.search.placeholder">
<source>Search...</source>
<target>Search...</target>
</trans-unit>
<trans-unit id="ids.dropdown.search.no_results" resname="ids.dropdown.search.no_results">
<source>No results found</source>
<target>No results found</target>
</trans-unit>
<trans-unit id="ids.toggle.label.on" resname="ids.toggle.label.on">
<source>On</source>
<target>On</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,20 @@ import type { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from 'storybook/test';
import { action } from 'storybook/actions';

import { generateGroupedItemsArray, generateItemsArray } from '@ids-sb-utils/generators';
import { DropdownDecorator } from '@ids-sb-decorators/DropdownDecorator';
import { DropdownMultiInputStateful } from '.';
import { generateItemsArray } from '@ids-sb-utils/generators';

const DEFAULT_ITEMS_LENGTH = 5;
const MANY_ITEMS_LENGTH = 50;
const WRAPPER_HEIGHT_FOR_LONG_LIST = 500;
const GROUPED_GROUPS_COUNT = 3;
const GROUPED_ITEMS_PER_GROUP = 3;
const GROUPED_UNGROUPED_COUNT = 1;
const MANY_GROUPED_GROUPS_COUNT = 6;
const MANY_GROUPED_ITEMS_PER_GROUP = 8;
const GROUPED_ITEMS = generateGroupedItemsArray(GROUPED_GROUPS_COUNT, GROUPED_ITEMS_PER_GROUP, GROUPED_UNGROUPED_COUNT);
const MANY_GROUPED_ITEMS = generateGroupedItemsArray(MANY_GROUPED_GROUPS_COUNT, MANY_GROUPED_ITEMS_PER_GROUP, GROUPED_UNGROUPED_COUNT);
const NARROW_WRAPPER_WIDTH = 220;
const LONG_LABEL_ITEMS = generateItemsArray(DEFAULT_ITEMS_LENGTH).map((item) => ({
...item,
Expand Down Expand Up @@ -185,3 +192,66 @@ export const NarrowLongLabelsOpenedMenu: Story = {
await userEvent.click(dropdownWidget);
},
};

export const Grouped: Story = {
name: 'Grouped',
args: {
items: GROUPED_ITEMS,
},
parameters: {
wrapperHeight: WRAPPER_HEIGHT_FOR_LONG_LIST,
},
};

export const GroupedOpenedMenu: Story = {
name: 'Grouped (Opened Menu)',
tags: ['!dev'],
args: {
items: GROUPED_ITEMS,
},
parameters: {
wrapperHeight: WRAPPER_HEIGHT_FOR_LONG_LIST,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const dropdownWidget = canvas.getByText('Select an item');

await userEvent.click(dropdownWidget);
},
};

export const GroupedSelectedOpenedMenu: Story = {
name: 'Grouped / Selected (Opened Menu)',
tags: ['!dev'],
args: {
items: GROUPED_ITEMS,
value: ['group-2-item-1'],
},
parameters: {
wrapperHeight: WRAPPER_HEIGHT_FOR_LONG_LIST,
},
play: async ({ canvasElement }) => {
const dropdownWidget = canvasElement.querySelector('.ids-dropdown__widget');

if (dropdownWidget) {
await userEvent.click(dropdownWidget);
}
},
};

export const GroupedManyItemsOpenedMenu: Story = {
name: 'Grouped / Many Items (Opened Menu)',
tags: ['!dev'],
args: {
items: MANY_GROUPED_ITEMS,
},
parameters: {
wrapperHeight: WRAPPER_HEIGHT_FOR_LONG_LIST,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const dropdownWidget = canvas.getByText('Select an item');

await userEvent.click(dropdownWidget);
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,54 @@ export const Default: Story = {
});
},
};

export const Grouped: Story = {
name: 'Grouped',
args: {
items: [
{ id: 'ungrouped', label: 'Ungrouped item' },
{
id: 'fruits',
items: [
{ id: 'apple', label: 'Apple' },
{ id: 'banana', label: 'Banana' },
],
label: 'Fruits',
},
{
id: 'vegetables',
items: [{ id: 'carrot', label: 'Carrot' }],
label: 'Vegetables',
},
],
},
play: async ({ canvasElement, step }) => {
const GROUPS_COUNT = 2;
const canvas = within(canvasElement);
const dropdownWidget = canvas.getByText('Select an item');

await step('Groups render with group semantics and checkbox items inside', async () => {
await userEvent.click(dropdownWidget);

const groups = canvasElement.querySelectorAll('.ids-dropdown__items .ids-dropdown__group');
const fruitsLabel = canvas.getByText('Fruits', { selector: '.ids-dropdown__group-label' });

await expect(groups).toHaveLength(GROUPS_COUNT);
await expect(groups[0]).toHaveAttribute('aria-labelledby', fruitsLabel.id);
await expect(groups[0].querySelectorAll('.ids-input--checkbox')).toHaveLength(GROUPS_COUNT);
});

await step('Selecting a grouped item shows its chip and keeps the optgroup in the source select', async () => {
const appleItem = canvas.getByText('Apple', { selector: '.ids-dropdown__item-label' });

await userEvent.click(appleItem);
await userEvent.click(canvasElement);

const selectedChip = canvas.getByText('Apple', { selector: 'div' });
const groupedOption = canvasElement.querySelector('select optgroup[label="Fruits"] option[value="apple"]');

await expect(selectedChip).toBeVisible();
await expect(groupedOption).not.toBeNull();
});
},
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';

import { BaseDropdown, flattenDropdownItems, isDropdownItemGroup } from '@ids-partials/BaseDropdown';
import { ExtraParamsType, getNextFocusableItem } from '../utils/focus';
import { BaseDropdown } from '@ids-partials/BaseDropdown';
import { CheckboxInput } from '@ids-components/Checkbox';
import { Chip } from '@ids-components/Chip';
import { OverflowList } from '@ids-components/OverflowList';
Expand Down Expand Up @@ -47,7 +47,8 @@ export const DropdownMultiInput = ({
</>
);
};
const selectedItems = value.length ? items.filter((item) => value.includes(item.id)) : [];
const flatItems = flattenDropdownItems(items);
const selectedItems = value.length ? flatItems.filter((item) => value.includes(item.id)) : [];
const renderSelectedItems = () => (
<OverflowList
items={selectedItems}
Expand All @@ -64,21 +65,30 @@ export const DropdownMultiInput = ({
renderMore={({ hiddenCount }) => <Chip isDeletable={false}>+{hiddenCount}</Chip>}
/>
);
const renderOption = (item: DropdownMultiInputItem) => (
<option key={item.id} value={item.id}>
{item.label}
</option>
);
const renderSource = () => {
return (
<select defaultValue={value} multiple name={name} tabIndex={-1}>
{items.map((item) => (
<option key={item.id} value={item.id}>
{item.label}
</option>
))}
{items.map((entry) =>
isDropdownItemGroup(entry) ? (
<optgroup key={entry.id ?? entry.label} label={entry.label}>
{entry.items.map(renderOption)}
</optgroup>
) : (
renderOption(entry)
),
)}
</select>
);
};
const getFocusableElements = ({ itemsList, search }: ExtraParamsType): HTMLElement[] => {
const focusableElements = [
...(search instanceof HTMLElement ? [search] : []),
...Array.from(itemsList.children).reduce((acc: HTMLElement[], child) => {
...Array.from(itemsList.querySelectorAll('.ids-dropdown__item')).reduce((acc: HTMLElement[], child) => {
if (child instanceof HTMLElement) {
const checkbox = child.querySelector('.ids-input--checkbox');

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { BaseDropdownEntry, BaseDropdownItem, BaseDropdownItemGroup } from '@ids-partials/BaseDropdown';
import { BaseComponentAttributes } from '@ids-types/general';
import { BaseDropdownItem } from '@ids-partials/BaseDropdown';

export type DropdownMultiInputItem = BaseDropdownItem;
export type DropdownMultiInputItemGroup = BaseDropdownItemGroup<DropdownMultiInputItem>;
export type DropdownMultiInputEntry = BaseDropdownEntry<DropdownMultiInputItem>;

export enum DropdownMultiInputAction {
Check = 'check',
Expand All @@ -12,7 +14,7 @@ export interface DropdownMultiInputProps extends BaseComponentAttributes {
name: string;
disabled?: boolean;
error?: boolean;
items?: DropdownMultiInputItem[];
items?: DropdownMultiInputEntry[];
onChange?: (value: string[], itemValue: string, action: DropdownMultiInputAction) => void;
placeholder?: string;
value?: string[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,20 @@ import type { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from 'storybook/test';
import { action } from 'storybook/actions';

import { generateGroupedItemsArray, generateItemsArray } from '@ids-sb-utils/generators';
import { DropdownDecorator } from '@ids-sb-decorators/DropdownDecorator';
import { DropdownSingleInputStateful } from '.';
import { generateItemsArray } from '@ids-sb-utils/generators';

const DEFAULT_ITEMS_LENGTH = 5;
const MANY_ITEMS_LENGTH = 50;
const WRAPPER_HEIGHT_FOR_LONG_LIST = 500;
const GROUPED_GROUPS_COUNT = 3;
const GROUPED_ITEMS_PER_GROUP = 3;
const GROUPED_UNGROUPED_COUNT = 1;
const MANY_GROUPED_GROUPS_COUNT = 6;
const MANY_GROUPED_ITEMS_PER_GROUP = 8;
const GROUPED_ITEMS = generateGroupedItemsArray(GROUPED_GROUPS_COUNT, GROUPED_ITEMS_PER_GROUP, GROUPED_UNGROUPED_COUNT);
const MANY_GROUPED_ITEMS = generateGroupedItemsArray(MANY_GROUPED_GROUPS_COUNT, MANY_GROUPED_ITEMS_PER_GROUP, GROUPED_UNGROUPED_COUNT);

const meta: Meta<typeof DropdownSingleInputStateful> = {
component: DropdownSingleInputStateful,
Expand Down Expand Up @@ -120,3 +127,65 @@ export const ManyItemsOpenedMenu: Story = {
await userEvent.click(dropdownWidget);
},
};

export const Grouped: Story = {
name: 'Grouped',
args: {
items: GROUPED_ITEMS,
},
parameters: {
wrapperHeight: WRAPPER_HEIGHT_FOR_LONG_LIST,
},
};

export const GroupedOpenedMenu: Story = {
name: 'Grouped (Opened Menu)',
tags: ['!dev'],
args: {
items: GROUPED_ITEMS,
},
parameters: {
wrapperHeight: WRAPPER_HEIGHT_FOR_LONG_LIST,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const dropdownWidget = canvas.getByText('Select an item');

await userEvent.click(dropdownWidget);
},
};

export const GroupedSelectedOpenedMenu: Story = {
name: 'Grouped / Selected (Opened Menu)',
tags: ['!dev'],
args: {
items: GROUPED_ITEMS,
value: 'group-2-item-1',
},
parameters: {
wrapperHeight: WRAPPER_HEIGHT_FOR_LONG_LIST,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const dropdownWidget = canvas.getByText('Group 2 item 1', { selector: '.ids-dropdown__selection-info-items' });

await userEvent.click(dropdownWidget);
},
};

export const GroupedManyItemsOpenedMenu: Story = {
name: 'Grouped / Many Items (Opened Menu)',
tags: ['!dev'],
args: {
items: MANY_GROUPED_ITEMS,
},
parameters: {
wrapperHeight: WRAPPER_HEIGHT_FOR_LONG_LIST,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const dropdownWidget = canvas.getByText('Select an item');

await userEvent.click(dropdownWidget);
},
};
Loading
Loading