Skip to content

Commit 5184217

Browse files
musaleMnickiigavinbarron
authored
fix(a11y): Add keyboard navigation to mgt-teams-channel-picker (#2415)
Add a11y attributes for the close button Use ArrowDown to show the dropdown and focus on the first el Show the dropdown with Arrowdown or Enter keys Escape keypress closes the dropdown Fix error with input value state not being cleared from fluentui control Add keyboard navigation on selected channel view Set the min-width, height on fluent-buttons with svgs to auto Prefer fluent-button to using div with role=button Change to transparent background for tree item Set fill-color for the card Set item bg in card to transparent and fix outlines restore combobox role --------- Signed-off-by: Martin Musale <[email protected]> Co-authored-by: Nickii Miaro <[email protected]> Co-authored-by: Gavin Barron <[email protected]>
1 parent 99884f8 commit 5184217

File tree

4 files changed

+136
-36
lines changed

4 files changed

+136
-36
lines changed

packages/mgt-components/src/components/mgt-teams-channel-picker/mgt-teams-channel-picker.scss

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@
6767
}
6868

6969
fluent-card {
70-
background: $dropdown-background-color;
70+
background: $dropdown-card-background-color;
71+
padding: 2px;
7172

7273
--card-height: auto;
7374
--width: var(--card-width);
@@ -121,6 +122,9 @@
121122
}
122123

