Skip to content

Commit 85e2dd2

Browse files
committed
docs: add Signal Forms integration example to Angular Aria Autocomplete guide
1 parent 3cc9e2b commit 85e2dd2

7 files changed

Lines changed: 529 additions & 0 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
:host {
2+
display: flex;
3+
justify-content: center;
4+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<div class="form-container">
2+
<country-selector [formField]="countryForm.country"></country-selector>
3+
</div>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import {Component, signal} from '@angular/core';
2+
import {form, FormField, required, validate} from '@angular/forms/signals';
3+
import {ALL_COUNTRIES, CountrySelector} from './country-selector';
4+
5+
@Component({
6+
selector: 'app-root',
7+
templateUrl: 'app.html',
8+
styleUrl: 'app.css',
9+
imports: [CountrySelector, FormField],
10+
})
11+
export class App {
12+
// Signal Forms setup
13+
model = signal({country: ''});
14+
countryForm = form(this.model, (p) => {
15+
required(p.country, {message: 'Country selection is required'});
16+
17+
validate(p.country, (ctx) => {
18+
const value = ctx.value();
19+
if (value && !ALL_COUNTRIES.includes(value)) {
20+
return {
21+
kind: 'invalidCountry',
22+
message: 'Please select a valid country.',
23+
};
24+
}
25+
return null;
26+
});
27+
});
28+
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
2+
3+
:host {
4+
display: block;
5+
font-family: var(--inter-font);
6+
}
7+
8+
.autocomplete-container {
9+
display: flex;
10+
flex-direction: column;
11+
position: relative;
12+
}
13+
14+
.autocomplete-input-container {
15+
display: flex;
16+
position: relative;
17+
align-items: center;
18+
}
19+
20+
.material-symbols-outlined {
21+
font-size: 1.25rem;
22+
pointer-events: none;
23+
}
24+
25+
.search-icon {
26+
left: 0.75rem;
27+
position: absolute;
28+
color: var(--quaternary-contrast);
29+
}
30+
31+
.autocomplete-input {
32+
width: 13rem;
33+
font-size: 1rem;
34+
border-radius: 0.25rem;
35+
padding: 0.75rem 2.5rem 0.75rem 2.5rem;
36+
color: var(--primary-contrast);
37+
outline: none;
38+
border: 1px solid var(--quinary-contrast);
39+
background-color: var(--page-background);
40+
}
41+
42+
.autocomplete-input:focus-visible {
43+
border-color: var(--hot-pink);
44+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--hot-pink) 20%, transparent);
45+
}
46+
47+
.autocomplete-input::placeholder {
48+
color: var(--quaternary-contrast);
49+
}
50+
51+
.popup {
52+
width: 100%;
53+
margin-top: 8px;
54+
padding: 0.5rem;
55+
max-height: 11rem;
56+
border-radius: 0.5rem;
57+
background-color: var(--septenary-contrast);
58+
font-size: 0.9rem;
59+
box-shadow:
60+
0 4px 6px -1px rgba(0, 0, 0, 0.1),
61+
0 2px 4px -1px rgba(0, 0, 0, 0.06);
62+
}
63+
64+
.no-results {
65+
padding: 1rem;
66+
}
67+
68+
.listbox {
69+
gap: 2px;
70+
height: 100%;
71+
display: flex;
72+
overflow: auto;
73+
flex-direction: column;
74+
outline: none;
75+
}
76+
77+
.option {
78+
display: flex;
79+
cursor: pointer;
80+
align-items: center;
81+
margin: 1px;
82+
padding: 0 1rem;
83+
min-height: 2.25rem;
84+
border-radius: 0.5rem;
85+
outline: none;
86+
}
87+
88+
.option:hover {
89+
background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
90+
}
91+
92+
.option[data-active='true'] {
93+
outline-offset: -2px;
94+
outline: 2px solid var(--hot-pink);
95+
}
96+
97+
.option[aria-selected='true'] {
98+
color: var(--hot-pink);
99+
background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent);
100+
}
101+
102+
.option:not([aria-selected='true']) .check-icon {
103+
display: none;
104+
}
105+
106+
.option-label {
107+
flex: 1;
108+
}
109+
110+
.check-icon {
111+
font-size: 0.9rem;
112+
}
113+
114+
.clear-button {
115+
position: absolute;
116+
right: 0.75rem;
117+
width: 2rem;
118+
height: 2rem;
119+
display: flex;
120+
align-items: center;
121+
justify-content: center;
122+
border: none;
123+
background-color: transparent;
124+
border-radius: 0.25rem;
125+
cursor: pointer;
126+
color: var(--quaternary-contrast);
127+
padding: 0;
128+
margin: 0;
129+
outline: none;
130+
transition: background-color 0.15s ease;
131+
}
132+
133+
.clear-button:hover {
134+
background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
135+
}
136+
137+
.clear-button:focus-visible {
138+
outline: 2px solid var(--hot-pink);
139+
outline-offset: -2px;
140+
}
141+
142+
.clear-icon {
143+
font-size: 1.25rem;
144+
}
145+
146+
.error-message {
147+
display: flex;
148+
align-items: center;
149+
gap: 4px;
150+
color: var(--error-color, #e15241);
151+
font-size: 0.85rem;
152+
margin-top: 6px;
153+
}
154+
155+
.error-message span {
156+
font-size: 1rem;
157+
}
158+
159+
.autocomplete-input.invalid {
160+
border-color: var(--error-color, #e15241);
161+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<div class="autocomplete-container">
2+
<div #origin class="autocomplete-input-container">
3+
<span class="search-icon material-symbols-outlined" translate="no" aria-hidden="true"
4+
>search</span
5+
>
6+
<input
7+
#combobox="ngCombobox"
8+
ngCombobox
9+
class="autocomplete-input"
10+
[class.invalid]="touched() && invalid()"
11+
placeholder="Select a country"
12+
[value]="value()"
13+
(input)="value.set($any($event.target).value)"
14+
[(expanded)]="popupExpanded"
15+
(click)="popupExpanded.set(true)"
16+
/>
17+
<button
18+
type="button"
19+
class="clear-button"
20+
aria-label="Clear"
21+
(mousedown)="$event.preventDefault()"
22+
(click)="clear()"
23+
>
24+
<span class="clear-icon material-symbols-outlined" translate="no" aria-hidden="true"
25+
>close</span
26+
>
27+
</button>
28+
</div>
29+
30+
<ng-template
31+
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
32+
[cdkConnectedOverlayOpen]="popupExpanded()"
33+
>
34+
<ng-template ngComboboxPopup [combobox]="combobox">
35+
<div class="popup">
36+
@if (filteredCountries().length === 0) {
37+
<div class="no-results">No results found</div>
38+
}
39+
<div
40+
#listbox="ngListbox"
41+
ngListbox
42+
ngComboboxWidget
43+
class="listbox"
44+
focusMode="activedescendant"
45+
selectionMode="explicit"
46+
[tabindex]="-1"
47+
[activeDescendant]="listbox.activeDescendant()"
48+
[(value)]="selectedOption"
49+
(click)="onCommit()"
50+
(keydown.enter)="onCommit()"
51+
>
52+
@for (country of filteredCountries(); track country) {
53+
<div class="option" ngOption [value]="country" [label]="country">
54+
<span class="option-label">{{ country }}</span>
55+
<span class="check-icon material-symbols-outlined" translate="no" aria-hidden="true"
56+
>check</span
57+
>
58+
</div>
59+
}
60+
</div>
61+
</div>
62+
</ng-template>
63+
</ng-template>
64+
65+
@if (touched() && invalid() && errors().length > 0) {
66+
<div class="error-message">
67+
<span class="material-symbols-outlined" translate="no" aria-hidden="true">error</span>
68+
{{ errors()[0].message }}
69+
</div>
70+
}
71+
</div>

0 commit comments

Comments
 (0)