Skip to content
This repository was archived by the owner on Mar 4, 2020. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 9 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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## [Unreleased]

### Features
- Upgrade `FocusZone` to the latest version from `fabric-ui` @sophieH29 ([#1772](https://github.com/stardust-ui/react/pull/1772))

### Documentation
- Restore docs for `Ref` component @layershifter ([#1777](https://github.com/stardust-ui/react/pull/1777))

Expand Down
6 changes: 6 additions & 0 deletions packages/react/src/lib/accessibility/FocusZone/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ This is a list of changes made to this Stardust copy of FocusZone in comparison
- Handle keyDownCapture based on `shouldHandleKeyDownCapture` prop @sophieH29 ([#563](https://github.com/stardust-ui/react/pull/563))
- Add `bidirectionalDomOrder` direction allowing arrow keys navigation following DOM order @sophieH29 ([#1637](https://github.com/stardust-ui/react/pull/1647))

### Upgrade `FocusZone` to the latest version from `fabric-ui` @sophieH29 ([#1772](https://github.com/stardust-ui/react/pull/1772))
Comment thread
sophieH29 marked this conversation as resolved.
- Restore focus on removing item ([OfficeDev/office-ui-fabric-react#7818](https://github.com/OfficeDev/office-ui-fabric-react/pull/7818))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't see any UTs added for 7818.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

- Reduce global event listeners ([OfficeDev/office-ui-fabric-react#7958](https://github.com/OfficeDev/office-ui-fabric-react/pull/7958))
- Track innerzones correctly ([OfficeDev/office-ui-fabric-react#8560](https://github.com/OfficeDev/office-ui-fabric-react/pull/8560))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No UTs from 8560.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I didn't like an idea to add a public static method to FZ API to test it as here https://github.com/OfficeDev/office-ui-fabric-react/pull/8560/files#diff-2949b523d6a4d1f7e2e111abb6557158R82
If you're ok with that, I'll add it

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would rather add it

- Check for no wrap fix ([OfficeDev/office-ui-fabric-react#9542](https://github.com/OfficeDev/office-ui-fabric-react/pull/9542))


#### feat(FocusZone): Implement FocusZone into renderComponent [#116](https://github.com/stardust-ui/react/pull/116)
- Prettier and linting fixes, e.g., removing semicolons, removing underscores from private methods.
Expand Down
190 changes: 158 additions & 32 deletions packages/react/src/lib/accessibility/FocusZone/FocusZone.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ import {
isElementFocusSubZone,
isElementTabbable,
getWindow,
getDocument,
getElementIndexPath,
getFocusableByIndexPath,
getParent,
IS_FOCUSABLE_ATTRIBUTE,
FOCUSZONE_ID_ATTRIBUTE,
} from './focusUtilities'
Expand All @@ -31,16 +35,14 @@ const _allInstances: {
[key: string]: FocusZone
} = {}

const _outerZones: Set<FocusZone> = new Set()

interface Point {
left: number
top: number
}
const ALLOWED_INPUT_TYPES = ['text', 'number', 'password', 'email', 'tel', 'url', 'search']

function getParent(child: HTMLElement): HTMLElement | null {
return child && child.parentElement
}

export default class FocusZone extends React.Component<FocusZoneProps> implements IFocusZone {
static propTypes = {
className: PropTypes.string,
Expand All @@ -55,13 +57,13 @@ export default class FocusZone extends React.Component<FocusZoneProps> implement
shouldEnterInnerZone: PropTypes.func,
onActiveElementChanged: PropTypes.func,
shouldReceiveFocus: PropTypes.func,
allowFocusRoot: PropTypes.bool,
handleTabKey: PropTypes.number,
shouldInputLoseFocusOnArrowKey: PropTypes.func,
stopFocusPropagation: PropTypes.bool,
onFocus: PropTypes.func,
preventDefaultWhenHandled: PropTypes.bool,
isRtl: PropTypes.bool,
restoreFocusFromRoot: PropTypes.bool,
}

static defaultProps: FocusZoneProps = {
Expand All @@ -78,6 +80,19 @@ export default class FocusZone extends React.Component<FocusZoneProps> implement
_id: string
/** The most recently focused child element. */
_activeElement: HTMLElement | null

/**
* The index path to the last focused child element.
*/
_lastIndexPath: number[] | undefined

/**
* Flag to define when we've intentionally parked focus on the root element to temporarily
* hold focus until items appear within the zone.
*/
_isParked: boolean
_parkedTabIndex: string | null | undefined

/** The child element with tabindex=0. */
_defaultFocusElement: HTMLElement | null
_focusAlignment: Point
Expand All @@ -99,43 +114,89 @@ export default class FocusZone extends React.Component<FocusZoneProps> implement
}

this._processingTabKey = false
this.onKeyDownCapture = this.onKeyDownCapture.bind(this)
}

componentDidMount(): void {
_allInstances[this._id] = this

this.setRef(this) // called here to support functional components, we only need HTMLElement ref anyway
if (this._root.current) {
this.windowElement = getWindow(this._root.current)

let parentElement = getParent(this._root.current)
if (!this._root.current) {
return
}

while (parentElement && parentElement !== document.body && parentElement.nodeType === 1) {
if (isElementFocusZone(parentElement)) {
this._isInnerZone = true
break
}
parentElement = getParent(parentElement)
}
this.windowElement = getWindow(this._root.current)
let parentElement = getParent(this._root.current)

if (!this._isInnerZone) {
this.windowElement.addEventListener('keydown', this.onKeyDownCapture, true)
while (parentElement && parentElement !== document.body && parentElement.nodeType === 1) {
if (isElementFocusZone(parentElement)) {
this._isInnerZone = true
break
}
parentElement = getParent(parentElement)
}

if (!this._isInnerZone) {
_outerZones.add(this)
}

if (this.windowElement && _outerZones.size === 1) {
this.windowElement.addEventListener('keydown', this._onKeyDownCapture, true)
}

this._root.current.addEventListener('blur', this._onBlur, true)

// Assign initial tab indexes so that we can set initial focus as appropriate.
this.updateTabIndexes()
// Assign initial tab indexes so that we can set initial focus as appropriate.
this.updateTabIndexes()

if (this.props.shouldFocusOnMount) {
this.focus()
if (this.props.shouldFocusOnMount) {
this.focus()
}
}

componentDidUpdate(): void {
if (!this._root.current) {
return
}
const doc = getDocument(this._root.current)

if (
doc &&
this._lastIndexPath &&
(doc.activeElement === doc.body ||
(this.props.restoreFocusFromRoot && doc.activeElement === this._root.current))
) {
// The element has been removed after the render, attempt to restore focus.
const elementToFocus = getFocusableByIndexPath(
this._root.current as HTMLElement,
this._lastIndexPath,
)

if (elementToFocus) {
this.setActiveElement(elementToFocus, true)
elementToFocus.focus()
this.setParkedFocus(false)
} else {
// We had a focus path to restore, but now that path is unresolvable. Park focus
// on the container until we can try again.
this.setParkedFocus(true)
}
}
}

componentWillUnmount() {
delete _allInstances[this._id]

if (!this._isInnerZone) {
_outerZones.delete(this)
}

if (this.windowElement) {
this.windowElement.removeEventListener('keydown', this.onKeyDownCapture, true)
this.windowElement.removeEventListener('keydown', this._onKeyDownCapture, true)
}

if (this._root.current) {
this._root.current.removeEventListener('blur', this._onBlur, true)
}
}

Expand All @@ -148,6 +209,13 @@ export default class FocusZone extends React.Component<FocusZoneProps> implement
this.props,
)

// Note, right before rendering/reconciling proceeds, we need to record if focus
// was in the zone before the update. This helper will track this and, if focus
// was actually in the zone, what the index path to the element is at this time.
// Then, later in componentDidUpdate, we can evaluate if we need to restore it in
// the case the element was removed.
this.evaluateFocusBeforeRender()

return (
<ElementType
{...unhandledProps}
Expand Down Expand Up @@ -253,6 +321,56 @@ export default class FocusZone extends React.Component<FocusZoneProps> implement
this._root.current = ReactDOM.findDOMNode(elem) as HTMLElement
}

// Record if focus was in the zone, what the index path to the element is at this time.
evaluateFocusBeforeRender(): void {
if (!this._root.current) {
return
}
const doc = getDocument(this._root.current)

if (!doc) {
return
}

const focusedElement = doc.activeElement as HTMLElement

// Only update the index path if we are not parked on the root.
if (focusedElement !== this._root.current) {
const shouldRestoreFocus = this._root.current.contains(focusedElement)

this._lastIndexPath = shouldRestoreFocus
? getElementIndexPath(this._root.current as HTMLElement, doc.activeElement as HTMLElement)
: undefined
}
}

/**
* When focus is in the zone at render time but then all focusable elements are removed,
* we "park" focus temporarily on the root. Once we update with focusable children, we restore
* focus to the closest path from previous. If the user tabs away from the parked container,
* we restore focusability to the pre-parked state.
*/
setParkedFocus(isParked: boolean): void {
if (this._root.current && this._isParked !== isParked) {
this._isParked = isParked

if (isParked) {
this._parkedTabIndex = this._root.current.getAttribute('tabindex')
this._root.current.setAttribute('tabindex', '-1')
this._root.current.focus()
} else if (this._parkedTabIndex) {
this._root.current.setAttribute('tabindex', this._parkedTabIndex)
this._parkedTabIndex = undefined
} else {
this._root.current.removeAttribute('tabindex')
}
}
}

_onBlur = () => {
this.setParkedFocus(false)
}

_onFocus = (ev: React.FocusEvent<HTMLElement>): void => {
const {
onActiveElementChanged,
Expand All @@ -262,8 +380,9 @@ export default class FocusZone extends React.Component<FocusZoneProps> implement
} = this.props

let newActiveElement: HTMLElement | undefined
const isImmediateDescendant = this.isImmediateDescendantOfZone(ev.target as HTMLElement)

if (this.isImmediateDescendantOfZone(ev.target as HTMLElement)) {
if (isImmediateDescendant) {
newActiveElement = ev.target as HTMLElement
} else {
let parentElement = ev.target as HTMLElement
Expand Down Expand Up @@ -298,8 +417,11 @@ export default class FocusZone extends React.Component<FocusZoneProps> implement

if (newActiveElement && newActiveElement !== this._activeElement) {
this._activeElement = newActiveElement
this.setFocusAlignment(newActiveElement, true)
this.updateTabIndexes()

if (isImmediateDescendant) {
this.setFocusAlignment(this._activeElement)
this.updateTabIndexes()
}
}

if (onActiveElementChanged) {
Expand All @@ -316,9 +438,9 @@ export default class FocusZone extends React.Component<FocusZoneProps> implement
/**
* Handle global tab presses so that we can patch tabindexes on the fly.
*/
onKeyDownCapture(ev: KeyboardEvent) {
_onKeyDownCapture = (ev: KeyboardEvent) => {
if (keyboardKey.getCode(ev) === keyboardKey.Tab) {
this.updateTabIndexes()
_outerZones.forEach(zone => zone.updateTabIndexes())
}
}

Expand Down Expand Up @@ -781,9 +903,11 @@ export default class FocusZone extends React.Component<FocusZoneProps> implement
// Going left at a leftmost rectangle will go down a line instead of up a line like in LTR.
// This is important, because we want to be comparing the top of the target rect
// with the bottom of the active rect.
topBottomComparison = targetRect.top.toFixed(3) < activeRect.bottom.toFixed(3)
topBottomComparison =
parseFloat(targetRect.top.toFixed(3)) < parseFloat(activeRect.bottom.toFixed(3))
} else {
topBottomComparison = targetRect.bottom.toFixed(3) > activeRect.top.toFixed(3)
topBottomComparison =
parseFloat(targetRect.bottom.toFixed(3)) > parseFloat(activeRect.top.toFixed(3))
}

if (
Expand Down Expand Up @@ -820,9 +944,11 @@ export default class FocusZone extends React.Component<FocusZoneProps> implement
// Going right at a rightmost rectangle will go up a line instead of down a line like in LTR.
// This is important, because we want to be comparing the bottom of the target rect
// with the top of the active rect.
topBottomComparison = targetRect.bottom.toFixed(3) > activeRect.top.toFixed(3)
topBottomComparison =
parseFloat(targetRect.bottom.toFixed(3)) > parseFloat(activeRect.top.toFixed(3))
} else {
topBottomComparison = targetRect.top.toFixed(3) < activeRect.bottom.toFixed(3)
topBottomComparison =
parseFloat(targetRect.top.toFixed(3)) < parseFloat(activeRect.bottom.toFixed(3))
}

if (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,6 @@ export interface FocusZoneProps extends React.HTMLAttributes<HTMLElement | Focus
*/
shouldReceiveFocus?: (childElement?: HTMLElement) => boolean

/**
* Allow focus to move to root container
*/
allowFocusRoot?: boolean

/**
* Allows TAB key to be handled, thus alows tabbing through a focusable list of items in the
* focus zone. A side effect is that users will not be able to TAB out of the focus zone and
Expand Down Expand Up @@ -148,6 +143,11 @@ export interface FocusZoneProps extends React.HTMLAttributes<HTMLElement | Focus
* If true, FocusZone prevents default behavior.
*/
preventDefaultWhenHandled?: boolean

/**
* If focus is on root element after componentDidUpdate, will attempt to restore the focus to inner element
*/
restoreFocusFromRoot?: boolean
}

export enum FocusZoneTabbableElements {
Expand Down
Loading