Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Descriptive aria-labels for Keyboard shortcuts which include punctuation previously ignored by screen readers #666

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
47 changes: 47 additions & 0 deletions packages/commands/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1282,6 +1282,53 @@ export namespace CommandRegistry {
mods.push(key);
return mods.join(' ');
}

/**
* Format keystroke aria labels.
*
* If a list of keystrokes includes puntuation, it will create an
* aria label and substitue symbol values to text.
*
* @param keystroke The keystrokes to format
* @param keyToText - Optional object for converting punctuation.
* @returns The keystrokes representation
*/
export function formatKeystrokeAriaLabel(
keystroke: readonly string[],
keyToText?: { [key: string]: string }
): string {
const internalKeyToText = keyToText ?? {
']': 'Closing bracket',
'[': 'Opening bracket',
',': 'Comma',
'.': 'Full stop',
"'": 'Single quote',
'-': 'Hyphen-minus'
};

let result = CommandRegistry.formatKeystroke(keystroke);

let punctuationRegex = /\p{P}/u;
let punctuations = result?.match(punctuationRegex);
if (!punctuations) {
return result;
}
for (const punctuation of punctuations) {
if (
result != null &&
Object.keys(internalKeyToText).includes(punctuation)
) {
const individualKeys = result.split('+');
let index = individualKeys.indexOf(punctuation);
if (index != -1) {
individualKeys[index] = internalKeyToText[punctuation];
}
const textShortcut = individualKeys.join('+');
return textShortcut;
}
}
return result;
}
}

/**
Expand Down
35 changes: 34 additions & 1 deletion packages/widgets/src/commandpalette.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export class CommandPalette extends Widget {
super({ node: Private.createNode() });
this.addClass('lm-CommandPalette');
this.setFlag(Widget.Flag.DisallowLayout);
this.keyToText = options.renderer?.keyToText;
this.commands = options.commands;
this.renderer = options.renderer || CommandPalette.defaultRenderer;
this.commands.commandChanged.connect(this._onGenericChange, this);
Expand All @@ -63,6 +64,10 @@ export class CommandPalette extends Widget {
* The renderer used by the command palette.
*/
readonly renderer: CommandPalette.IRenderer;
/**
* The optional object used for translation of aria label punctuation.
*/
readonly keyToText: CommandPalette.IRenderer['keyToText'];

/**
* The command palette search node.
Expand Down Expand Up @@ -750,12 +755,21 @@ export namespace CommandPalette {
* @returns A virtual element representing the message.
*/
renderEmptyMessage(data: IEmptyMessageRenderData): VirtualElement;

/**
* The optional object used for translation of aria label punctuation.
*/
keyToText?: { [key: string]: string };
}

/**
* The default implementation of `IRenderer`.
*/
export class Renderer implements IRenderer {
/**
* The optional object used for translation of aria label punctuation.
*/
keyToText?: { [key: string]: string };
/**
* Render the virtual element for a command palette header.
*
Expand Down Expand Up @@ -877,7 +891,14 @@ export namespace CommandPalette {
*/
renderItemShortcut(data: IItemRenderData): VirtualElement {
let content = this.formatItemShortcut(data);
return h.div({ className: 'lm-CommandPalette-itemShortcut' }, content);
let ariaContent = this.formatItemAria(data);
return h.div(
{
className: 'lm-CommandPalette-itemShortcut',
'aria-label': `${ariaContent}`
},
content
);
}

/**
Expand Down Expand Up @@ -973,6 +994,18 @@ export namespace CommandPalette {
return kb ? CommandRegistry.formatKeystroke(kb.keys) : null;
}

/**
* @param data - The data to use for the aria label content.
*
* @returns The aria label content to add to the shortcut node.
*/
formatItemAria(data: IItemRenderData): h.Child {
let kbText = data.item.keyBinding;
return kbText
? CommandRegistry.formatKeystrokeAriaLabel(kbText.keys, this.keyToText)
: null;
}

/**
* Create the render content for the item label node.
*
Expand Down
35 changes: 34 additions & 1 deletion packages/widgets/src/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export class Menu extends Widget {
super({ node: Private.createNode() });
this.addClass('lm-Menu');
this.setFlag(Widget.Flag.DisallowLayout);
this.keyToText = options.renderer?.keyToText;
this.commands = options.commands;
this.renderer = options.renderer || Menu.defaultRenderer;
}
Expand All @@ -64,6 +65,10 @@ export class Menu extends Widget {
this._items.length = 0;
super.dispose();
}
/**
* The optional object used for translation of aria label punctuation.
*/
readonly keyToText: Menu.IRenderer['keyToText'];

/**
* A signal emitted just before the menu is closed.
Expand Down Expand Up @@ -1151,6 +1156,10 @@ export namespace Menu {
* @returns A virtual element representing the item.
*/
renderItem(data: IRenderData): VirtualElement;
/**
* The optional object used for translation of aria label punctuation.
*/
keyToText?: { [key: string]: string };
}

