Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "ContextualMenu: focused div when menu is opened now has role = menu",
"packageName": "@fluentui/react",
"email": "tristan.watanabe@gmail.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "applying package updates",
"packageName": "office-ui-fabric-react",
"email": "rongqizhou@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -3308,6 +3308,8 @@ export interface IContextualMenuItemStyles extends IButtonStyles {

// @public (undocumented)
export interface IContextualMenuListProps {
// (undocumented)
ariaLabel?: string;
// (undocumented)
defaultMenuItemRenderer: (item: IContextualMenuItemRenderProps) => React.ReactNode;
// (undocumented)
Expand All @@ -3317,6 +3319,8 @@ export interface IContextualMenuListProps {
// (undocumented)
items: IContextualMenuItem[];
// (undocumented)
labelElementId?: string;
// (undocumented)
role?: string;
// (undocumented)
totalItemCount: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1000,8 +1000,8 @@ describe('Button', () => {
const contextualMenuElement = buildRenderAndClickButtonAndReturnContextualMenuDOMElement(null, 'Button Text');

expect(contextualMenuElement).not.toBeNull();
expect(contextualMenuElement.getAttribute('aria-label')).toBeNull();
expect(contextualMenuElement.getAttribute('aria-labelledBy')).toBeDefined();
expect(contextualMenuElement?.getAttribute('aria-label')).toBeNull();
expect(contextualMenuElement?.getAttribute('aria-labelledBy')).toBeTruthy();
});

it('If button has a text child, contextual menu has aria-labelledBy attribute set', () => {
Expand All @@ -1012,16 +1012,16 @@ describe('Button', () => {
);

expect(contextualMenuElement).not.toBeNull();
expect(contextualMenuElement.getAttribute('aria-label')).toBeNull();
expect(contextualMenuElement.getAttribute('aria-labelledBy')).not.toBeNull();
expect(contextualMenuElement?.getAttribute('aria-label')).toBeNull();
expect(contextualMenuElement?.getAttribute('aria-labelledBy')).not.toBeNull();
});

it('If button has no text, contextual menu has no aria-label or aria-labelledBy attributes', () => {
const contextualMenuElement = buildRenderAndClickButtonAndReturnContextualMenuDOMElement();

expect(contextualMenuElement).not.toBeNull();
expect(contextualMenuElement.getAttribute('aria-label')).toBeNull();
expect(contextualMenuElement.getAttribute('aria-labelledBy')).toBeNull();
expect(contextualMenuElement?.getAttribute('aria-label')).toBeNull();
expect(contextualMenuElement?.getAttribute('aria-labelledBy')).toBeNull();
});

it('If button has text but ariaLabel provided in menuProps, contextual menu has aria-label set', () => {
Expand All @@ -1032,8 +1032,8 @@ describe('Button', () => {
);

expect(contextualMenuElement).not.toBeNull();
expect(contextualMenuElement.getAttribute('aria-label')).toEqual(explicitLabel);
expect(contextualMenuElement.getAttribute('aria-labelledBy')).toBeNull();
expect(contextualMenuElement?.getAttribute('aria-label')).toEqual(explicitLabel);
expect(contextualMenuElement?.getAttribute('aria-labelledBy')).toBeNull();
});
it('Click on button opens the menu, escape press dismisses menu', () => {
const callbackMock = jest.fn();
Expand Down Expand Up @@ -1075,8 +1075,8 @@ describe('Button', () => {
);

expect(contextualMenuElement).not.toBeNull();
expect(contextualMenuElement.getAttribute('aria-label')).toBeNull();
expect(contextualMenuElement.getAttribute('aria-labelledBy')).toEqual(explicitLabelElementId);
expect(contextualMenuElement?.getAttribute('aria-label')).toBeNull();
expect(contextualMenuElement?.getAttribute('aria-labelledBy')).toEqual(explicitLabelElementId);
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -359,9 +359,6 @@ export class ContextualMenuBase extends React.Component<IContextualMenuProps, IC
hidden={this.props.hidden}
>
<div
aria-label={ariaLabel}
aria-labelledby={labelElementId}
role={'menu'}
style={contextMenuStyle}
ref={(host: HTMLDivElement) => (this._host = host)}
id={id}
Expand All @@ -370,6 +367,9 @@ export class ContextualMenuBase extends React.Component<IContextualMenuProps, IC
onKeyDown={this._onMenuKeyDown}
onKeyUp={this._onKeyUp}
onFocusCapture={this._onMenuFocusCapture}
aria-label={ariaLabel}
aria-labelledby={labelElementId}
role={'menu'}
>
{title && <div className={this._classNames.title}> {title} </div>}
{items && items.length ? (
Expand All @@ -381,11 +381,13 @@ export class ContextualMenuBase extends React.Component<IContextualMenuProps, IC
>
{onRenderMenuList(
{
ariaLabel,
items,
totalItemCount,
hasCheckmarks,
hasIcons,
defaultMenuItemRenderer: this._defaultMenuItemRenderer,
labelElementId,
},
this._onRenderMenuList,
)}
Expand Down Expand Up @@ -493,6 +495,7 @@ export class ContextualMenuBase extends React.Component<IContextualMenuProps, IC
): JSX.Element => {
let indexCorrection = 0;
const { items, totalItemCount, hasCheckmarks, hasIcons } = menuListProps;

return (
<ul className={this._classNames.list} onKeyDown={this._onKeyDown} onKeyUp={this._onKeyUp} role={'presentation'}>
{items.map((item, index) => {
Expand Down Expand Up @@ -667,7 +670,7 @@ export class ContextualMenuBase extends React.Component<IContextualMenuProps, IC
return (
<li role="presentation" key={sectionProps.key || sectionItem.key || `section-${index}`}>
<div {...groupProps}>
<ul className={this._classNames.list} role="menu">
<ul className={this._classNames.list} role="presentation">
{sectionProps.topDivider && this._renderSeparator(index, menuClassNames, true, true)}
{headerItem &&
this._renderListItem(headerItem, sectionItem.key || index, menuClassNames, sectionItem.title)}
Expand Down Expand Up @@ -827,7 +830,6 @@ export class ContextualMenuBase extends React.Component<IContextualMenuProps, IC
executeItemClick={this._executeItemClick}
onItemClick={this._onAnchorClick}
onItemKeyDown={this._onItemKeyDown}
getSubMenuId={this._getSubMenuId}
expandedMenuItemKey={expandedMenuItemKey}
openSubMenu={this._onItemSubMenuExpand}
dismissSubMenu={this._onSubMenuDismiss}
Expand Down Expand Up @@ -867,7 +869,6 @@ export class ContextualMenuBase extends React.Component<IContextualMenuProps, IC
onItemClick={this._onItemClick}
onItemClickBase={this._onItemClickBase}
onItemKeyDown={this._onItemKeyDown}
getSubMenuId={this._getSubMenuId}
expandedMenuItemKey={expandedMenuItemKey}
openSubMenu={this._onItemSubMenuExpand}
dismissSubMenu={this._onSubMenuDismiss}
Expand Down Expand Up @@ -1382,16 +1383,6 @@ export class ContextualMenuBase extends React.Component<IContextualMenuProps, IC
}
}

private _getSubMenuId = (item: IContextualMenuItem): string | undefined => {
let { subMenuId } = this.state;

if (item.subMenuProps && item.subMenuProps.id) {
subMenuId = item.subMenuProps.id;
}

return subMenuId;
};

private _onPointerAndTouchEvent = (ev: React.TouchEvent<HTMLElement> | PointerEvent) => {
this._cancelSubMenuTimer();
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -484,35 +484,6 @@ describe('ContextualMenu', () => {
expect(document.querySelector('.is-expanded')).toBeTruthy();
});

it('sets the correct aria-owns attribute for the submenu', () => {
const submenuId = 'testSubmenuId';
const items: IContextualMenuItem[] = [
{
text: 'TestText 1',
key: 'TestKey1',
subMenuProps: {
id: submenuId,
items: [
{
text: 'SubmenuText 1',
key: 'SubmenuKey1',
className: 'SubMenuClass',
},
],
},
},
];

ReactTestUtils.renderIntoDocument<IContextualMenuProps>(<ContextualMenu items={items} />);

const parentMenuItem = document.querySelector('button.ms-ContextualMenu-link') as HTMLButtonElement;
ReactTestUtils.Simulate.click(parentMenuItem);
const childMenu = document.getElementById(submenuId);

expect(childMenu!.id).toBe(submenuId);
expect(parentMenuItem.getAttribute('aria-owns')).toBe(submenuId);
});

it('can focus on disabled items', () => {
const items: IContextualMenuItem[] = [
{
Expand Down Expand Up @@ -926,8 +897,10 @@ describe('ContextualMenu', () => {
// Mock createPortal to capture its component hierarchy in snapshot output.
const ReactDOM = require('react-dom');
const createPortal = ReactDOM.createPortal;
ReactDOM.createPortal = jest.fn(element => {
return element;
ReactTestUtils.act(() => {
ReactDOM.createPortal = jest.fn(element => {
return element;
});
});
const buttonRef = React.createRef<IButton>();
const component = renderer.create(
Expand Down Expand Up @@ -1271,15 +1244,16 @@ describe('ContextualMenu', () => {
});

describe('onRenderMenuList function tests', () => {
it('List has default role as presentation', () => {
it('List has default role as presentation.', () => {
const items: IContextualMenuItem[] = [
{
text: 'TestText 1',
key: 'TestKey1',
},
];

ReactTestUtils.renderIntoDocument<IContextualMenuProps>(<ContextualMenu items={items} />);
ReactTestUtils.act(() => {
ReactTestUtils.renderIntoDocument<IContextualMenuProps>(<ContextualMenu items={items} />);
});

const internalList = document.querySelector('ul.ms-ContextualMenu-list') as HTMLUListElement;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,8 @@ export interface IContextualMenuListProps {
hasCheckmarks: boolean;
hasIcons: boolean;
defaultMenuItemRenderer: (item: IContextualMenuItemRenderProps) => React.ReactNode;
ariaLabel?: string;
labelElementId?: string;
role?: string;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ export class ContextualMenuAnchor extends ContextualMenuItemWrapper {
anchorRel = anchorRel ? anchorRel : 'nofollow noopener noreferrer'; // Safe default to prevent tabjacking
}

const subMenuId = this._getSubMenuId(item);
const itemHasSubmenu = hasSubmenu(item);
const nativeProps = getNativeProps<React.HTMLAttributes<HTMLAnchorElement>>(item, anchorProperties);
const disabled = isItemDisabled(item);
Expand Down Expand Up @@ -73,7 +72,6 @@ export class ContextualMenuAnchor extends ContextualMenuItemWrapper {
rel={anchorRel}
className={classNames.root}
role="menuitem"
aria-owns={item.key === expandedMenuItemKey ? subMenuId : undefined}
aria-haspopup={itemHasSubmenu || undefined}
aria-expanded={itemHasSubmenu ? item.key === expandedMenuItemKey : undefined}
aria-posinset={focusableElementIndex + 1}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,6 @@ export class ContextualMenuButton extends ContextualMenuItemWrapper {
dismissMenu,
} = this.props;

const subMenuId = this._getSubMenuId(item);

const isChecked: boolean | null | undefined = getIsChecked(item);
const canCheck: boolean = isChecked !== null;
const defaultRole = getMenuItemAriaRole(item);
Expand Down Expand Up @@ -73,7 +71,6 @@ export class ContextualMenuButton extends ContextualMenuItemWrapper {
'aria-label': ariaLabel,
'aria-describedby': ariaDescribedByIds,
'aria-haspopup': itemHasSubmenu || undefined,
'aria-owns': item.key === expandedMenuItemKey ? subMenuId : undefined,
'aria-expanded': itemHasSubmenu ? item.key === expandedMenuItemKey : undefined,
'aria-posinset': focusableElementIndex + 1,
'aria-setsize': totalItemCount,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as React from 'react';
import { initializeComponentRef, shallowCompare } from '../../../Utilities';
import { IContextualMenuItemWrapperProps } from './ContextualMenuItemWrapper.types';
import { IContextualMenuItem } from '../../../ContextualMenu';

export class ContextualMenuItemWrapper extends React.Component<IContextualMenuItemWrapperProps> {
constructor(props: IContextualMenuItemWrapperProps) {
Expand Down Expand Up @@ -48,13 +47,6 @@ export class ContextualMenuItemWrapper extends React.Component<IContextualMenuIt
}
};

protected _getSubMenuId = (item: IContextualMenuItem): string | undefined => {
const { getSubMenuId } = this.props;
if (getSubMenuId) {
return getSubMenuId(item);
}
};

protected _getSubmenuTarget = (): HTMLElement | undefined => {
return undefined;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,9 @@ export interface IContextualMenuItemWrapperProps extends React.ClassAttributes<I

/**
* Callback to get the subMenu ID for an IContextualMenuItem.
* @deprecated ID relationship between a menu button and menu isn't necessary
*/
// eslint-disable-next-line deprecation/deprecation
getSubMenuId?: (item: IContextualMenuItem) => string | undefined;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ exports[`ContextualMenu ContextualMenu snapshot ContextualMenu should be present
&:focus {
outline: 0px;
}
id="id__132-menu"
id="id__127-menu"
onFocusCapture={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
Expand All @@ -286,7 +286,7 @@ exports[`ContextualMenu ContextualMenu snapshot ContextualMenu should be present
font-weight: 400;
min-width: 180px;
}
data-focuszone-id="FocusZone136"
data-focuszone-id="FocusZone131"
onFocus={[Function]}
onKeyDown={[Function]}
onMouseDownCapture={[Function]}
Expand Down