123124
.down-chevron {
125+
height: auto;
126+
min-width: auto;
127+
124128
svg {
125129
path {
126130
fill: var(--channel-picker-down-chevron-color, currentColor);
@@ -129,6 +133,9 @@
129133
}
130134

131135
.up-chevron {
136+
height: auto;
137+
min-width: auto;
138+
132139
svg {
133140
path {
134141
fill: var(--channel-picker-up-chevron-color, currentColor);
@@ -137,6 +144,9 @@
137144
}
138145

139146
.close-icon {
147+
height: auto;
148+
min-width: auto;
149+
140150
svg {
141151
path {
142152
fill: var(--channel-picker-close-icon-color, currentColor);
@@ -156,11 +166,20 @@
156166

157167
--tree-item-nested-width: 2em;
158168

159-
&::part(content-region),
160-
&::part(positioning-region),
169+
&:focus-visible {
170+
outline: none;
171+
}
172+
161173
&::part(expand-collapse-button) {
174+
background: transparent;
175+
}
176+
177+
&::part(content-region),
178+
&::part(positioning-region) {
162179
color: $dropdown-item-text-color;
163180
background: $dropdown-background-color;
181+
border: calc(var(--stroke-width) * 2px) solid transparent;
182+
height: auto;
164183

165184
&:hover {
166185
background: $dropdown-item-background-color-hover;
@@ -170,6 +189,13 @@
170189
}
171190
}
172191
}
192+
193+
// Handle nexted tree-items
194+
fluent-tree-item {
195+
&::part(content-region) {
196+
height: auto;
197+
}
198+
}
173199
}
174200

175201
fluent-breadcrumb-item {

packages/mgt-components/src/components/mgt-teams-channel-picker/mgt-teams-channel-picker.theme.scss

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,39 @@
77

88
@import '../../styles/shared-sass-variables';
99

10-
$input-background: padding-box linear-gradient(
11-
var(--channel-picker-input-background-color, var(--neutral-fill-input-rest)),
12-
var(--channel-picker-input-background-color, var(--neutral-fill-input-rest))),
13-
border-box var(--channel-picker-input-border-color, var(--neutral-stroke-input-rest));
14-
$input-hover-background: padding-box linear-gradient(
15-
var(--channel-picker-input-background-color-hover, var(--neutral-fill-input-hover)),
16-
var(--channel-picker-input-background-color-hover, var(--neutral-fill-input-hover))),
17-
border-box var(--channel-picker-input-hover-border-color, var(--neutral-stroke-input-hover));
18-
$input-focus-background: padding-box linear-gradient(
19-
var(--channel-picker-input-background-color-focus, var(--neutral-fill-input-focus)),
20-
var(--channel-picker-input-background-color-focus, var(--neutral-fill-input-focus))),
21-
border-box var(--channel-picker-input-focus-border-color, var(--neutral-stroke-input-focus));
10+
$input-background: padding-box
11+
linear-gradient(
12+
var(--channel-picker-input-background-color, var(--neutral-fill-input-rest)),
13+
var(--channel-picker-input-background-color, var(--neutral-fill-input-rest))
14+
),
15+
border-box var(--channel-picker-input-border-color, var(--neutral-stroke-input-rest));
16+
$input-hover-background: padding-box
17+
linear-gradient(
18+
var(--channel-picker-input-background-color-hover, var(--neutral-fill-input-hover)),
19+
var(--channel-picker-input-background-color-hover, var(--neutral-fill-input-hover))
20+
),
21+
border-box var(--channel-picker-input-hover-border-color, var(--neutral-stroke-input-hover));
22+
$input-focus-background: padding-box
23+
linear-gradient(
24+
var(--channel-picker-input-background-color-focus, var(--neutral-fill-input-focus)),
25+
var(--channel-picker-input-background-color-focus, var(--neutral-fill-input-focus))
26+
),
27+
border-box var(--channel-picker-input-focus-border-color, var(--neutral-stroke-input-focus));
2228
$placeholder-text-color: var(--channel-picker-input-placeholder-text-color, var(--input-placeholder-rest));
2329
$placeholder-hover-text-color: var(--channel-picker-input-placeholder-text-color-hover, var(--input-placeholder-hover));
24-
$placeholder-focus-text-color: var(--channel-picker-input-placeholder-text-color-focus, var(--input-placeholder-filled));
25-
$dropdown-background-color: var(--channel-picker-dropdown-background-color, var(--fill-color));
30+
$placeholder-focus-text-color: var(
31+
--channel-picker-input-placeholder-text-color-focus,
32+
var(--input-placeholder-filled)
33+
);
34+
$dropdown-card-background-color: var(--channel-picker-dropdown-background-color, var(--fill-color));
35+
$dropdown-background-color: var(--channel-picker-dropdown-background-color, transparent);
2636
$dropdown-item-text-color: var(--channel-picker-dropdown-item-text-color, currentColor);
27-
$dropdown-item-background-color-hover: var(--channel-picker-dropdown-item-background-color-hover, var(--neutral-fill-stealth-hover));
28-
$dropdown-item-selected-text-color: var(--channel-picker-dropdown-item-text-color-selected, var(--neutral-foreground-rest));
37+
$dropdown-item-background-color-hover: var(
38+
--channel-picker-dropdown-item-background-color-hover,
39+
var(--neutral-fill-stealth-hover)
40+
);
41+
$dropdown-item-selected-text-color: var(
42+
--channel-picker-dropdown-item-text-color-selected,
43+
var(--neutral-foreground-rest)
44+
);
2945
$channel-picker-arrow-fill-color: var(--channel-picker-arrow-fill, var(--neutral-foreground-rest));

packages/mgt-components/src/components/mgt-teams-channel-picker/mgt-teams-channel-picker.ts

Lines changed: 73 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -360,10 +360,11 @@ export class MgtTeamsChannelPicker extends MgtTemplatedComponent {
360360
placeholder="${this._selectedItemState ? '' : this.strings.inputPlaceholderText} "
361361
label="teams-channel-picker-input"
362362
role="combobox"
363-
@click=${this.gainedFocus}
364-
@keyup=${(e: KeyboardEvent) => this.handleInputChanged(e)}>
365-
<div slot="start" style="width: max-content;">${this.renderSelected()}</div>
366-
<div slot="end">${this.renderChevrons()}${this.renderCloseButton()}</div>
363+
@click=${this.handleInputClick}
364+
@keydown=${this.handleInputKeydown}
365+
@keyup=${this.handleInputChanged}>
366+
<div tabindex="0" slot="start" style="width: max-content;">${this.renderSelected()}</div>
367+
<div tabindex="0" slot="end">${this.renderChevrons()}${this.renderCloseButton()}</div>
367368
</fluent-text-field>
368369
<fluent-card class=${classMap(dropdownClasses)}>
369370
${this.renderDropdown()}
@@ -372,6 +373,31 @@ export class MgtTeamsChannelPicker extends MgtTemplatedComponent {
372373
);
373374
}
374375

376+
/**
377+
* Handles clicks on the input section.
378+
*
379+
* @param e {UIEvent}
380+
*/
381+
handleInputClick = (e: UIEvent) => {
382+
e.stopPropagation();
383+
this.gainedFocus();
384+
};
385+
386+
handleInputKeydown = (e: KeyboardEvent) => {
387+
const keyName = e.key;
388+
if (['ArrowDown', 'Enter'].includes(keyName)) {
389+
if (!this._isDropdownVisible) {
390+
this.gainedFocus();
391+
} else {
392+
// focus on the first item on the list. Ideally, focus on the selected.
393+
const firstTreeItem = this.renderRoot.querySelector<HTMLElement>('fluent-tree-item');
394+
firstTreeItem.focus();
395+
}
396+
} else if (keyName === 'Escape') {
397+
this.lostFocus();
398+
}
399+
};
400+
375401
/**
376402
* Renders selected channel
377403
*
@@ -444,17 +470,38 @@ export class MgtTeamsChannelPicker extends MgtTemplatedComponent {
444470
*/
445471
protected renderCloseButton() {
446472
return html`
447-
<div
473+
<fluent-button
474+
appearance="stealth"
448475
class="close-icon"
449476
style="display:none"
450-
@click="${() => this.removeSelectedChannel(null)}">
477+
aria-label=${this.strings.closeButtonAriaLabel}
478+
@click=${this.onClickCloseButton}
479+
@keydown=${this.onKeydownCloseButton}>
451480
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
452481
<path d="M0.0885911 0.215694L0.146447 0.146447C0.320013 -0.0271197 0.589437 -0.046405 0.784306 0.0885911L0.853553 0.146447L4 3.293L7.14645 0.146447C7.34171 -0.0488154 7.65829 -0.0488154 7.85355 0.146447C8.04882 0.341709 8.04882 0.658291 7.85355 0.853553L4.707 4L7.85355 7.14645C8.02712 7.32001 8.0464 7.58944 7.91141 7.78431L7.85355 7.85355C7.67999 8.02712 7.41056 8.0464 7.21569 7.91141L7.14645 7.85355L4 4.707L0.853553 7.85355C0.658291 8.04882 0.341709 8.04882 0.146447 7.85355C-0.0488154 7.65829 -0.0488154 7.34171 0.146447 7.14645L3.293 4L0.146447 0.853553C-0.0271197 0.679987 -0.046405 0.410563 0.0885911 0.215694L0.146447 0.146447L0.0885911 0.215694Z" fill="#212121"/>
453482
</svg>
454-
</div>
483+
</fluent-button>
455484
`;
456485
}
457486

487+
/**
488+
* Handles clicks on the close button after selecting a channel.
489+
*
490+
* @param e {UIEvent}
491+
*/
492+
onClickCloseButton = () => {
493+
this.removeSelectedChannel(null);
494+
};
495+
496+
/**
497+
* Handles keypresses on the close button.
498+
*
499+
* @param e {KeyboardEvent}
500+
*/
501+
onKeydownCloseButton = (e: KeyboardEvent) => {
502+
if (e.key === 'Enter') this.removeSelectedChannel(null);
503+
};
504+
458505
/**
459506
* Displays the close button after selecting a channel.
460507
*/
@@ -483,11 +530,11 @@ export class MgtTeamsChannelPicker extends MgtTemplatedComponent {
483530
*/
484531
protected renderDownChevron() {
485532
return html`
486-
<div class="down-chevron" @click=${this.gainedFocus}>
533+
<fluent-button appearance="stealth" class="down-chevron" @click=${this.gainedFocus}>
487534
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
488535
<path d="M2.21967 4.46967C2.51256 4.17678 2.98744 4.17678 3.28033 4.46967L6 7.18934L8.71967 4.46967C9.01256 4.17678 9.48744 4.17678 9.78033 4.46967C10.0732 4.76256 10.0732 5.23744 9.78033 5.53033L6.53033 8.78033C6.23744 9.07322 5.76256 9.07322 5.46967 8.78033L2.21967 5.53033C1.92678 5.23744 1.92678 4.76256 2.21967 4.46967Z" fill="#212121" />
489536
</svg>
490-
</div>`;
537+
</fluent-button>`;
491538
}
492539

493540
/**
@@ -499,11 +546,11 @@ export class MgtTeamsChannelPicker extends MgtTemplatedComponent {
499546
*/
500547
protected renderUpChevron() {
501548
return html`
502-
<div style="display:none" class="up-chevron" @click=${(e: Event) => this.handleUpChevronClick(e)}>
549+
<fluent-button appearance="stealth" style="display:none" class="up-chevron" @click=${this.handleUpChevronClick}>
503550
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
504551
<path d="M2.21967 7.53033C2.51256 7.82322 2.98744 7.82322 3.28033 7.53033L6 4.81066L8.71967 7.53033C9.01256 7.82322 9.48744 7.82322 9.78033 7.53033C10.0732 7.23744 10.0732 6.76256 9.78033 6.46967L6.53033 3.21967C6.23744 2.92678 5.76256 2.92678 5.46967 3.21967L2.21967 6.46967C1.92678 6.76256 1.92678 7.23744 2.21967 7.53033Z" fill="#212121" />
505552
</svg>
506-
</div>`;
553+
</fluent-button>`;
507554
}
508555

509556
/**
@@ -550,7 +597,8 @@ export class MgtTeamsChannelPicker extends MgtTemplatedComponent {
550597
return html`
551598
<fluent-tree-view
552599
class="tree-view"
553-
dir=${this.direction}>
600+
dir=${this.direction}
601+
@keydown=${this.onKeydownTreeView}>
554602
${repeat(
555603
items,
556604
(itemObj: ChannelPickerItemState) => itemObj?.item,
@@ -708,6 +756,13 @@ export class MgtTeamsChannelPicker extends MgtTemplatedComponent {
708756
}
709757
}
710758

759+
onKeydownTreeView = (e: KeyboardEvent) => {
760+
const keyName = e.key;
761+
if (keyName === 'Escape') {
762+
this.lostFocus();
763+
}
764+
};
765+
711766
private handleItemClick(item: ChannelPickerItemState) {
712767
if (item.channels) {
713768
item.isExpanded = !item.isExpanded;
@@ -737,7 +792,7 @@ export class MgtTeamsChannelPicker extends MgtTemplatedComponent {
737792
}
738793
}
739794

740-
private handleInputChanged(e: KeyboardEvent) {
795+
handleInputChanged = (e: KeyboardEvent) => {
741796
const target = e.target as HTMLInputElement;
742797
if (this._inputValue !== target?.value) {
743798
this._inputValue = target?.value;
@@ -755,7 +810,7 @@ export class MgtTeamsChannelPicker extends MgtTemplatedComponent {
755810
}
756811

757812
this.debouncedSearch();
758-
}
813+
};
759814

760815
private onUserKeyDown(e: KeyboardEvent, item?: ChannelPickerItemState) {
761816
const key = e.code;
@@ -872,6 +927,8 @@ export class MgtTeamsChannelPicker extends MgtTemplatedComponent {
872927
if (input) {
873928
input.value = this._inputValue = '';
874929
input.textContent = '';
930+
const wrapper = this.renderRoot.querySelector<HTMLInputElement>('fluent-text-field');
931+
wrapper.value = '';
875932
}
876933

877934
this._isFocused = false;
@@ -932,8 +989,8 @@ export class MgtTeamsChannelPicker extends MgtTemplatedComponent {
932989
this.hideCloseIcon();
933990
}
934991

935-
private handleUpChevronClick(e: Event) {
992+
handleUpChevronClick = (e: Event) => {
936993
e.stopPropagation();
937994
this.lostFocus();
938-
}
995+
};
939996
}

packages/mgt-components/src/components/mgt-teams-channel-picker/strings.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@
1515
export const strings = {
1616
inputPlaceholderText: 'Select a channel',
1717
noResultsFound: "We didn't find any matches.",
18-
loadingMessage: 'Loading...'
18+
loadingMessage: 'Loading...',
19+
closeButtonAriaLabel: 'remove the selected channel'
1920
};

0 commit comments

Comments
 (0)