/**
Expand All @@ -1160,6 +1169,10 @@ export namespace Menu {
* Subclasses are free to reimplement rendering methods as needed.
*/
export class Renderer implements IRenderer {
/**
* The optional object used for translation of aria label punctuation.
*/
keyToText?: { [key: string]: string };
/**
* Render the virtual element for a menu item.
*
Expand Down Expand Up @@ -1221,7 +1234,14 @@ export namespace Menu {
*/
renderShortcut(data: IRenderData): VirtualElement {
let content = this.formatShortcut(data);
return h.div({ className: 'lm-Menu-itemShortcut' }, content);
let ariaContent = this.formatShortcutText(data);
return h.div(
{
className: 'lm-Menu-itemShortcut',
'aria-label': `${ariaContent}`
},
content
);
}

/**
Expand Down Expand Up @@ -1371,6 +1391,19 @@ export namespace Menu {
let kb = data.item.keyBinding;
return kb ? CommandRegistry.formatKeystroke(kb.keys) : null;
}

/**
* @param data - The data to use for the aria label content.
*
* @returns The aria label content to add to the shortcut node.
*/
formatShortcutText(data: IRenderData): h.Child {
let kbText = data.item.keyBinding;

return kbText
? CommandRegistry.formatKeystrokeAriaLabel(kbText.keys, this.keyToText)
: null;
}
}

