Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions packages/lexical-playground/__tests__/e2e/Mentions.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -968,8 +968,26 @@ test.describe('Mentions', () => {
await focusEditor(page);
await page.keyboard.type('@a');

// The outer element only positions the menu. The listbox is the <ul> that
// holds the options, because a listbox has to own its options directly for
// a screen reader to read them as choices and count their position.
const menuElement = page.locator('#typeahead-menu');
expect(await menuElement.getAttribute('aria-label')).toBe('Typeahead menu');
expect(await menuElement.getAttribute('role')).toBe('listbox');
expect(await menuElement.getAttribute('role')).toBe('presentation');

const listbox = page.locator('#typeahead-listbox');
expect(await listbox.getAttribute('role')).toBe('listbox');
expect(await listbox.getAttribute('aria-label')).toBe('Mentions');

// The options have to be the listbox's own children. The browser works
// out "1 of 5" from that relationship and passes it on; nothing in the
// markup states the position.
const options = listbox.locator('[role="option"]');
const count = await options.count();
expect(count).toBeGreaterThan(0);
for (let i = 0; i < count; i++) {
expect(await options.nth(i).evaluate(el => el.parentElement.id)).toBe(
'typeahead-listbox',
);
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ export default function AutoEmbedPlugin(): JSX.Element {
<>
{modal}
<LexicalAutoEmbedPlugin<PlaygroundEmbedConfig>
menuAriaLabel="Embed"
embedConfigs={EmbedConfigs}
onOpenEmbedModalForConfig={openEmbedModal}
getMenuOptions={getMenuOptions}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,7 @@ export default function ComponentPickerMenuPlugin(): JSX.Element {
<>
{modal}
<LexicalTypeaheadMenuPlugin<ComponentPickerOption>
menuAriaLabel="Blocks"
onQueryChange={setQueryString}
onSelectOption={onSelectOption}
triggerFn={checkForTriggerMatch}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@ class EmojiOption extends MenuOption {
title: string,
emoji: string,
options: {
ariaLabel?: string;
keywords?: string[];
},
) {
super(title);
this.title = title;
this.emoji = emoji;
this.ariaLabel = options.ariaLabel;
this.keywords = options.keywords || [];
}
}
Expand Down Expand Up @@ -67,6 +69,17 @@ export default function EmojiPickerPlugin() {
? emojis.map(
({emoji, aliases, tags}) =>
new EmojiOption(`${emoji} ${aliases[0]}`, emoji, {
// Announce the shortcode, not the glyph and not the
// dataset description.
//
// The glyph is announced from the screen reader's own
// dictionary, so leaving it in the name says the same emoji
// twice. The description reads well ("face savoring food") but
// is NOT searchable - only aliases and tags are matched - so a
// screen-reader user would hear a name they cannot type. The
// shortcode is what you search by, so it is what should be
// read; underscores become spaces so it is intelligible.
ariaLabel: aliases[0].replace(/_/g, ' '),
keywords: [...aliases, ...tags],
}),
)
Expand Down Expand Up @@ -123,6 +136,7 @@ export default function EmojiPickerPlugin() {

return (
<LexicalTypeaheadMenuPlugin
menuAriaLabel="Emojis"
onQueryChange={setQueryString}
onSelectOption={onSelectOption}
triggerFn={checkForTriggerMatch}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,7 @@ export function MentionsPlugin(): JSX.Element | null {

return (
<LexicalTypeaheadMenuPlugin<MentionTypeaheadOption>
menuAriaLabel="Mentions"
onQueryChange={setQueryString}
onSelectOption={onSelectOption}
triggerFn={checkForMentionMatch}
Expand Down
8 changes: 8 additions & 0 deletions packages/lexical-react/src/LexicalAutoEmbedPlugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,12 @@ type LexicalAutoEmbedPluginProps<TEmbedConfig extends EmbedConfig> = {
* By default, it displays a plain list with the option titles
*/
menuRenderFn?: MenuRenderFn<AutoEmbedOption>;
/**
* Accessible label for the menu.
* Screen readers will announce this when the menu opens.
* @default 'Typeahead menu'
*/
menuAriaLabel?: string;
/**
* Priority for key handling in the menu. The default is `COMMAND_PRIORITY_LOW`
*/
Expand Down Expand Up @@ -171,6 +177,7 @@ export function LexicalAutoEmbedPlugin<TEmbedConfig extends EmbedConfig>({
onOpenEmbedModalForConfig,
getMenuOptions,
menuRenderFn,
menuAriaLabel,
menuCommandPriority = COMMAND_PRIORITY_LOW,
}: LexicalAutoEmbedPluginProps<TEmbedConfig>): JSX.Element | null {
const [editor] = useLexicalComposerContext();
Expand Down Expand Up @@ -316,6 +323,7 @@ export function LexicalAutoEmbedPlugin<TEmbedConfig extends EmbedConfig>({
onSelectOption={onSelectOption}
options={options}
menuRenderFn={menuRenderFn}
menuAriaLabel={menuAriaLabel}
commandPriority={menuCommandPriority}
/>
) : null;
Expand Down
10 changes: 10 additions & 0 deletions packages/lexical-react/src/LexicalNodeMenuPlugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ export type NodeMenuPluginProps<TOption extends MenuOption> = {
anchorClassName?: string;
commandPriority?: CommandListenerPriority;
parent?: HTMLElement;
/**
* Accessible label for the menu.
* Screen readers will announce this when the menu opens.
* @default 'Typeahead menu'
*/
menuAriaLabel?: string;
};

/**
Expand All @@ -70,6 +76,7 @@ export function LexicalNodeMenuPlugin<TOption extends MenuOption>({
anchorClassName,
commandPriority = COMMAND_PRIORITY_LOW,
parent,
menuAriaLabel,
}: NodeMenuPluginProps<TOption>): JSX.Element | null {
const [editor] = useLexicalComposerContext();
const [resolution, setResolution] = useState<MenuResolution | null>(null);
Expand All @@ -78,6 +85,8 @@ export function LexicalNodeMenuPlugin<TOption extends MenuOption>({
setResolution,
anchorClassName,
parent,
true, // shouldIncludePageYOffset__EXPERIMENTAL
menuAriaLabel,
);

const closeNodeMenu = useCallback(() => {
Expand Down Expand Up @@ -136,6 +145,7 @@ export function LexicalNodeMenuPlugin<TOption extends MenuOption>({
resolution === null ||
editor === null ? null : (
<LexicalMenu
ariaLabel={menuAriaLabel}
close={closeNodeMenu}
resolution={resolution}
editor={editor}
Expand Down
10 changes: 10 additions & 0 deletions packages/lexical-react/src/LexicalTypeaheadMenuPlugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,12 @@ export type TypeaheadMenuPluginProps<TOption extends MenuOption> = {
parent?: HTMLElement;
preselectFirstItem?: boolean;
ignoreEntityBoundary?: boolean;
/**
* Accessible label for the menu container.
* Screen readers will announce this when the menu opens.
* @default 'Typeahead menu'
*/
menuAriaLabel?: string;
};

/**
Expand All @@ -237,6 +243,7 @@ export function LexicalTypeaheadMenuPlugin<TOption extends MenuOption>({
parent,
preselectFirstItem = true,
ignoreEntityBoundary = false,
menuAriaLabel,
}: TypeaheadMenuPluginProps<TOption>): JSX.Element | null {
const [editor] = useLexicalComposerContext();
const [resolution, setResolution] = useState<MenuResolution | null>(null);
Expand All @@ -245,6 +252,8 @@ export function LexicalTypeaheadMenuPlugin<TOption extends MenuOption>({
setResolution,
anchorClassName,
parent,
true, // shouldIncludePageYOffset__EXPERIMENTAL
menuAriaLabel,
);

const closeTypeahead = useCallback(() => {
Expand Down Expand Up @@ -361,6 +370,7 @@ export function LexicalTypeaheadMenuPlugin<TOption extends MenuOption>({
editor === null ||
anchorElementRef.current === null ? null : (
<LexicalMenu
ariaLabel={menuAriaLabel}
close={closeTypeahead}
resolution={resolution}
editor={editor}
Expand Down
Loading
Loading