Skip to content

Commit

Permalink
fix: make tasks header navigable with the keyboard. (#2313)
Browse files Browse the repository at this point in the history
update the mgt-arrow-options to use fluent-menu
update mgt-arrow-options items to use fluent ui tokens
change to use a chevron separator
use a fluentui button for arrow default state
enter key press on the header to show the menu and focus the first menu element
close the menu when it has focus on element and tab key is pressed
return focus to header and close menu when you press Escape
set focus on header after selecting an element
click outside open menu closes it

Signed-off-by: Musale Martin <[email protected]>
Co-authored-by: Gavin Barron <[email protected]>
  • Loading branch information
musale and gavinbarron authored May 30, 2023
1 parent af34d15 commit 4747189
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 90 deletions.
4 changes: 2 additions & 2 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ scssFileHeader = `
// ANY CHANGES WILL BE LOST DURING BUILD
// MODIFY THE .SCSS FILE INSTEAD
import { css } from 'lit';
import { css, CSSResult } from 'lit';
/**
* exports lit-element css
* @export styles
*/
export const styles = [
export const styles: CSSResult[] = [
css\``;

scssFileFooter = '`];';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,20 @@ $task-new-dropdown-border-radius: var(--task-new-dropdown-border-radius, calc(va

.title {
justify-content: left;
display: flex;
align-items: center;

.shimmer {
width: 80px;
height: 20px;
}

svg {
margin-top: 3px;
padding: 0 4px;
width: 16px;
height: 16px;
}
}

.new-task-button {
Expand Down
16 changes: 9 additions & 7 deletions packages/mgt-components/src/components/mgt-tasks/mgt-tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { classMap } from 'lit/directives/class-map.js';
import { repeat } from 'lit/directives/repeat.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { getMe } from '../../graph/graph.user';
import { debounce, getShortDateString } from '../../utils/Utils';
import { getShortDateString } from '../../utils/Utils';
import { MgtPeoplePicker } from '../mgt-people-picker/mgt-people-picker';
import { MgtPeople } from '../mgt-people/mgt-people';
import '../mgt-person/mgt-person';
Expand Down Expand Up @@ -901,11 +901,13 @@ export class MgtTasks extends MgtTemplatedComponent {
this._currentFolder = null;
};
}
const groupSelect = mgtHtml`
<mgt-arrow-options class="arrow-options" .options="${groupOptions}" .value="${currentGroup.title}"></mgt-arrow-options>
`;
const groupSelect: TemplateResult = mgtHtml`
<mgt-arrow-options
class="arrow-options"
.options="${groupOptions}"
.value="${currentGroup.title}"></mgt-arrow-options>`;

const divider = !this._currentGroup ? null : html`<fluent-divider></fluent-divider>`;
const separator = !this._currentGroup ? null : getSvg(SvgIcon.ChevronRight);

const currentFolder = this._folders.find(d => d.id === this._currentFolder) || {
name: this.res.BUCKETS_SELF_ASSIGNED
Expand All @@ -932,8 +934,8 @@ export class MgtTasks extends MgtTemplatedComponent {
`;

return html`
<div class="title">
${groupSelect} ${divider} ${!this._currentGroup ? null : folderSelect}
<div class="Title">
${groupSelect} ${separator} ${!this._currentGroup ? null : folderSelect}
</div>
${addButton}
`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,70 +9,43 @@
@import '../../../styles/shared-sass-variables';

$arrow-options-left: var(--arrow-options-left, 0);
$arrow-options-btn-bg-color: var(--arrow-options-button-background-color, var(--neutral-layer-2));
$arrow-options-btn-font-size: var(--arrow-options-button-font-size, large);
$arrow-options-btn-font-weight: var(--arrow-options-button-font-weight, 600);
$arrow-options-btn-font-color: var(--arrow-options-button-font-color, var(--accent-base-color));

:host {
position: relative;
font-family: $font-family;

.arrow-icon {
font-family: $font-icon;
margin: 0 0 0 20px;
user-select: none;

}

.header {
cursor: pointer;

&:hover {
color: var(--theme-primary-color);
&::part(control){
font-size: $arrow-options-btn-font-size;
font-weight: $arrow-options-btn-font-weight;
color: $arrow-options-btn-font-color;
background: $arrow-options-btn-bg-color;

&:hover {
background: var(--neutral-fill-stealth-hover);
}

&:active,
&:focus {
background: var(--neutral-fill-stealth-active);
}
}
}

.menu {
.menu{
position: absolute;
left: var(--arrow-options-left, 0);
box-shadow: set-var(box-shadow__color, $theme-default, $common) 0 0 40px 5px;
background: set-var(background-color, $theme-default, $common);
left: $arrow-options-left;
z-index: 1;
display: none;
color: set-var(color, $theme-default, $common);
white-space: nowrap;

&.open {

&.open{
display: block;
width: max-content;
}
}

.menu-option {
padding: 20px;
cursor: pointer;
user-select: none;
display: flex;
align-items: center;
justify-content: stretch;
justify-items: stretch;

&:first {
padding: 12px 20px 20px;
}

&:hover {
background-color: set-var(background-color--hover, $theme-default, $common);
}

&:active {
background-color: set-var(background-color--active, $theme-default, $common);
}
}

.menu-option-check {
font-family: $font-icon;
color: rgb(0 0 0 / 0%);
margin-right: 10px;

&.current-value {
color: $comm-blue-primary;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import { property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { MgtBaseComponent, customElement } from '@microsoft/mgt-element';
import { styles } from './mgt-arrow-options-css';
import { registerFluentComponents } from '../../../utils/FluentComponents';
import { fluentMenu, fluentMenuItem, fluentButton } from '@fluentui/web-components';
registerFluentComponents(fluentMenu, fluentMenuItem, fluentButton);

/*
Ok, the name here deserves a bit of explanation,
Expand All @@ -22,6 +25,12 @@ import { styles } from './mgt-arrow-options-css';
/**
* Custom Component used to handle an arrow rendering for TaskGroups utilized in the task component.
*
* @cssprop --arrow-options-left {Length} The distance of the dropdown menu from the left in absolute position. Default is 0.
* @cssprop --arrow-options-button-background-color {Color} The background color of the arrow options button.
* @cssprop --arrow-options-button-font-size {Length} The font size of the button text. Default is large.
* @cssprop --arrow-options-button-font-weight {Length} The font weight of the button text. Default is 600.
* @cssprop --arrow-options-button-font-color {Color} The font color of the text in the button.
*
* @export MgtArrowOptions
* @class MgtArrowOptions
* @extends {MgtBaseComponent}
Expand Down Expand Up @@ -53,20 +62,21 @@ export class MgtArrowOptions extends MgtBaseComponent {
@property({ type: String }) public value: string;

/**
* Menu options to be rendered with an attached MouseEvent handler for expansion of details
* Menu options to be rendered with an attached UIEvent handler for expansion of details
*
* @type {object}
* @memberof MgtArrowOptions
*/
@property({ type: Object }) public options: { [name: string]: (e: MouseEvent) => any | void };
@property({ type: Object }) public options: { [name: string]: (e: UIEvent) => any | void };

private _clickHandler: (e: MouseEvent) => void | any;
private _clickHandler: (e: UIEvent) => void | any;

constructor() {
super();
this.value = '';
this.options = {};
this._clickHandler = (e: MouseEvent) => (this.open = false);
this._clickHandler = () => (this.open = false);
window.addEventListener('onblur', () => (this.open = false));
}

// eslint-disable-next-line @typescript-eslint/tslint/config
Expand Down Expand Up @@ -96,41 +106,90 @@ export class MgtArrowOptions extends MgtBaseComponent {
}
};

/**
* Handles key down presses done on the header element.
*
* @param {KeyboardEvent} e
*/
private onHeaderKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
e.stopPropagation();
this.open = !this.open;

// Manually adding the 'open' class to display the menu because
// by the time I set the first element's focus, the classes are not
// updated and that has no effect. You can't set focus on elements
// that have no display.
const fluentMenuEl: HTMLElement = this.renderRoot.querySelector('fluent-menu');
if (fluentMenuEl) {
fluentMenuEl.classList.remove('closed');
fluentMenuEl.classList.add('open');
}

const header: HTMLButtonElement = e.target as HTMLButtonElement;
if (header) {
const firstMenuItem: HTMLElement = this.renderRoot.querySelector("fluent-menu-item[tabindex='0']");
if (firstMenuItem) {
header.blur();
firstMenuItem.focus();
}
}
}
};

/**
* Invoked on each update to perform rendering tasks. This method must return
* a lit-html TemplateResult. Setting properties inside this method will *not*
* trigger the element to update.
*/
public render() {
return html`
<span class="header" @click=${this.onHeaderClick}>
<span class="current-value">${this.value}</span>
</span>
<div class=${classMap({ menu: true, open: this.open, closed: !this.open })}>
${this.getMenuOptions()}
</div>
`;
<fluent-button
class="header"
@click=${this.onHeaderClick}
@keydown=${this.onHeaderKeyDown}
appearance="lightweight">
${this.value}
</fluent-button>
<fluent-menu
class=${classMap({ menu: true, open: this.open, closed: !this.open })}>
${this.getMenuOptions()}
</fluent-menu>`;
}

private getMenuOptions() {
const keys = Object.keys(this.options);
const funcs = this.options;

return keys.map(
opt => html`
<div
class="menu-option"
@click="${(e: MouseEvent) => {
this.open = false;
funcs[opt](e);
}}"
>
<span class=${classMap({ 'menu-option-check': true, 'current-value': this.value === opt })}>
\uE73E
</span>
<span class="menu-option-name">${opt}</span>
</div>
`
);

return keys.map((opt: string) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
const clickFn = (e: MouseEvent) => {
this.open = false;
this.options[opt](e);
};

const keyDownFn = (e: KeyboardEvent) => {
const header: HTMLButtonElement = this.renderRoot.querySelector<HTMLButtonElement>('.header');
if (e.key === 'Enter') {
this.open = false;
this.options[opt](e);
header.focus();
} else if (e.key === 'Tab') {
this.open = false;
} else if (e.key === 'Escape') {
this.open = false;
if (header) {
header.focus();
}
}
};

return html`
<fluent-menu-item
@click=${clickFn}
@keydown=${keyDownFn}>
${opt}
</fluent-menu-item>`;
});
}
}
4 changes: 2 additions & 2 deletions packages/mgt-components/src/utils/SvgHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -634,8 +634,8 @@ export const getSvg = (svgIcon: SvgIcon, color?: string) => {

case SvgIcon.ChevronRight:
return html`
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true" xmlns="http://www.w3.org/2000/svg">
<path d="M8.46967 4.21967C8.17678 4.51256 8.17678 4.98744 8.46967 5.28033L15.1893 12L8.46967 18.7197C8.17678 19.0126 8.17678 19.4874 8.46967 19.7803C8.76256 20.0732 9.23744 20.0732 9.53033 19.7803L16.7803 12.5303C17.0732 12.2374 17.0732 11.7626 16.7803 11.4697L9.53033 4.21967C9.23744 3.92678 8.76256 3.92678 8.46967 4.21967Z" fill="none" />
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" xmlns="http://www.w3.org/2000/svg">
<path d="M8.46967 4.21967C8.17678 4.51256 8.17678 4.98744 8.46967 5.28033L15.1893 12L8.46967 18.7197C8.17678 19.0126 8.17678 19.4874 8.46967 19.7803C8.76256 20.0732 9.23744 20.0732 9.53033 19.7803L16.7803 12.5303C17.0732 12.2374 17.0732 11.7626 16.7803 11.4697L9.53033 4.21967C9.23744 3.92678 8.76256 3.92678 8.46967 4.21967Z" fill="currentColor" />
</svg>`;

case SvgIcon.Delete:
Expand Down

0 comments on commit 4747189

Please sign in to comment.