Skip to content
This repository has been archived by the owner on Jul 30, 2018. It is now read-only.

Update focus mixin #958

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
41 changes: 13 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -649,9 +649,9 @@ v('input', { type: 'text', focus: () => true) })

This primitive is a base that enables further abstractions to be built to handle more complex behaviors. One of these is handling focus across the boundaries of encapsulated widgets. The `FocusMixin` should be used by widgets to provide `focus` to their children or to accept `focus` from a parent widget.

The `FocusMixin` adds `focus` and `shouldFocus` to a widget's API. `shouldFocus` checks if the widget is in a state to perform a focus action and will only return `true` once until the widget's `focus` method has been called again. This `shouldFocus` method is designed to be passed to child widgets or nodes are the value of their `focus` property.
The `FocusMixin` adds `focus` to a widget's API. When called with a key, `focus` decorates the corresponding child node with `focus: () => true` for one render.

When `shouldFocus` is passed to a widget it will be called as the properties are set on the child widget, meaning that any other usages of the parent's `shouldFocus` method will result in a return value of `false`.
The `FocusMixin` can also be used to handle receiving a `focus` property from a parent widget. If `focusKey` is defined on the widget class, the `FocusMixin` will pass on `widget.properties.focus` to the corresponding child node.

An example usage controlling focus across child VNodes (DOM) and WNodes (widgets):

Expand All @@ -661,9 +661,10 @@ An example usage controlling focus across child VNodes (DOM) and WNodes (widgets
}

class FocusInputChild extends Focus(WidgetBase)<FocusInputChildProperties> {
// focusKey must be defined, or nothing will happen if the widget is passed a truthy focus property
focusKey = 'input';
protected render() {
// the child widget's `this.shouldFocus` is passed directly to the input nodes focus property
return v('input', { onfocus: this.properties.onFocus, focus: this.shouldFocus });
return v('input', { key: 'input', onfocus: this.properties.onFocus });
}
}

Expand All @@ -676,59 +677,43 @@ An example usage controlling focus across child VNodes (DOM) and WNodes (widgets
}

private _previous() {
if (this._focusedInputKey === 0) {
this._focusedInputKey--;
if (this._focusedInputKey < 0) {
this._focusedInputKey = 4;
} else {
this._focusedInputKey--;
}
// calling focus resets the widget so that `this.shouldFocus`
// will return true on its next use
this.focus();

// call this.focus with the updated focus key
this.focus(this._focusedInputKey);
}

private _next() {
if (this._focusedInputKey === 4) {
this._focusedInputKey = 0;
} else {
this._focusedInputKey++;
}
// calling focus resets the widget so that `this.shouldFocus`
// will return true on its next use
this.focus();
this._focusedInputKey = (this._focusedInputKey + 1) % 5;
// call this.focus with the updated focus key
this.focus(this._focusedInputKey);
}

protected render() {
return v('div', [
v('button', { onclick: this._previous }, ['Previous']),
v('button', { onclick: this._next }, ['Next']),
// `this.shouldFocus` is passed to the child that requires focus based on
// some widget logic. If the child is a widget it can then deal with that
// however is necessary. The widget may also have internal logic and pass
// its own `this.shouldFocus` down further or could apply directly to a
// VNode child.
w(FocusInputChild, {
key: 0,
focus: this._focusedInputKey === 0 ? this.shouldFocus : undefined,
onFocus: () => this._onFocus(0)
}),
w(FocusInputChild, {
key: 1,
focus: this._focusedInputKey === 1 ? this.shouldFocus : undefined,
onFocus: () => this._onFocus(1)
}),
w(FocusInputChild, {
key: 2,
focus: this._focusedInputKey === 2 ? this.shouldFocus : undefined,
onFocus: () => this._onFocus(2)
}),
w(FocusInputChild, {
key: 3,
focus: this._focusedInputKey === 3 ? this.shouldFocus : undefined,
onFocus: () => this._onFocus(3)
}),
v('input', {
key: 4,
focus: this._focusedInputKey === 4 ? this.shouldFocus : undefined,
onfocus: () => this._onFocus(4)
})
]);
Expand Down
79 changes: 70 additions & 9 deletions src/mixins/Focus.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,106 @@
import { Constructor } from './../interfaces';
import { WidgetBase } from './../WidgetBase';
import { DNode, VNode, WNode, NodeOperationPredicate } from '../interfaces';
import { decorate, isWNode, isVNode } from '../d';
import { diffProperty } from './../decorators/diffProperty';
import { afterRender } from './../decorators/afterRender';

export interface FocusProperties {
focus?: (() => boolean);
/**
* The focus property allows a widget to be focused by a parent.
* It must be used in conjunction with focusKey, or have a custom implementation within the widget
*/
focus?: boolean | NodeOperationPredicate;
}

export interface FocusMixin {
focus: () => void;
shouldFocus: () => boolean;
/**
* The focus method marks a specific node for decoration with focus: () => true;
*/
focus: (key: string) => void;
/**
* The focusKey property is used with the focus widget property to allow a widget to be focused by a parent.
* If present, a truthy FocusProperties.focus will decorate this node with focus: () => true
*/
focusKey?: string | number;
properties: FocusProperties;
}

function diffFocus(previousProperty: Function, newProperty: Function) {
const result = newProperty && newProperty();
let changed = newProperty !== previousProperty;
if (typeof newProperty === 'function') {
changed = newProperty();
}

return {
changed: result,
changed,
value: newProperty
};
}

export function FocusMixin<T extends Constructor<WidgetBase<FocusProperties>>>(Base: T): T & Constructor<FocusMixin> {
abstract class Focus extends Base {
public abstract properties: FocusProperties;
public focusKey: string | number;

private _currentToken = 0;

private _previousToken = 0;

private _currentFocusKey: string | number;

private _shouldFocusChild() {
return this._currentFocusKey && this._shouldFocus();
}

private _shouldFocusSelf() {
let { focus } = this.properties;
if (typeof focus === 'function') {
focus = focus();
}
return focus && this.focusKey !== undefined;
}

@diffProperty('focus', diffFocus)
protected isFocusedReaction() {
this._currentToken++;
@afterRender()
protected updateFocusProperties(result: DNode | DNode[]): DNode | DNode[] {
if (!this._shouldFocusChild() && !this._shouldFocusSelf()) {
return result;
}

decorate(result, {
modifier: (node, breaker) => {
if (this._shouldFocusSelf() && node.properties.key === this.focusKey) {
node.properties = { ...node.properties, focus: this.properties.focus };
} else if (node.properties.key === this._currentFocusKey) {
node.properties = { ...node.properties, focus: () => true };
}
breaker();
},
predicate: (node: DNode): node is VNode | WNode => {
if (!isVNode(node) && !isWNode(node)) {
return false;
}
const { key } = node.properties;
return key === this._currentFocusKey || (!!this.focusKey && key === this.focusKey);
}
});
return result;
}

public shouldFocus = () => {
private _shouldFocus = () => {
const result = this._currentToken !== this._previousToken;
this._previousToken = this._currentToken;
return result;
};

public focus() {
/**
* Will mark a node for decoration with the focus property and trigger a render
*
* @param key The key to call focus on
*/
public focus(key: string | number) {
this._currentFocusKey = key;
this._currentToken++;
this.invalidate();
}
Expand Down
Loading