From 3545de25ffae10ed6862f8eb97de7c7910bf7f07 Mon Sep 17 00:00:00 2001 From: Jon Schectman Date: Fri, 19 Aug 2016 11:34:30 -0700 Subject: [PATCH 1/2] Added initial focus to callout --- src/components/Callout/Callout.Props.ts | 8 ++++++++ src/components/Callout/Callout.tsx | 13 +++++++++++-- src/components/Popup/Popup.tsx | 5 +++-- .../CalloutPage/examples/Callout.Basic.Example.tsx | 1 + .../examples/Callout.Nested.Example.tsx | 3 ++- src/utilities/focus.ts | 14 ++++++++++++++ 6 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/components/Callout/Callout.Props.ts b/src/components/Callout/Callout.Props.ts index 9135fea56d796a..9fef8081cb59f5 100644 --- a/src/components/Callout/Callout.Props.ts +++ b/src/components/Callout/Callout.Props.ts @@ -59,6 +59,14 @@ 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 does not find an element then focus will not be given to the callout. + * This means that it's the contents responsibility to either set focus or have + * focusable items. + */ + setInitialFocus?: boolean; } export interface ILink { diff --git a/src/components/Callout/Callout.tsx b/src/components/Callout/Callout.tsx index 4c2c0b7c8c6fbb..f45591a49d1e16 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 { focusFirstFocusable } 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) { + focusFirstFocusable(this._calloutElement, this._calloutElement, true); + } + if (this.props.onLayerMounted) { this.props.onLayerMounted(); } diff --git a/src/components/Popup/Popup.tsx b/src/components/Popup/Popup.tsx index 1c76f88641c01b..3ad48e8c73ed31 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 3b2199c1e0b0e0..c0384ed99fcd7e 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 1dd84373e5ca37..ad10fde4e5dd51 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 7cde3cad0de6c2..dddd11de9786a7 100644 --- a/src/utilities/focus.ts +++ b/src/utilities/focus.ts @@ -20,6 +20,20 @@ export function getLastFocusable( return getPreviousElement(rootElement, currentElement, true, false, true, includeElementsInFocusZones); } +// Attempts to focus the first focusable element. If it successfully focuses it will return true. Otherwise it will return false. +export function focusFirstFocusable( + rootElement: HTMLElement, + currentElement: HTMLElement, + includeElementsInFocusZones?: boolean): boolean { + let element: HTMLElement = getNextElement(rootElement, currentElement, true, false, false, includeElementsInFocusZones); + + if (element) { + element.focus(); + return true; + } + return false; +} + /** Traverse to find the previous element. */ export function getPreviousElement( rootElement: HTMLElement, From e27580a611e6e3fe7270f9575929310d44a27eee Mon Sep 17 00:00:00 2001 From: Jon Schectman Date: Fri, 19 Aug 2016 14:28:27 -0700 Subject: [PATCH 2/2] changed focus method to be more clear. Added more jsdoc comments --- src/components/Callout/Callout.Props.ts | 3 ++- src/components/Callout/Callout.tsx | 4 ++-- src/utilities/focus.ts | 14 ++++++++------ 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/components/Callout/Callout.Props.ts b/src/components/Callout/Callout.Props.ts index 9fef8081cb59f5..3bd5f089c5a0e0 100644 --- a/src/components/Callout/Callout.Props.ts +++ b/src/components/Callout/Callout.Props.ts @@ -62,9 +62,10 @@ export interface ICalloutProps extends React.Props, IPositionProps { /** * If true then the callout will attempt to focus the first focusable element that it contains. - * If it does not find an element then focus will not be given to the callout. + * 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; } diff --git a/src/components/Callout/Callout.tsx b/src/components/Callout/Callout.tsx index f45591a49d1e16..aaa6a7793df6aa 100644 --- a/src/components/Callout/Callout.tsx +++ b/src/components/Callout/Callout.tsx @@ -5,7 +5,7 @@ import { Layer } from '../../Layer'; import { css } from '../../utilities/css'; import { EventGroup } from '../../utilities/eventGroup/EventGroup'; import { getRelativePositions, IPositionInfo } from '../../utilities/positioning'; -import { focusFirstFocusable } from '../../utilities/focus'; +import { focusFirstChild } from '../../utilities/focus'; import { Popup } from '../Popup/index'; import './Callout.scss'; @@ -116,7 +116,7 @@ export class Callout extends React.Component { this._events.on(window, 'click', this._dismissOnLostFocus, true); if (this.props.setInitialFocus) { - focusFirstFocusable(this._calloutElement, this._calloutElement, true); + focusFirstChild(this._calloutElement); } if (this.props.onLayerMounted) { diff --git a/src/utilities/focus.ts b/src/utilities/focus.ts index dddd11de9786a7..a77f3181031ba9 100644 --- a/src/utilities/focus.ts +++ b/src/utilities/focus.ts @@ -20,12 +20,14 @@ export function getLastFocusable( return getPreviousElement(rootElement, currentElement, true, false, true, includeElementsInFocusZones); } -// Attempts to focus the first focusable element. If it successfully focuses it will return true. Otherwise it will return false. -export function focusFirstFocusable( - rootElement: HTMLElement, - currentElement: HTMLElement, - includeElementsInFocusZones?: boolean): boolean { - let element: HTMLElement = getNextElement(rootElement, currentElement, true, false, false, 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();