/**
Expand Down
47 changes: 47 additions & 0 deletions packages/widgets/tests/src/commandpalette.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,20 @@ describe('@lumino/widgets', () => {
keys: ['Ctrl A'],
selector: 'body'
});
commands.addCommand('test-aria', {
label: 'Test Aria',
caption: 'A simple aria-label test',
className: 'testAriaClass',
isEnabled: () => enabledFlag,
isToggled: () => toggledFlag,
execute: () => {}
});
commands.addKeyBinding({
command: 'test-aria',
keys: ['Ctrl ,'],
selector: 'body'
});

item = palette.addItem({
command: 'test',
category: 'Test Category'
Expand Down Expand Up @@ -827,6 +841,39 @@ describe('@lumino/widgets', () => {
expect(child).to.equal('A simple test command');
});
});

describe('#formatItemShortcut()', () => {
it('should format the item shortcut text', () => {
let child = renderer.formatItemShortcut({
item,
indices: null,
active: false
});
if (Platform.IS_MAC) {
expect(child).to.equal('\u2303 A');
} else {
expect(child).to.equal('Ctrl+A');
}
});
});
describe('#formatItemAria', () => {
it('should format the item aria-label', () => {
let item = palette.addItem({
command: 'test-aria',
category: 'Test Category'
});
let child = renderer.formatItemAria({
item,
indices: null,
active: false
});
if (Platform.IS_MAC) {
expect(child).to.equal('\u2303 ,');
} else {
expect(child).to.equal('Ctrl+Comma');
}
});
});
});
});
});
31 changes: 31 additions & 0 deletions packages/widgets/tests/src/menu.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,17 @@ describe('@lumino/widgets', () => {
className: 'testClass',
mnemonic: 0
});
commands.addCommand('test-aria', {
execute: (args: JSONObject) => {
executed = 'test-aria';
},
label: 'Test Aria Label',
icon: iconRenderer,
iconClass,
caption: 'Test Caption',
className: 'testClass',
mnemonic: 0
});
commands.addCommand('test-toggled', {
execute: (args: JSONObject) => {
executed = 'test-toggled';
Expand Down Expand Up @@ -127,6 +138,11 @@ describe('@lumino/widgets', () => {
selector: 'body',
command: 'test'
});
commands.addKeyBinding({
keys: ['Ctrl ,'],
selector: 'body',
command: 'test-aria'
});
});

beforeEach(() => {
Expand Down Expand Up @@ -1591,6 +1607,21 @@ describe('@lumino/widgets', () => {
expect(child).to.equal('Ctrl+T');
}
});
describe('#formatShortcutText()', () => {
it('should format the item aria-label', () => {
let item = menu.addItem({ command: 'test-aria' });
let child = renderer.formatShortcutText({
item,
active: false,
collapsed: false
});
if (Platform.IS_MAC) {
expect(child).to.equal('\u2303 ,');
} else {
expect(child).to.equal('Ctrl+Comma');
}
});
});
});
});
});
Expand Down
3 changes: 3 additions & 0 deletions review/api/commands.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ export namespace CommandRegistry {
args: ReadonlyJSONObject | null;
};
export function formatKeystroke(keystroke: string | readonly string[]): string;
export function formatKeystrokeAriaLabel(keystroke: readonly string[], keyToText?: {
[key: string]: string;
}): string;
export interface ICommandChangedArgs {
readonly id: string | undefined;
readonly type: 'added' | 'removed' | 'changed' | 'many-changed';
Expand Down
18 changes: 18 additions & 0 deletions review/api/widgets.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ export class CommandPalette extends Widget {
handleEvent(event: Event): void;
get inputNode(): HTMLInputElement;
get items(): ReadonlyArray<CommandPalette.IItem>;
readonly keyToText: CommandPalette.IRenderer['keyToText'];
protected onActivateRequest(msg: Message): void;
protected onAfterDetach(msg: Message): void;
protected onAfterShow(msg: Message): void;
Expand Down Expand Up @@ -235,6 +236,9 @@ export namespace CommandPalette {
renderer?: IRenderer;
}
export interface IRenderer {
keyToText?: {
[key: string]: string;
};
renderEmptyMessage(data: IEmptyMessageRenderData): VirtualElement;
renderHeader(data: IHeaderRenderData): VirtualElement;
renderItem(data: IItemRenderData): VirtualElement;
Expand All @@ -245,9 +249,14 @@ export namespace CommandPalette {
createItemDataset(data: IItemRenderData): ElementDataset;
formatEmptyMessage(data: IEmptyMessageRenderData): h.Child;
formatHeader(data: IHeaderRenderData): h.Child;
// (undocumented)
formatItemAria(data: IItemRenderData): h.Child;
formatItemCaption(data: IItemRenderData): h.Child;
formatItemLabel(data: IItemRenderData): h.Child;
formatItemShortcut(data: IItemRenderData): h.Child;
keyToText?: {
[key: string]: string;
};
renderEmptyMessage(data: IEmptyMessageRenderData): VirtualElement;
renderHeader(data: IHeaderRenderData): VirtualElement;
renderItem(data: IItemRenderData): VirtualElement;
Expand Down Expand Up @@ -695,6 +704,7 @@ export class Menu extends Widget {
handleEvent(event: Event): void;
insertItem(index: number, options: Menu.IItemOptions): Menu.IItem;
get items(): ReadonlyArray<Menu.IItem>;
readonly keyToText: Menu.IRenderer['keyToText'];
get leafMenu(): Menu;
get menuRequested(): ISignal<this, 'next' | 'previous'>;
protected onActivateRequest(msg: Message): void;
Expand Down Expand Up @@ -753,6 +763,9 @@ export namespace Menu {
readonly onfocus?: () => void;
}
export interface IRenderer {
keyToText?: {
[key: string]: string;
};
renderItem(data: IRenderData): VirtualElement;
}
export type ItemType = 'command' | 'submenu' | 'separator';
Expand All @@ -763,6 +776,11 @@ export namespace Menu {
createItemDataset(data: IRenderData): ElementDataset;
formatLabel(data: IRenderData): h.Child;
formatShortcut(data: IRenderData): h.Child;
// (undocumented)
formatShortcutText(data: IRenderData): h.Child;
keyToText?: {
[key: string]: string;
};
renderIcon(data: IRenderData): VirtualElement;
renderItem(data: IRenderData): VirtualElement;
renderLabel(data: IRenderData): VirtualElement;
Expand Down