Skip to content

Commit f19a20a

Browse files
authored
Merge branch 'angular:main' into accordion
2 parents 42c7610 + f0e411b commit f19a20a

File tree

40 files changed

+377
-302
lines changed

40 files changed

+377
-302
lines changed

guides/theming.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,81 @@ structure and CSS classes are considered private implementation details that may
458458
change at any time. CSS variables used by the Angular Material components should
459459
be defined through the `overrides` API instead of defined explicitly.
460460

461+
## Strong focus indicators
462+
463+
By default, most components indicate browser focus by changing their background color as described
464+
by the Material Design specification. This behavior, however, can fall short of accessibility
465+
requirements, such as [WCAG 4.5:1][], which require a stronger indication of browser focus.
466+
467+
Angular Material supports rendering highly visible outlines on focused elements. Applications can
468+
enable these strong focus indicators via two Sass mixins:
469+
`strong-focus-indicators` and `strong-focus-indicators-theme`.
470+
471+
The `strong-focus-indicators` mixin emits structural indicator styles for all components. This mixin
472+
should be included exactly once in an application, similar to the `core` mixin described above.
473+
474+
The `strong-focus-indicators-theme` mixin emits only the indicator's color styles. This mixin should
475+
be included once per theme, similar to the theme mixins described above. Additionally, you can use
476+
this mixin to change the color of the focus indicators in situations in which the default color
477+
would not contrast sufficiently with the background color.
478+
479+
The following example includes strong focus indicator styles in an application alongside the rest of
480+
the custom theme API.
481+
482+
```scss
483+
@use '@angular/material' as mat;
484+
485+
$my-theme: (
486+
color: mat.$violet-palette,
487+
typography: Roboto,
488+
density: 0
489+
);
490+
491+
@include mat.strong-focus-indicators();
492+
493+
html {
494+
color-scheme: light dark;
495+
@include mat.theme($my-theme);
496+
@include mat.strong-focus-indicators-theme($my-theme);
497+
}
498+
```
499+
500+
### Customizing strong focus indicators
501+
502+
You can pass a configuration map to `strong-focus-indicators` to customize the appearance of the
503+
indicators. This configuration includes `border-style`, `border-width`, and `border-radius`.
504+
505+
You also can customize the color of indicators with `strong-focus-indicators-theme`. This mixin
506+
accepts either a theme, as described earlier in this guide, or a CSS color value. When providing a
507+
theme, the indicators will use the default hue of the primary palette.
508+
509+
The following example includes strong focus indicator styles with custom settings alongside the rest
510+
of the custom theme API.
511+
512+
```scss
513+
@use '@angular/material' as mat;
514+
515+
@include mat.strong-focus-indicators((
516+
border-style: dotted,
517+
border-width: 4px,
518+
border-radius: 2px,
519+
));
520+
521+
html {
522+
color-scheme: light dark;
523+
524+
@include mat.theme((
525+
color: mat.$rose-palette,
526+
typography: Roboto,
527+
density: 0
528+
));
529+
530+
@include mat.strong-focus-indicators-theme(orange);
531+
}
532+
```
533+
534+
[WCAG]: https://www.w3.org/WAI/standards-guidelines/wcag/glance/
535+
461536
## Shadow DOM
462537

463538
Angular Material assumes that, by default, all theme styles are loaded as global

