diff --git a/src/components/Callout/Callout.Props.ts b/src/components/Callout/Callout.Props.ts index 9135fea56d796..3bd5f089c5a0e 100644 --- a/src/components/Callout/Callout.Props.ts +++ b/src/components/Callout/Callout.Props.ts @@ -59,6 +59,15 @@ export interface ICalloutProps extends React.Props, IPositionProps { * If true do not render on a new layer. If false render on a new layer. */ doNotLayer?: boolean; + + /** + * If true then the callout will attempt to focus the first focusable element that it contains. + * If it doesn't find an element, no focus will be set and the method will return false. + * This means that it's the contents responsibility to either set focus or have + * focusable items. + * @returns True if focus was set, false if it was not. + */ + setInitialFocus?: boolean; } export interface ILink { diff --git a/src/components/Callout/Callout.tsx b/src/components/Callout/Callout.tsx index 4c2c0b7c8c6fb..aaa6a7793df6a 100644 --- a/src/components/Callout/Callout.tsx +++ b/src/components/Callout/Callout.tsx @@ -5,6 +5,8 @@ import { Layer } from '../../Layer'; import { css } from '../../utilities/css'; import { EventGroup } from '../../utilities/eventGroup/EventGroup'; import { getRelativePositions, IPositionInfo } from '../../utilities/positioning'; +import { focusFirstChild } from '../../utilities/focus'; +import { Popup } from '../Popup/index'; import './Callout.scss'; const BEAK_ORIGIN_POSITION = { top: 0, left: 0 }; @@ -69,9 +71,12 @@ export class Callout extends React.Component { ref={ (callout: HTMLDivElement) => this._calloutElement = callout } > { isBeakVisible && targetElement ? (
) : (null) } -
+ this.dismiss() } + shouldRestoreFocus={ true }> { children } -
+
); @@ -110,6 +115,10 @@ export class Callout extends React.Component { this._events.on(window, 'focus', this._dismissOnLostFocus, true); this._events.on(window, 'click', this._dismissOnLostFocus, true); + if (this.props.setInitialFocus) { + focusFirstChild(this._calloutElement); + } + if (this.props.onLayerMounted) { this.props.onLayerMounted(); } diff --git a/src/components/Popup/Popup.tsx b/src/components/Popup/Popup.tsx index 1c76f88641c01..3ad48e8c73ed3 100644 --- a/src/components/Popup/Popup.tsx +++ b/src/components/Popup/Popup.tsx @@ -45,12 +45,13 @@ export class Popup extends BaseComponent { return (
+ aria-desribedby={ ariaDescribedBy }> + { this.props.children } +
); } diff --git a/src/demo/pages/CalloutPage/examples/Callout.Basic.Example.tsx b/src/demo/pages/CalloutPage/examples/Callout.Basic.Example.tsx index 3b2199c1e0b0e..c0384ed99fcd7 100644 --- a/src/demo/pages/CalloutPage/examples/Callout.Basic.Example.tsx +++ b/src/demo/pages/CalloutPage/examples/Callout.Basic.Example.tsx @@ -38,6 +38,7 @@ export class CalloutBasicExample extends React.Component

diff --git a/src/demo/pages/CalloutPage/examples/Callout.Nested.Example.tsx b/src/demo/pages/CalloutPage/examples/Callout.Nested.Example.tsx index 1dd84373e5ca3..ad10fde4e5dd5 100644 --- a/src/demo/pages/CalloutPage/examples/Callout.Nested.Example.tsx +++ b/src/demo/pages/CalloutPage/examples/Callout.Nested.Example.tsx @@ -38,7 +38,8 @@ export class CalloutNestedExample extends React.Component { this._onDismiss(ev); } } + onDismiss={ (ev: any) => { this._onDismiss(ev); } } + setInitialFocus={ true } >

diff --git a/src/utilities/focus.ts b/src/utilities/focus.ts index 7cde3cad0de6c..a77f3181031ba 100644 --- a/src/utilities/focus.ts +++ b/src/utilities/focus.ts @@ -20,6 +20,22 @@ export function getLastFocusable( return getPreviousElement(rootElement, currentElement, true, false, true, includeElementsInFocusZones); } +/** + * Attempts to focus the first focusable element that is a child or child's child of the rootElement. + * @return True if focus was set, false if it was not. + * @param {HTMLElement} rootElement - element to start the search for a focusable child. + */ +export function focusFirstChild( + rootElement: HTMLElement): boolean { + let element: HTMLElement = getNextElement(rootElement, rootElement, true, false, false, true); + + if (element) { + element.focus(); + return true; + } + return false; +} + /** Traverse to find the previous element. */ export function getPreviousElement( rootElement: HTMLElement,