Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
8 changes: 8 additions & 0 deletions src/components/Callout/Callout.Props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ export interface ICalloutProps extends React.Props<Callout>, 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it doesn't find a focusable element, no focus will be set and the method will return false.
@returns True if focus was able to be set, false otherwise.

* 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 {
Expand Down
13 changes: 11 additions & 2 deletions src/components/Callout/Callout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down Expand Up @@ -69,9 +71,12 @@ export class Callout extends React.Component<ICalloutProps, ICalloutState> {
ref={ (callout: HTMLDivElement) => this._calloutElement = callout }
>
{ isBeakVisible && targetElement ? (<div className={ beakStyle } style={ ((positions) ? positions.beak : BEAK_ORIGIN_POSITION) } />) : (null) }
<div className='ms-Callout-main'>
<Popup
className='ms-Callout-main'
onDismiss={ (ev:any) => this.dismiss() }
shouldRestoreFocus={ true }>
{ children }
</div>
</Popup>
</div>
</div>
);
Expand Down Expand Up @@ -110,6 +115,10 @@ export class Callout extends React.Component<ICalloutProps, ICalloutState> {
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();
}
Expand Down
5 changes: 3 additions & 2 deletions src/components/Popup/Popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,13 @@ export class Popup extends BaseComponent<IPopupProps, {}> {

return (
<div
{ ...this.props as any }
ref='root'
className={ className }
role={ role }
aria-labelledby={ ariaLabelledBy }
aria-desribedby={ ariaDescribedBy } />
aria-desribedby={ ariaDescribedBy }>
{ this.props.children }
</div>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export class CalloutBasicExample extends React.Component<any, ICalloutBaiscExamp
gapSpace={ 20 }
targetElement={ this._menuButtonElement }
onDismiss={ this._onCalloutDismiss }
setInitialFocus={ true }
>
<div className='ms-Callout-header'>
<p className='ms-Callout-title'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ export class CalloutNestedExample extends React.Component<any, ICalloutBaiscExam
className='ms-CalloutExample-callout'
gapSpace={ 20 }
targetElement={ this._menuButtonElement }
onDismiss= { (ev: any) => { this._onDismiss(ev); } }
onDismiss={ (ev: any) => { this._onDismiss(ev); } }
setInitialFocus={ true }
>
<div className='ms-Callout-header'>
<p className='ms-Callout-title'>
Expand Down
14 changes: 14 additions & 0 deletions src/utilities/focus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Member

@dzearing dzearing Aug 19, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

focusFirstChild(rootElement) would seem like a simpler helper.

Also use JSDoc, describe the params.

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,
Expand Down