src/aria/accordion/accordion.ts

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ import {
4444
host: {
4545
'class': 'ng-accordion-panel',
4646
'role': 'region',
47-
'[attr.id]': 'pattern.id()',
48-
'[attr.aria-labelledby]': 'pattern.accordionTrigger()?.id()',
49-
'[attr.inert]': 'pattern.hidden() ? true : null',
47+
'[attr.id]': '_pattern.id()',
48+
'[attr.aria-labelledby]': '_pattern.accordionTrigger()?.id()',
49+
'[attr.inert]': '_pattern.hidden() ? true : null',
5050
},
5151
})
5252
export class AccordionPanel {
@@ -64,7 +64,7 @@ export class AccordionPanel {
6464
signal(undefined);
6565

6666
/** The UI pattern instance for this panel. */
67-
readonly pattern: AccordionPanelPattern = new AccordionPanelPattern({
67+
readonly _pattern: AccordionPanelPattern = new AccordionPanelPattern({
6868
id: () => this._id,
6969
value: this.value,
7070
accordionTrigger: () => this.accordionTrigger(),
@@ -73,7 +73,7 @@ export class AccordionPanel {
7373
constructor() {
7474
// Connect the panel's hidden state to the DeferredContentAware's visibility.
7575
afterRenderEffect(() => {
76-
this._deferredContentAware.contentVisible.set(!this.pattern.hidden());
76+
this._deferredContentAware.contentVisible.set(!this._pattern.hidden());
7777
});
7878
}
7979

@@ -102,17 +102,17 @@ export class AccordionPanel {
102102
exportAs: 'ngAccordionTrigger',
103103
host: {
104104
'class': 'ng-accordion-trigger',
105-
'[attr.data-active]': 'pattern.active()',
105+
'[attr.data-active]': '_pattern.active()',
106106
'role': 'button',
107-
'[id]': 'pattern.id()',
108-
'[attr.aria-expanded]': 'pattern.expanded()',
109-
'[attr.aria-controls]': 'pattern.controls()',
110-
'[attr.aria-disabled]': 'pattern.disabled()',
107+
'[id]': '_pattern.id()',
108+
'[attr.aria-expanded]': '_pattern.expanded()',
109+
'[attr.aria-controls]': '_pattern.controls()',
110+
'[attr.aria-disabled]': '_pattern.disabled()',
111111
'[attr.disabled]': 'hardDisabled() ? true : null',
112-
'[attr.tabindex]': 'pattern.tabindex()',
113-
'(keydown)': 'pattern.onKeydown($event)',
114-
'(pointerdown)': 'pattern.onPointerdown($event)',
115-
'(focusin)': 'pattern.onFocus($event)',
112+
'[attr.tabindex]': '_pattern.tabindex()',
113+
'(keydown)': '_pattern.onKeydown($event)',
114+
'(pointerdown)': '_pattern.onPointerdown($event)',
115+
'(focusin)': '_pattern.onFocus($event)',
116116
},
117117
})
118118
export class AccordionTrigger {
@@ -136,18 +136,18 @@ export class AccordionTrigger {
136136
*
137137
* TODO(ok7sai): Consider move this to UI patterns.
138138
*/
139-
readonly hardDisabled = computed(() => this.pattern.disabled() && this.pattern.tabindex() < 0);
139+
readonly hardDisabled = computed(() => this._pattern.disabled() && this._pattern.tabindex() < 0);
140140

141141
/** The accordion panel pattern controlled by this trigger. This is set by AccordionGroup. */
142142
readonly accordionPanel: WritableSignal<AccordionPanelPattern | undefined> = signal(undefined);
143143

144144
/** The UI pattern instance for this trigger. */
145-
readonly pattern: AccordionTriggerPattern = new AccordionTriggerPattern({
145+
readonly _pattern: AccordionTriggerPattern = new AccordionTriggerPattern({
146146
id: () => this._id,
147147
value: this.value,
148148
disabled: this.disabled,
149149
element: () => this._elementRef.nativeElement,
150-
accordionGroup: computed(() => this._accordionGroup.pattern),
150+
accordionGroup: computed(() => this._accordionGroup._pattern),
151151
accordionPanel: this.accordionPanel,
152152
});
153153

@@ -207,12 +207,12 @@ export class AccordionGroup {
207207
wrap = input(false, {transform: booleanAttribute});
208208

209209
/** The UI pattern instance for this accordion group. */
210-
readonly pattern: AccordionGroupPattern = new AccordionGroupPattern({
210+
readonly _pattern: AccordionGroupPattern = new AccordionGroupPattern({
211211
...this,
212212
// TODO(ok7sai): Consider making `activeItem` an internal state in the pattern and call
213213
// `setDefaultState` in the CDK.
214214
activeItem: signal(undefined),
215-
items: computed(() => this._triggers().map(trigger => trigger.pattern)),
215+
items: computed(() => this._triggers().map(trigger => trigger._pattern)),
216216
expandedIds: this.value,
217217
// TODO(ok7sai): Investigate whether an accordion should support horizontal mode.
218218
orientation: () => 'vertical',
@@ -227,9 +227,9 @@ export class AccordionGroup {
227227

228228
for (const trigger of triggers) {
229229
const panel = panels.find(p => p.value() === trigger.value());
230-
trigger.accordionPanel.set(panel?.pattern);
230+
trigger.accordionPanel.set(panel?._pattern);
231231
if (panel) {
232-
panel.accordionTrigger.set(trigger.pattern);
232+
panel.accordionTrigger.set(trigger._pattern);
233233
}
234234
}
235235
});

src/aria/combobox/combobox.ts

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import {
1010
afterRenderEffect,
11+
computed,
1112
contentChild,
1213
Directive,
1314
ElementRef,
@@ -37,12 +38,12 @@ import {toSignal} from '@angular/core/rxjs-interop';
3738
},
3839
],
3940
host: {
40-
'[attr.data-expanded]': 'pattern.expanded()',
41-
'(input)': 'pattern.onInput($event)',
42-
'(keydown)': 'pattern.onKeydown($event)',
43-
'(pointerup)': 'pattern.onPointerup($event)',
44-
'(focusin)': 'pattern.onFocusIn()',
45-
'(focusout)': 'pattern.onFocusOut($event)',
41+
'[attr.data-expanded]': 'expanded()',
42+
'(input)': '_pattern.onInput($event)',
43+
'(keydown)': '_pattern.onKeydown($event)',
44+
'(pointerup)': '_pattern.onPointerup($event)',
45+
'(focusin)': '_pattern.onFocusIn()',
46+
'(focusout)': '_pattern.onFocusOut($event)',
4647
},
4748
})
4849
export class Combobox<V> {
@@ -81,8 +82,14 @@ export class Combobox<V> {
8182
/** The value of the first matching item in the popup. */
8283
readonly firstMatch = input<V | undefined>(undefined);
8384

85+
/** Whether the combobox is expanded. */
86+
readonly expanded = computed(() => this._pattern.expanded());
87+
88+
/** Input element connected to the combobox, if any. */
89+
readonly inputElement = computed(() => this._pattern.inputs.inputEl());
90+
8491
/** The combobox ui pattern. */
85-
readonly pattern = new ComboboxPattern<any, V>({
92+
readonly _pattern = new ComboboxPattern<any, V>({
8693
...this,
8794
textDirection: this.textDirection,
8895
disabled: this.disabled,
@@ -95,13 +102,13 @@ export class Combobox<V> {
95102

96103
constructor() {
97104
afterRenderEffect(() => {
98-
if (!this._deferredContentAware?.contentVisible() && this.pattern.isFocused()) {
105+
if (!this._deferredContentAware?.contentVisible() && this._pattern.isFocused()) {
99106
this._deferredContentAware?.contentVisible.set(true);
100107
}
101108
});
102109

103110
afterRenderEffect(() => {
104-
if (!this._hasBeenFocused() && this.pattern.isFocused()) {
111+
if (!this._hasBeenFocused() && this._pattern.isFocused()) {
105112
this._hasBeenFocused.set(true);
106113
}
107114
});
@@ -114,12 +121,12 @@ export class Combobox<V> {
114121
host: {
115122
'role': 'combobox',
116123
'[value]': 'value()',
117-
'[attr.aria-expanded]': 'combobox.pattern.expanded()',
118-
'[attr.aria-activedescendant]': 'combobox.pattern.activedescendant()',
119-
'[attr.aria-controls]': 'combobox.pattern.popupId()',
120-
'[attr.aria-haspopup]': 'combobox.pattern.hasPopup()',
121-
'[attr.aria-autocomplete]': 'combobox.pattern.autocomplete()',
122-
'[attr.readonly]': 'combobox.pattern.readonly()',
124+
'[attr.aria-expanded]': 'combobox._pattern.expanded()',
125+
'[attr.aria-activedescendant]': 'combobox._pattern.activedescendant()',
126+
'[attr.aria-controls]': 'combobox._pattern.popupId()',
127+
'[attr.aria-haspopup]': 'combobox._pattern.hasPopup()',
128+
'[attr.aria-autocomplete]': 'combobox._pattern.autocomplete()',
129+
'[attr.readonly]': 'combobox._pattern.readonly()',
123130
},
124131
})
125132
export class ComboboxInput {
@@ -133,16 +140,16 @@ export class ComboboxInput {
133140
value = model<string>('');
134141

135142
constructor() {
136-
(this.combobox.pattern.inputs.inputEl as WritableSignal<HTMLInputElement>).set(
143+
(this.combobox._pattern.inputs.inputEl as WritableSignal<HTMLInputElement>).set(
137144
this._elementRef.nativeElement,
138145
);
139-
this.combobox.pattern.inputs.inputValue = this.value;
146+
this.combobox._pattern.inputs.inputValue = this.value;
140147

141148
/** Focuses & selects the first item in the combobox if the user changes the input value. */
142149
afterRenderEffect(() => {
143150
this.value();
144151
this.combobox.popup()?.controls()?.items();
145-
untracked(() => this.combobox.pattern.onFilter());
152+
untracked(() => this.combobox._pattern.onFilter());
146153
});
147154
}
148155
}

0 commit comments

Comments
 (0)