diff --git a/common/changes/office-ui-fabric-react/BaseButton_ContextMenu_AriaLabelledBy_2018-05-23-16-36.json b/common/changes/office-ui-fabric-react/BaseButton_ContextMenu_AriaLabelledBy_2018-05-23-16-36.json new file mode 100644 index 00000000000000..92c58af4905aca --- /dev/null +++ b/common/changes/office-ui-fabric-react/BaseButton_ContextMenu_AriaLabelledBy_2018-05-23-16-36.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "office-ui-fabric-react", + "comment": "BaseButton sometimes has aria-labelledBy pointing to element that isn't in the DOM", + "type": "patch" + } + ], + "packageName": "office-ui-fabric-react", + "email": "rpsenski@microsoft.com" +} \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/Button/BaseButton.tsx b/packages/office-ui-fabric-react/src/components/Button/BaseButton.tsx index 01af263a9b64e8..fe9d56983bb5e7 100644 --- a/packages/office-ui-fabric-react/src/components/Button/BaseButton.tsx +++ b/packages/office-ui-fabric-react/src/components/Button/BaseButton.tsx @@ -347,7 +347,7 @@ export class BaseButton extends BaseComponent { const { children } = this.props; @@ -427,6 +434,13 @@ export class BaseButton extends BaseComponent { const { onDismiss = this._dismissMenu } = menuProps; + // the accessible menu label (accessible name) has a relationship to the button. + // If the menu props do not specify an explicit value for aria-label or aria-labelledBy, + // AND the button has text, we'll set the menu aria-labelledBy to the text element id. + if (!menuProps.ariaLabel && !menuProps.labelElementId && this._hasText()) { + menuProps = { ...menuProps, labelElementId: this._labelId }; + } + return ( ); diff --git a/packages/office-ui-fabric-react/src/components/Button/Button.test.tsx b/packages/office-ui-fabric-react/src/components/Button/Button.test.tsx index 205cf98bc14388..0688542992bedf 100644 --- a/packages/office-ui-fabric-react/src/components/Button/Button.test.tsx +++ b/packages/office-ui-fabric-react/src/components/Button/Button.test.tsx @@ -459,7 +459,7 @@ describe('Button', () => { expect(menuButtonDOM.getAttribute('aria-expanded')).toEqual('false'); }); - it('If menu trigger is specefied, default key is overridden', () => { + it('If menu trigger is specified, default key is overridden', () => { const button = ReactTestUtils.renderIntoDocument( { expect(didClick).toEqual(false); }); }); + + describe('with contextual menu', () => { + function buildRenderAndClickButtonAndReturnContextualMenuDOMElement(menuPropsPatch?: any, text?: string, textAsChildElement: boolean = false): HTMLElement { + const menuProps = { items: [{ key: 'item', name: 'Item' }], ...menuPropsPatch }; + const element: React.ReactElement = ( + + { textAsChildElement && text ? text : null } + + ); + + const button = ReactTestUtils.renderIntoDocument(element) as React.ReactInstance; + const renderedDOM = ReactDOM.findDOMNode(button) as HTMLElement; + + expect(renderedDOM).toBeDefined(); + ReactTestUtils.Simulate.click(renderedDOM); + + // get the menu id from the button's aria attribute + const menuId = renderedDOM.getAttribute('aria-owns'); + expect(menuId).toBeDefined(); + + const menuDOM = renderedDOM.ownerDocument.getElementById(menuId as string); + expect(menuDOM).toBeDefined(); + + return menuDOM as HTMLElement; + } + + it('If button has text, contextual menu has aria-labelledBy attribute set', () => { + const contextualMenuElement = buildRenderAndClickButtonAndReturnContextualMenuDOMElement(null, 'Button Text'); + + expect(contextualMenuElement).not.toBeNull(); + expect(contextualMenuElement.getAttribute('aria-label') === null); + expect(contextualMenuElement.getAttribute('aria-labelledBy') !== null); + }); + + it('If button has a text child, contextual menu has aria-labelledBy attribute set', () => { + const contextualMenuElement = buildRenderAndClickButtonAndReturnContextualMenuDOMElement(null, 'Button Text', true); + + expect(contextualMenuElement).not.toBeNull(); + expect(contextualMenuElement.getAttribute('aria-label') === null); + expect(contextualMenuElement.getAttribute('aria-labelledBy') !== null); + }); + + 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') === null); + expect(contextualMenuElement.getAttribute('aria-labelledBy') === null); + }); + + it('If button has text but ariaLabel provided in menuProps, contextual menu has aria-label set', () => { + const explicitLabel = 'ExplicitLabel'; + const contextualMenuElement = buildRenderAndClickButtonAndReturnContextualMenuDOMElement({ ariaLabel: explicitLabel }, 'Button Text'); + + expect(contextualMenuElement).not.toBeNull(); + expect(contextualMenuElement.getAttribute('aria-label') === explicitLabel); + expect(contextualMenuElement.getAttribute('aria-labelledBy') === null); + }); + + it('If button has text but labelElementId provided in menuProps, contextual menu has aria-labelledBy reflecting labelElementId', () => { + const explicitLabelElementId = 'id_ExplicitLabel'; + const contextualMenuElement = buildRenderAndClickButtonAndReturnContextualMenuDOMElement({ labelElementId: explicitLabelElementId }, 'Button Text'); + + expect(contextualMenuElement).not.toBeNull(); + expect(contextualMenuElement.getAttribute('aria-label') === null); + expect(contextualMenuElement.getAttribute('aria-labelledBy') === explicitLabelElementId); + }); + }); }); });