Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
3bfc1f3
fix(filtered-search): omit daterange specific properties from datepic…
chintankavathia Jan 7, 2026
9513442
fix(theme): ensure the input line-height is consistent between browsers
spliffone May 9, 2026
3dc17b3
fixup(theme): make circle-status size icon size + padding
dr-itz May 6, 2026
bafb93e
fixup(theme): make avatar size icon size + padding
dr-itz May 6, 2026
ff4f7ab
fix(theme): make .badge-text center aligned
dr-itz May 6, 2026
776017b
fix(side-panel): fix position of legacy status action dot with increa…
dr-itz May 6, 2026
270a66d
refactor(tree): tree state indicator grow with root font size
spliffone May 12, 2026
c0a61ec
refactor(datatable): migrate to component bindings
spliffone May 12, 2026
844f678
refactor(slider): use relative units for si-slider
spliffone May 6, 2026
7655f66
refactor(select): prevent search icon from overlapping
spliffone May 12, 2026
8da23b4
refactor(header-dropdown): migrate to component bindings
spliffone May 12, 2026
baeada7
refactor(menu): migrate to component bindings
spliffone May 13, 2026
b682c4f
refactor(navbar-vertical): migrate to component bindings
spliffone May 13, 2026
61d8b93
refactor(select): migrate to component bindings
spliffone May 13, 2026
c5393cd
chore(datepicker): remove redundant tabindex
akashsonune May 12, 2026
87191d9
refactor(tree): use DestroyRef instead of life-cycle method
spliffone May 8, 2026
dcf58b1
feat(select): support constructions of custom select
spike-rabbit Apr 10, 2026
70ec30a
refactor(dashboard): measure scrollbar width directly from element
chintankavathia May 15, 2026
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
2 changes: 1 addition & 1 deletion api-goldens/element-ng/common/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ export const responsivelyCheckDirection: (params: {
close: boolean;
};

// @public
// @public @deprecated
export class ScrollbarHelper {
readonly width: number;
}
Expand Down
2 changes: 1 addition & 1 deletion api-goldens/element-ng/filtered-search/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { TranslatableString } from '@siemens/element-translate-ng/translate';

// @public
export interface CriterionDefinition {
datepickerConfig?: DatepickerInputConfig;
datepickerConfig?: Omit<DatepickerInputConfig, 'enableDateRange' | 'enableTwoMonthDateRange'>;
label?: TranslatableString;
multiSelect?: boolean;
name: string;
Expand Down
37 changes: 37 additions & 0 deletions api-goldens/element-ng/select/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,22 @@ export interface SelectOptionSource<TValue> {
valuesEqual?(optionA: TValue, optionB: TValue): boolean;
}

// @public
export class SiCustomSelectDirective<T> implements ControlValueAccessor, SiFormItemControl {
constructor();
close(): void;
readonly disabled: _angular_core.Signal<boolean>;
readonly disabledInput: _angular_core.InputSignalWithTransform<boolean, unknown>;
readonly errormessageId: _angular_core.InputSignal<string>;
readonly id: _angular_core.InputSignal<string>;
readonly isOpen: _angular_core.WritableSignal<boolean>;
open(event?: Event): void;
readonly openChange: _angular_core.OutputEmitterRef<boolean>;
readonly readonly: _angular_core.InputSignalWithTransform<boolean, unknown>;
updateValue(value: T | undefined): void;
readonly value: _angular_core.ModelSignal<T | undefined>;
}

// @public (undocumented)
export class SiSelectActionDirective {
readonly selectActionAutoClose: _angular_core.InputSignalWithTransform<boolean, unknown>;
Expand All @@ -61,6 +77,18 @@ export class SiSelectActionDirective {
export class SiSelectActionsDirective {
}

// @public
export class SiSelectComboboxComponent {
}

// @public
export class SiSelectComboboxValueComponent {
readonly icon: _angular_core.InputSignal<string | undefined>;
readonly iconColor: _angular_core.InputSignal<string | undefined>;
readonly stackedIcon: _angular_core.InputSignal<string | undefined>;
readonly stackedIconColor: _angular_core.InputSignal<string | undefined>;
}

// @public (undocumented)
export class SiSelectComponent<T> implements SiFormItemControl {
readonly ariaLabel: _angular_core.InputSignal<string | null>;
Expand All @@ -77,6 +105,15 @@ export class SiSelectComponent<T> implements SiFormItemControl {
readonly readonly: _angular_core.InputSignalWithTransform<boolean, unknown>;
}

// @public
export type SiSelectDropdownContentType = 'false' | 'true' | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog';

// @public
export class SiSelectDropdownDirective {
constructor();
readonly contentType: _angular_core.InputSignal<SiSelectDropdownContentType>;
}

// @public
export class SiSelectGroupTemplateDirective {
// (undocumented)
Expand Down
2 changes: 1 addition & 1 deletion api-goldens/element-ng/tree-view/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ export class SiTreeViewComponent implements OnInit, OnChanges, OnDestroy, AfterV
}

// @public (undocumented)
export class SiTreeViewItemComponent implements OnInit, OnDestroy, AfterViewInit, FocusableOption, DoCheck {
export class SiTreeViewItemComponent implements OnInit, AfterViewInit, FocusableOption, DoCheck {
constructor();
focus(): void;
getLabel(): string;
Expand Down
99 changes: 99 additions & 0 deletions docs/components/forms-inputs/select.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,8 +343,107 @@ The following demonstrates how to add a "Create" button within the dropdown acti

<si-docs-component example="si-select/si-select" height="500"></si-docs-component>

### Custom select (experimental)

When the rigid `si-select` options API is not enough — for example to embed a
tree, drive multi-stage selection with `Apply` / `Cancel` semantics, or render
arbitrary UI inside the dropdown — applications can compose their own select
from the following primitives:

- `SiCustomSelectDirective` — host directive providing the combobox wiring:
`ControlValueAccessor` (`formControl`, `ngModel`, `[(value)]`), disabled /
readonly handling, `SiFormItemControl` integration, ARIA `combobox` role,
keyboard activation (Enter, Space, ArrowDown / ArrowUp), CDK overlay
lifecycle, and focus trapping inside the dropdown.
- `SiSelectComboboxComponent` — the visual trigger that renders the projected
value and the dropdown caret. Pair it with `SiSelectComboboxValueComponent`
to mark individual selected entries.
- `SiSelectDropdownDirective` — structural directive (`*si-select-dropdown` /
`<ng-template si-select-dropdown>`) that registers the dropdown content
template with the parent custom select. Requires a `contentType` input
whose value is forwarded to the `aria-haspopup` attribute of the combobox
host (`menu`, `listbox`, `tree`, `grid`, `dialog`, `true`, or `false`).

The strategy is intentionally minimal: the host directive owns state, focus,
keyboard handling and form integration, while applications retain full
control over what is rendered in the trigger and in the dropdown. The same
component can therefore be styled either as a `form-control` (inside an
`si-form-item`, with validation) or as a button (`btn btn-primary-ghost`) by
applying the corresponding class on the host element.

A custom select component typically applies `SiCustomSelectDirective` as a
host directive and exposes the inputs and outputs the application needs:

```ts
@Component({
selector: 'app-tree-select',
imports: [SiSelectComboboxComponent, SiSelectDropdownDirective, SiTreeViewComponent],
template: `
<si-select-combobox>
@if (select.value(); as val) {
{{ val }}
} @else {
<span class="text-secondary">Select a location...</span>
}
</si-select-combobox>

<ng-template si-select-dropdown contentType="tree">
<si-tree-view
ariaLabel="Locations"
[items]="items()"
[enableSelection]="true"
[singleSelectMode]="true"
(treeItemClicked)="selectItem($event)"
/>
</ng-template>
`,
hostDirectives: [
{
directive: SiCustomSelectDirective,
inputs: ['disabled', 'readonly', 'value'],
outputs: ['valueChange']
}
]
})
export class TreeSelectComponent {
protected readonly select = inject<SiCustomSelectDirective<string>>(SiCustomSelectDirective);

readonly items = input<TreeItem[]>([]);

selectItem(item: TreeItem): void {
if (item.label) {
this.select.updateValue(item.label as string);
this.select.close();
}
}
}
```

Because `si-tree-view` (and similar components) mutate the model they render,
inputs must be deep-cloned before they are passed in. A robust pattern is to
keep a `pendingItems` signal that is repopulated from a fresh
`JSON.parse(JSON.stringify(...))` clone whenever the dropdown opens, so each
opening starts from a clean slate without leaking state back to the caller.

<si-docs-component example="si-select/si-select-custom" height="450"></si-docs-component>

The same primitives can drive a multi-select with `Apply` / `Cancel`. The
host component buffers pending changes while the dropdown is open and only
calls `select.updateValue(...)` once the user confirms — keeping the
`ControlValueAccessor` value stable until the selection is applied.

<si-docs-component example="si-select/si-select-multi-custom" height="450"></si-docs-component>

<si-docs-api component="SiSelectComponent"></si-docs-api>

<si-docs-api directive="SiSelectSimpleOptionsDirective" heading="Simple options (via SiSelectSimpleOptionsDirective)"></si-docs-api>

<si-docs-api directive="SiCustomSelectDirective"></si-docs-api>

<si-docs-api component="SiSelectComboboxComponent"></si-docs-api>

<si-docs-api component="SiSelectComboboxValueComponent"></si-docs-api>

<si-docs-api directive="SiSelectDropdownDirective"></si-docs-api>

<si-docs-types></si-docs-types>
49 changes: 49 additions & 0 deletions playwright/e2e/element-examples/si-select.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { expect, test } from '../../support/test-helpers';
test.describe('si-select', () => {
const example = 'si-select/si-select';
const lazyLoadExample = 'si-select/si-select-lazy-load';
const customExample = 'si-select/si-select-custom';
const multiCustomExample = 'si-select/si-select-multi-custom';

test(example, async ({ page, si }) => {
await si.visitExample(example);
Expand Down Expand Up @@ -103,4 +105,51 @@ test.describe('si-select', () => {
await page.getByText('Switzerland').click();
await si.runVisualAndA11yTests('checked');
});

test(customExample, async ({ page, si }) => {
await si.visitExample(customExample);

const comboboxes = page.getByRole('combobox');
const buttonCombobox = comboboxes.first();
const formControlCombobox = comboboxes.last();

// Button variant: open, snapshot, select a value.
await buttonCombobox.click();
await expect(buttonCombobox).toHaveAttribute('aria-expanded', 'true');
await si.runVisualAndA11yTests('custom-button-opened');

await page.getByRole('treeitem', { name: 'Company1' }).focus();
await page.keyboard.press('ArrowRight');
await page.getByRole('treeitem', { name: 'Milano' }).click();
await expect(buttonCombobox).not.toHaveAttribute('aria-expanded', 'true');
await expect(buttonCombobox).toContainText('Milano');

// Form-control variant: open, then close and snapshot the closed (invalid) state.
await formControlCombobox.click();
await expect(formControlCombobox).toHaveAttribute('aria-expanded', 'true');

await page.keyboard.press('Escape');
await expect(formControlCombobox).not.toHaveAttribute('aria-expanded', 'true');
await si.runVisualAndA11yTests('custom-form-control-closed');
});

test(multiCustomExample, async ({ page, si }) => {
await si.visitExample(multiCustomExample);

const formControlCombobox = page.getByRole('combobox').last();

// Open form-control variant.
await formControlCombobox.click();
await expect(formControlCombobox).toHaveAttribute('aria-expanded', 'true');

// Check the first node (selects all of its children).
await page.getByRole('tree').getByRole('checkbox').first().check();
await si.runVisualAndA11yTests('multi-custom-first-selected');

// Apply.
await page.getByRole('button', { name: 'Apply' }).click();
await expect(formControlCombobox).not.toHaveAttribute('aria-expanded', 'true');
await expect(formControlCombobox).toContainText('Company1');
await si.runVisualAndA11yTests('multi-custom-applied');
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
- text: Button variant
- combobox "Select a location..." [expanded]
- text: Form-control variant Location (required)*
- combobox "Select a location..."
- text: "Control panel Current value (ngModel): none Current value (formControl): none Location valid: false"
- checkbox "Readonly"
- text: Readonly
- checkbox "Disabled"
- text: Disabled
- tree "Locations":
- treeitem "Company1" [level=1]
- treeitem "Company2" [level=1]
- treeitem "Company3" [level=1]
- button "Clear selection" [disabled]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
- text: Button variant
- combobox "Milano"
- text: Form-control variant Location (required)*
- combobox "Select a location..."
- text: "Required Control panel Current value (ngModel): Milano Current value (formControl): none Location valid: false"
- checkbox "Readonly"
- text: Readonly
- checkbox "Disabled"
- text: Disabled
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
- text: Button variant
- combobox: Milano
- text: Form-control variant Location (required)*
- combobox [expanded]: Select a location...
- text: "Control panel Current value (ngModel): Milano Current value (formControl): none Location valid: false"
- checkbox "Readonly"
- text: Readonly
- checkbox "Disabled"
- text: Disabled
- tree "Locations":
- treeitem "Company1" [level=1]
- treeitem "Company2" [level=1]
- treeitem "Company3" [level=1]
- button "Clear selection" [disabled]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
- text: Button variant
- combobox "Select locations..."
- text: Form-control variant Locations (required)*
- combobox "Company1"
- text: "Control panel Selected (ngModel): none Selected (formControl): Company1 Locations valid: true"
- checkbox "Readonly"
- text: Readonly
- checkbox "Disabled"
- text: Disabled
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
- text: Button variant
- combobox "Select locations..."
- text: Form-control variant Locations (required)*
- combobox "Select locations..." [expanded]
- text: "Control panel Selected (ngModel): none Selected (formControl): none Locations valid: false"
- checkbox "Readonly"
- text: Readonly
- checkbox "Disabled"
- text: Disabled
- dialog "Select locations":
- tree "Locations":
- treeitem "Company1 Company1" [checked] [level=1]:
- checkbox "Company1" [checked]
- text: ""
- treeitem "Company2 Company2" [level=1]:
- checkbox "Company2"
- text: ""
- treeitem "Company3 Company3" [level=1]:
- checkbox "Company3"
- text: ""
- button "Cancel"
- button "Apply"
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
@use 'sass:map';
@use '@siemens/element-theme/src/styles/variables/spacers';

si-avatar {
--avatar-size: calc(1.5rem + 2 * #{map.get(spacers.$spacers, 2)});
si-avatar.small {
// makes the avatar slightly larger with increased font size, aligning with the icons
--avatar-size: calc(1.5rem + #{2 * map.get(spacers.$spacers, 2)});
}
Loading
Loading