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
6 changes: 6 additions & 0 deletions packages/components/src/components/SelectNext/Select.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,12 @@ To control how tags are displayed, use the `selectedTagsOverflow` prop. It suppo

<Story of={Stories.SelectedTagsOverflow} />

### Custom Tag Render

Allows for custom rendering of tags.

<Story of={Stories.CustomTagRender} />

### Disabled

When the select component is disabled, it cannot be interacted with.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const meta = {
'Select.ItemAddon': Select.ItemAddon,
},
argTypes: {},
tags: ['status:updated', 'date:2026-05-15'],
tags: ['status:updated', 'date:2026-07-02'],
} satisfies Meta<typeof Select>;

export default meta;
Expand Down Expand Up @@ -445,6 +445,28 @@ export const SelectedTagsOverflow: Story = {
},
};

export const CustomTagRender: Story = {
render: function Render() {
return (
<Select
items={options}
label="Attack type"
selectionMode="multiple"
style={{ inlineSize: 260 }}
placeholder="Select an option"
defaultValue={[1, 2]}
renderTag={(item, tagProps) => (
<Select.Tag {...tagProps} variant="warning-fade" icon={<IconBug16 />}>
{item.textValue}
</Select.Tag>
)}
>
{(item) => <Select.Item id={item.id}>{item.name}</Select.Item>}
</Select>
);
},
};

export const Disabled: Story = {
render: function Render() {
return (
Expand Down
11 changes: 9 additions & 2 deletions packages/components/src/components/SelectNext/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,10 @@ import { List } from '../List';
import type { ListItemAddon } from '../List/components';
import type { PopoverInnerProps, PopoverProps } from '../Popover';
import { PopoverInner } from '../Popover/PopoverInner';
import { SelectedTags } from '../SelectedTags';
import { Tag } from '../Tag';

import {
TagGroup,
SelectList,
SelectOption,
SelectSection,
Expand Down Expand Up @@ -96,6 +97,7 @@ function SelectInner<T extends object, M extends SelectionMode = 'single'>({
onClear,
style,
label,
renderTag,
} = props;

const { validationBehavior: formValidationBehavior } =
Expand Down Expand Up @@ -275,14 +277,17 @@ function SelectInner<T extends object, M extends SelectionMode = 'single'>({
slotProps?.errorMessage
);

// renderTag is only consulted here; when renderValueProp is supplied, this
// function — and therefore renderTag — is never called (see `renderValue` below).
const renderDefaultValue: typeof renderValueProp = (state, states) => {
if (!state.selectedItems?.length) return null;

if (selectionMode === 'multiple')
return (
<TagGroup
<SelectedTags
state={state}
states={states}
renderTag={renderTag}
selectedTagsOverflow={selectedTagsOverflow}
/>
);
Expand Down Expand Up @@ -392,6 +397,7 @@ type CompoundedComponent = typeof SelectComponent & {
Divider: typeof Divider;
ItemText: typeof ListItemText;
ItemAddon: typeof ListItemAddon;
Tag: typeof Tag;
};

/**
Expand All @@ -405,3 +411,4 @@ SelectNext.Section = SelectSection;
SelectNext.Divider = Divider;
SelectNext.ItemText = List.ItemText;
SelectNext.ItemAddon = List.ItemAddon;
SelectNext.Tag = Tag;
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { render, screen } from '@testing-library/react';
import { render, screen, within } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { describe, expect, it, vi, beforeAll, afterAll } from 'vitest';

Expand Down Expand Up @@ -83,4 +83,76 @@ describe('Select_multiple', () => {

expect(onChange).toBeCalledTimes(0);
});

it('should render default tags with the selected item text when renderTag is not provided', () => {
render(renderComponent({ value: [1, 3] }));

const selectedItems = screen.getByLabelText('Selected items');

expect(selectedItems).toHaveTextContent('1');
expect(selectedItems).toHaveTextContent('3');
});

it('should use renderTag to customize selected tags', () => {
render(
renderComponent({
value: [1, 3],
renderTag: (item, tagProps) => (
<div data-testid={`custom-tag-${item.key}`} {...tagProps}>
{item.key}
</div>
),
})
);

expect(screen.getByTestId('custom-tag-1')).toBeInTheDocument();
expect(screen.getByTestId('custom-tag-3')).toBeInTheDocument();
});

it('should render Select.Tag inside renderTag and support removing via its button', async () => {
const onChange = vi.fn();

render(
renderComponent({
value: [1, 3],
onChange,
defaultOpen: false,
renderTag: (item, tagProps) => (
<Select.Tag {...tagProps} data-testid={`tag-${item.key}`}>
{item.key}
</Select.Tag>
),
})
);

expect(screen.getByTestId('tag-1')).toHaveTextContent('1');
expect(screen.getByTestId('tag-3')).toHaveTextContent('3');

await userEvent.click(
within(screen.getByTestId('tag-1')).getByRole('button', {
hidden: true,
})
);

expect(onChange).toHaveBeenCalled();

const selection = onChange.mock.calls.at(-1)?.[0];

expect([...selection]).toEqual([3]);
});

it('should ignore renderTag when renderValue is provided', () => {
render(
renderComponent({
value: [1, 3],
renderValue: () => <div data-testid="custom-value">custom value</div>,
renderTag: (item) => (
<div data-testid={`custom-tag-${item.key}`}>{item.key}</div>
),
})
);

expect(screen.getByTestId('custom-value')).toBeInTheDocument();
expect(screen.queryByTestId('custom-tag-1')).not.toBeInTheDocument();
});
});
105 changes: 0 additions & 105 deletions packages/components/src/components/SelectNext/components/Tag/Tag.tsx

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Loading
Loading