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: 3 additions & 3 deletions adev/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@
"@algolia/requester-browser-xhr": "5.52.0",
"@algolia/requester-node-http": "5.52.0",
"@angular/animations": "workspace:*",
"@angular/aria": "22.0.0-next.7",
"@angular/aria": "22.0.0-next.8",
"@angular/build": "22.0.0-next.7",
"@angular/cdk": "22.0.0-next.7",
"@angular/cdk": "22.0.0-next.8",
"@angular/cli": "22.0.0-next.7",
"@angular/common": "workspace:*",
"@angular/compiler": "workspace:*",
"@angular/compiler-cli": "workspace:*",
"@angular/core": "workspace:*",
"@angular/docs": "workspace:*",
"@angular/forms": "workspace:*",
"@angular/material": "22.0.0-next.7",
"@angular/material": "22.0.0-next.8",
"@angular/platform-browser": "workspace:*",
"@angular/platform-server": "workspace:*",
"@angular/router": "workspace:*",
Expand Down
1 change: 1 addition & 0 deletions adev/shared-docs/components/select/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ ng_project(
],
deps = [
"//adev:node_modules/@angular/aria",
"//adev:node_modules/@angular/cdk",
"//adev:node_modules/@angular/common",
"//adev:node_modules/@angular/core",
"//adev:node_modules/@angular/forms",
Expand Down
116 changes: 74 additions & 42 deletions adev/shared-docs/components/select/select.component.html
Original file line number Diff line number Diff line change
@@ -1,61 +1,93 @@
<div ngCombobox #outerCombobox="ngCombobox" [readonly]="true" [disabled]="disabled()">
<div class="docs-select-input-container">
<div ngCombobox #combobox="ngCombobox" class="docs-combobox-container" [(expanded)]="popupExpanded">
<div #origin class="docs-select-input-container">
<input
ngComboboxInput
[attr.id]="id()"
[attr.name]="name()"
[value]="displayValue()"
placeholder="Select an option..."
placeholder="Select an option"
readonly
[tabindex]="-1"
/>
<span class="material-symbols-outlined docs-select-arrow" translate="no" aria-hidden="true"
>arrow_drop_down</span
>
</div>

<ng-template ngComboboxPopupContainer>
<dialog ngComboboxDialog class="docs-select-dialog">
<div ngCombobox #innerCombobox="ngCombobox" filterMode="manual" [alwaysExpanded]="true">
<div class="docs-select-search-container">
<span
class="material-symbols-outlined docs-select-search-icon"
translate="no"
aria-hidden="true"
>search</span
>
<input
ngComboboxInput
class="docs-select-search-input"
placeholder="Search..."
[(value)]="searchString"
/>
</div>
<ng-template
[cdkConnectedOverlay]="{origin: combobox.element, usePopover: 'inline', matchWidth: true}"
[cdkConnectedOverlayOpen]="popupExpanded()"
[cdkConnectedOverlayDisableClose]="false"
(overlayOutsideClick)="popupExpanded.set(false)"
>
<ng-template ngComboboxPopup [combobox]="combobox" popupType="dialog">
<div class="docs-select-popover">
<div class="docs-select-dialog" ngComboboxWidget>
<div class="example-combobox-container">
<div class="docs-select-search-container">
<span
class="material-symbols-outlined docs-select-search-icon"
translate="no"
aria-hidden="true"
>search</span
>
<input
ngCombobox
#innerCombobox="ngCombobox"
#searchInput
class="docs-select-search-input"
placeholder="Search..."
[(value)]="searchString"
[alwaysExpanded]="true"
(keydown.escape)="onSearchEscape($event)"
/>
</div>

<ng-template ngComboboxPopupContainer>
@if (filteredOptions().length === 0) {
<div class="docs-select-no-results">No results found</div>
}
<div aria-live="polite" class="cdk-visually-hidden">
{{ filteredOptions().length === 0 ? 'No results found for ' + searchString() : '' }}
</div>

<div ngListbox [(value)]="selectedValues" class="docs-select-listbox">
@for (option of filteredOptions(); track option.value) {
<div
ngOption
[value]="option.value"
[label]="option.label"
class="docs-select-option"
>
<span class="docs-select-option-label">{{ option.label }}</span>
<span
class="material-symbols-outlined docs-select-check-icon"
translate="no"
aria-hidden="true"
>check</span
<ng-template ngComboboxPopup [combobox]="innerCombobox">
<div class="example-popup example-popup-no-margin">
@if (filteredOptions().length === 0) {
<div class="docs-select-no-results">No results found</div>
}
<div
#listbox="ngListbox"
ngListbox
[(value)]="selectedValues"
[multi]="false"
ngComboboxWidget
class="example-listbox"
focusMode="activedescendant"
tabindex="-1"
selectionMode="explicit"
(click)="onCommit()"
(keydown.enter)="onCommit()"
[activeDescendant]="listbox.activeDescendant()"
[class.example-empty]="filteredOptions().length === 0"
>
@for (option of filteredOptions(); track option.value) {
<div
ngOption
[value]="option.value"
[label]="option.label"
class="docs-select-option"
>
<span class="docs-select-option-label">{{ option.label }}</span>
<span
class="material-symbols-outlined docs-select-check-icon"
translate="no"
aria-hidden="true"
>check</span
>
</div>
}
</div>
</div>
}
</ng-template>
</div>
</ng-template>
</div>
</div>
</dialog>
</ng-template>
</ng-template>
</div>
41 changes: 24 additions & 17 deletions adev/shared-docs/components/select/select.component.scss
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
:host {
--border-color: color-mix(in srgb, var(--full-contrast) 20%, var(--page-background));
}

[ngCombobox] {
position: relative;
width: 100%;
display: flex;
Expand All @@ -16,22 +14,36 @@
position: relative;
align-items: center;
border-radius: 0.25rem;
}

[ngComboboxInput] {
border-radius: 0.25rem;
}

[ngComboboxInput][readonly='true'] {
cursor: pointer;
padding: 0.7rem 1rem;
input {
width: 100%;
border: none;
outline: none;
font-size: 1rem;
padding: 0.7rem 1rem 0.7rem;
background-color: var(--septenary-contrast);
color: var(--primary-contrast);

&[readonly] {
cursor: pointer;
padding: 0.7rem 1rem;
}

&[aria-expanded='true'] + .docs-select-arrow {
transform: rotate(180deg);
}
}
}

[ngCombobox]:focus-within [ngComboboxInput]:not(.docs-select-search-input) {
outline: 1.5px solid var(--vivid-pink);
box-shadow: 0 0 0 4px color-mix(in srgb, var(--vivid-pink) 25%, transparent);
}

.docs-select-popover {
width: 100%;
}

.docs-select-arrow {
width: 24px;
height: 24px;
Expand All @@ -46,16 +58,11 @@
transition: transform 0.2s ease;
}

[ngComboboxInput][aria-expanded='true'] + .docs-select-arrow {
transform: rotate(180deg);
}

[ngComboboxInput] {
width: 100%;
[ngComboboxInput] [ngCombobox] {
border: none;
outline: none;
font-size: 1rem;
padding: 0.7rem 1rem 0.7rem 2.5rem;
padding: 0.7rem 1rem 0.7rem;
background-color: var(--septenary-contrast);
color: var(--primary-contrast);
}
Expand Down
85 changes: 34 additions & 51 deletions adev/shared-docs/components/select/select.component.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,26 @@
/*!
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import {
Combobox,
ComboboxDialog,
ComboboxInput,
ComboboxPopupContainer,
} from '@angular/aria/combobox';
import {Combobox, ComboboxPopup, ComboboxWidget} from '@angular/aria/combobox';
import {Listbox, Option} from '@angular/aria/listbox';
import {OverlayModule} from '@angular/cdk/overlay';
import {
afterRenderEffect,
Component,
computed,
input,
model,
signal,
untracked,
viewChild,
} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {FormValueControl} from '@angular/forms/signals';

type SelectOptionValue = string | number | boolean;
type SelectOptionValue = string;

export interface SelectOption {
label: string;
Expand All @@ -35,17 +29,9 @@ export interface SelectOption {

@Component({
selector: 'docs-select',
imports: [
Combobox,
ComboboxDialog,
ComboboxInput,
ComboboxPopupContainer,
FormsModule,
Listbox,
Option,
],
templateUrl: './select.component.html',
styleUrls: ['./select.component.scss'],
templateUrl: 'select.component.html',
styleUrl: 'select.component.css',
imports: [Combobox, ComboboxPopup, ComboboxWidget, Listbox, Option, OverlayModule],
})
export class Select implements FormValueControl<string | null> {
readonly value = model<string | null>(null);
Expand All @@ -55,12 +41,12 @@ export class Select implements FormValueControl<string | null> {
readonly options = input.required<SelectOption[]>();
readonly disabled = input(false);

readonly dialog = viewChild(ComboboxDialog);
readonly listbox = viewChild<Listbox<SelectOptionValue>>(Listbox);
readonly combobox = viewChild<Combobox<SelectOptionValue>>(Combobox);
readonly listbox = viewChild(Listbox);
readonly combobox = viewChild(Combobox);

readonly searchString = signal('');

readonly popupExpanded = signal(false);
readonly filteredOptions = computed(() => {
const search = this.searchString().toLowerCase();
if (!search) {
Expand All @@ -82,39 +68,36 @@ export class Select implements FormValueControl<string | null> {

constructor() {
afterRenderEffect(() => {
if (this.dialog() && this.combobox()?.expanded()) {
untracked(() => this.listbox()?.gotoFirst());
this.positionDialog();
}
this.listbox()?.scrollActiveItemIntoView();
});

afterRenderEffect(() => {
const selected = this.selectedValues();
if (selected.length > 0) {
untracked(() => this.dialog()?.close());
this.value.set(selected[0] as string);
this.searchString.set('');
}
});

afterRenderEffect(() => this.listbox()?.scrollActiveItemIntoView());
}
// TODO: Improve once CDK overlay is fixed https://github.com/angular/components/issues/32504
private positionDialog(): void {
const dialog = this.dialog();
const combobox = this.combobox();

if (!dialog || !combobox) {
return;
onCommit() {
const values = this.selectedValues();
if (values.length) {
this.value.set(values[0]);
//this.popupExpanded.set(false);
}
}

const comboboxRect = combobox.inputElement()?.getBoundingClientRect();
const scrollY = window.scrollY;
/** Dismisses the dialog overlay on Escape key. */
onSearchEscape(event: Event) {
this.popupExpanded.set(false);
this.combobox()?.element.focus();
}

if (comboboxRect) {
dialog.element.style.width = `${comboboxRect.width}px`;
dialog.element.style.top = `${comboboxRect.bottom + scrollY + 4}px`;
dialog.element.style.left = `${comboboxRect.left}px`;
/** Handles keydown events on the clear button. */
onKeydown(event: KeyboardEvent): void {
if (event.key === 'Enter') {
this.clear();
this.popupExpanded.set(false);
event.stopPropagation();
}
}

/** Clears the search query and all selected options. */
clear(): void {
this.searchString.set('');
this.value.set(null);
}
}
Loading
Loading