Skip to content

feat(select): support constructions of custom select#1852

Draft
spike-rabbit wants to merge 1 commit intomainfrom
feat/custom-select
Draft

feat(select): support constructions of custom select#1852
spike-rabbit wants to merge 1 commit intomainfrom
feat/custom-select

Conversation

@spike-rabbit
Copy link
Copy Markdown
Member

@spike-rabbit spike-rabbit commented Apr 10, 2026

Adding a set of utilities that allows customers
to create selects with custom backed value selection, for instance using si-tree-view.

A very simple version of it can look like this:

@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>
      <si-tree-view
        class="d-block"
        ariaLabel="Locations"
        [items]="items()"
        [enableSelection]="true"
        [singleSelectMode]="true"
        [isVirtualized]="false"
        (treeItemClicked)="selectItem($event)"
      />
    </ng-template>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
  hostDirectives: [
    {
      directive: SiCustomSelectDirective,
      inputs: ['disabled', 'readonly', 'value'],
      outputs: ['valueChange']
    }
  ]
})
export class TreeSelectComponent {
  protected readonly select = inject(SiCustomSelectDirective);

  /** The tree items to display. */
  readonly items = input<TreeItem[]>([]);

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

The goal is to empower applications
to build selects with whatever content they need
while we take care of accesibility and proper appereance.

Closes #1840


Documentation.
Examples.
Dashboards Demo.
Playwright report.

Coverage Reports:

Code Coverage

@spike-rabbit spike-rabbit requested a review from spliffone April 10, 2026 14:31
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new set of components and directives—SiCustomSelectDirective, SiSelectComboboxComponent, and SiSelectDropdownDirective—to enable the creation of custom select components like tree-selects. The implementation includes ControlValueAccessor and SiFormItemControl integration, comprehensive unit tests, and a usage example. Feedback focuses on improving consistency with the standard select component through enhanced keyboard navigation and visual alignment, ensuring Server-Side Rendering (SSR) safety for browser-specific APIs, and refining the ControlValueAccessor implementation and resource cleanup.

'(click)': 'open()',
'(keydown.enter)': 'open()',
'(keydown.space)': 'open()',
'(keydown.arrowDown)': 'open()'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To maintain consistency with the standard si-select component, the custom select should also open when the ArrowUp key is pressed.

    '(keydown.arrowDown)': 'open()',
    '(keydown.arrowUp)': 'open()'

}

/** Opens the dropdown overlay. */
open(): void {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The open method uses browser-dependent APIs like getBoundingClientRect and the CDK Overlay service. To ensure compatibility with Server-Side Rendering (SSR), these calls should be guarded by a check for the browser platform using isPlatformBrowser.

References
  1. When using browser-dependent UI features like Angular CDK Overlay, ensure they are only executed in a browser environment. Use a check like isPlatformBrowser to prevent errors during Server-Side Rendering (SSR).

hasBackdrop: true,
backdropClass: 'cdk-overlay-transparent-backdrop',
panelClass: ['dropdown-menu', 'show'],
minWidth: width
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The standard si-select implementation adds a 2px offset to the overlay width to account for borders and ensure perfect visual alignment with the trigger element. Consider applying the same offset here.

Suggested change
minWidth: width
minWidth: width + 2

this.isOpen.set(false);
this.disposeOverlay();
this.openChange.emit(false);
this.elementRef.nativeElement.focus();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To adhere to the ControlValueAccessor contract and ensure proper form integration (e.g., triggering validation or marking the control as touched), onTouched() should be called when the dropdown is closed. Additionally, the focus() call should be guarded for SSR safety using isPlatformBrowser.

Suggested change
this.elementRef.nativeElement.focus();
if (isPlatformBrowser(this.platformId)) {
this.elementRef.nativeElement.focus();
}
this.onTouched();
References
  1. When using browser-dependent UI features like Angular CDK Overlay, ensure they are only executed in a browser environment. Use a check like isPlatformBrowser to prevent errors during Server-Side Rendering (SSR).


private disposeOverlay(): void {
if (this.overlayRef) {
this.closeOverlay$.next();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The closeOverlay$ Subject should be completed during the directive's destruction to prevent potential memory leaks.

Suggested change
this.closeOverlay$.next();
this.closeOverlay$.next();
this.closeOverlay$.complete();

block-size: 100%;
color: variables.$element-text-primary;
padding-block: map.get(variables.$spacers, 2);
padding-inline-end: calc(map.get(variables.$spacers, 3) + var(--si-feedback-icon-offset, 0px));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For visual consistency with the standard si-select-input, the padding-inline-end should use $spacers, 5 instead of $spacers, 3. This ensures the dropdown caret is positioned identically across all select variants.

Suggested change
padding-inline-end: calc(map.get(variables.$spacers, 3) + var(--si-feedback-icon-offset, 0px));
padding-inline-end: calc(map.get(variables.$spacers, 5) + var(--si-feedback-icon-offset, 0px));

Adding a set of utilities that allows customers
to create selects with custom backed value selection,
for instance using `si-tree-view`.

A very simple version of it can look like this:

```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>
      <si-tree-view
        class="d-block"
        ariaLabel="Locations"
        [items]="items()"
        [enableSelection]="true"
        [singleSelectMode]="true"
        [isVirtualized]="false"
        (treeItemClicked)="selectItem($event)"
      />
    </ng-template>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
  hostDirectives: [
    {
      directive: SiCustomSelectDirective,
      inputs: ['disabled', 'readonly', 'value'],
      outputs: ['valueChange']
    }
  ]
})
export class TreeSelectComponent {
  protected readonly select = inject(SiCustomSelectDirective);

  /** The tree items to display. */
  readonly items = input<TreeItem[]>([]);

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

The goal is to empower applications
to build selects with whatever content they need
while we take care of accesibility and proper appereance.

Closes #1840
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

New si-custom-select

1 participant