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 all 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

### Features
- Add `overwrite` prop to `Provider` @layershifter ([#1780](https://github.com/stardust-ui/react/pull/1780))
- 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
7 changes: 7 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,13 @@ 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))
- Restore focus on removing item ([OfficeDev/office-ui-fabric-react#7818](https://github.com/OfficeDev/office-ui-fabric-react/pull/7818))
Copy link
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
Contributor Author

Choose a reason for hiding this comment

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

- `onActiveItemChanged` now fires ([OfficeDev/office-ui-fabric-react#7958](https://github.com/OfficeDev/office-ui-fabric-react/pull/7958))
- Reduce global event listeners ([OfficeDev/office-ui-fabric-react#8421](https://github.com/OfficeDev/office-ui-fabric-react/pull/8421))
- Track innerzones correctly ([OfficeDev/office-ui-fabric-react#8560](https://github.com/OfficeDev/office-ui-fabric-react/pull/8560))
Copy link
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
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
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
195 changes: 163 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 @@ -74,10 +76,28 @@ export default class FocusZone extends React.Component<FocusZoneProps> implement
static displayName = 'FocusZone'
static className = 'ms-FocusZone'

/** Used for testing purposes only. */
static getOuterZones(): number {
return _outerZones.size
}

_root: { current: HTMLElement | null } = { current: null }
_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 +119,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)
}

// Assign initial tab indexes so that we can set initial focus as appropriate.
this.updateTabIndexes()
this._root.current.addEventListener('blur', this._onBlur, true)

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

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 +214,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 +326,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 +385,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 +422,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 +443,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 +908,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 +949,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