Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trigger callbacks and events on focus/blur within #99

Open
wants to merge 1 commit 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
34 changes: 34 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,18 @@ If given, the `onFocus` function will be called when the node gets focussed on.

If given, the `onBlur` function will be called when the node if the node had focus and a new node gains focus.

### `onFocusWithin`

`function`

If given, the `onFocusWithin` function will be called when any of the nested children of the node gains focus, and none had focus before. I.e. focus has moved into the parent.

### `onBlurWithin`

`function`

If given, the `onBlurWithin` function will be called when the node if one of the nested children of the node had focus, and now none of them do. I.e. focus has move out of the parent.

### `onSelect`

`function`
Expand Down Expand Up @@ -310,6 +322,28 @@ navigation.on('move', moveEvent => {

```

The following two events occur when focus moves into/out of a parent node:
* `navigation.on('focusWithin', function)` - Focus was given to a nested child node.
* `navigation.on('blurWithin', function)` - Focus was taken from all nested child nodes.

The `focusWithin` event callback is called with an event like so:

```js
{
node: <node> // the node that focus has moved into
focusNode: <node> // the child node that has gained focus
}
```

The `blurWithin` event callback is called with an event like so:

```js
{
node: <node> // the node that focus has moved outside of
blurNode: <node> // the child node that has lost focus
}
```

To unregister an event callback, simply call the .off() method

```js
Expand Down
49 changes: 49 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -966,11 +966,50 @@ export class Lrud {
throw new Error('trying to assign focus to a non focusable node')
}

const currentParents = []
const newParents = []

if (this.currentFocusNode) {
this.emitter.emit('blur', this.currentFocusNode)
if (this.currentFocusNode.onBlur) {
this.currentFocusNode.onBlur(this.currentFocusNode)
}

let parent = this.currentFocusNode.parent

// get list of all parents for current focus node
while (parent) {
currentParents.push(parent)

// if the parent has a parent, bubble up
parent = parent.parent
}
}

let parent = node.parent

// get list of all parents for new focus node
while (parent) {
newParents.push(parent)

// if the parent has a parent, bubble up
parent = parent.parent
}

// list of parents that will lose focus and not gain it again
const blurParents = currentParents.filter((node) => newParents.indexOf(node) === -1)

// list of parents that did not have focus and will gain it
const focusParents = newParents.filter((node) => currentParents.indexOf(node) === -1)

for (const blurParent of blurParents) {
this.emitter.emit('blurWithin', {
node: blurParent,
blurNode: this.currentFocusNode
})
if (blurParent.onBlurWithin) {
blurParent.onBlurWithin(blurParent, this.currentFocusNode)
}
}

this.currentFocusNode = node
Expand All @@ -984,6 +1023,16 @@ export class Lrud {
}

this.emitter.emit('focus', node)

for (const focusParent of focusParents) {
this.emitter.emit('focusWithin', {
node: focusParent,
focusNode: node
})
if (focusParent.onFocusWithin) {
focusParent.onFocusWithin(focusParent, node)
}
}
}

/**
Expand Down
2 changes: 2 additions & 0 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ export interface Node extends Tree<Node> {
onActiveChildChange?: (event: { node: Node, leave: Node, enter: Node }) => void
onBlur?: (node: Node) => void
onFocus?: (node: Node) => void
onFocusWithin?: (node: Node, focusNode: Node) => void
onBlurWithin?: (node: Node, blurNode: Node) => void
onMove?: (event: { node: Node, leave: Node, enter: Node, direction: Direction, offset: -1 | 1 }) => void
}

Expand Down
79 changes: 79 additions & 0 deletions src/lrud.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,85 @@ describe('lrud', () => {
navigation.assignFocus('root')
}).toThrow('trying to assign focus to a non focusable node')
})

test('should trigger onFocusWithin for parents', () => {
const onFocusSpy = jest.fn()
const focusEventSpy = jest.fn()

const navigation = new Lrud()
.registerNode('root', { onFocusWithin: onFocusSpy })
.registerNode('a', { parent: 'root', onFocusWithin: onFocusSpy })
.registerNode('aa', { parent: 'a', isFocusable: true })

navigation.on('focusWithin', focusEventSpy)

navigation.assignFocus('aa')

// 1st bubbling to 'a'
expect(onFocusSpy.mock.calls[0][0].id).toEqual('a')
expect(onFocusSpy.mock.calls[0][1].id).toEqual('aa')
// ...then to 'root'
expect(onFocusSpy.mock.calls[1][0].id).toEqual('root')
expect(onFocusSpy.mock.calls[1][1].id).toEqual('aa')

// 1st bubbling to 'a'
expect(focusEventSpy.mock.calls[0][0].node.id).toEqual('a')
expect(focusEventSpy.mock.calls[0][0].focusNode.id).toEqual('aa')
// ...then to 'root'
expect(focusEventSpy.mock.calls[1][0].node.id).toEqual('root')
expect(focusEventSpy.mock.calls[1][0].focusNode.id).toEqual('aa')
})

test('should trigger onBlurWithin for parents of previous focus node', () => {
const onBlurSpy = jest.fn()
const blurEventSpy = jest.fn()

const navigation = new Lrud()
.registerNode('root')
.registerNode('a', { parent: 'root', onBlurWithin: onBlurSpy })
.registerNode('aa', { parent: 'a', isFocusable: true })
.registerNode('b', { parent: 'root' })
.registerNode('bb', { parent: 'b', isFocusable: true })

navigation.on('blurWithin', blurEventSpy)

navigation.assignFocus('aa')
navigation.assignFocus('bb')

expect(onBlurSpy.mock.calls[0][0].id).toEqual('a')
expect(onBlurSpy.mock.calls[0][1].id).toEqual('aa')

expect(blurEventSpy.mock.calls[0][0].node.id).toEqual('a')
expect(blurEventSpy.mock.calls[0][0].blurNode.id).toEqual('aa')
})

test('should not trigger onFocusWithin or onBlurWithin if focus remains in parent', () => {
const focusSpy = jest.fn()
const blurSpy = jest.fn()
const focusEventSpy = jest.fn()
const blurEventSpy = jest.fn()

const navigation = new Lrud()
.registerNode('root', { onFocusWithin: focusSpy, onBlurWithin: blurSpy })
.registerNode('a', { parent: 'root' })
.registerNode('aa', { parent: 'a', isFocusable: true })
.registerNode('b', { parent: 'root' })
.registerNode('bb', { parent: 'b', isFocusable: true })

navigation.on('focusWithin', (event) => event.node.id === 'root' && focusEventSpy(event))
navigation.on('blurWithin', (event) => event.node.id === 'root' && blurEventSpy(event))

navigation.assignFocus('aa')
navigation.assignFocus('bb')

// should only trigger for the first assignment
expect(focusSpy.mock.calls.length).toEqual(1)
expect(focusEventSpy.mock.calls.length).toEqual(1)

// should never trigger
expect(blurSpy.mock.calls.length).toEqual(0)
expect(blurEventSpy.mock.calls.length).toEqual(0)
})
})

describe('climbUp()', () => {
Expand Down
2 changes: 2 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,8 @@ export const prepareNode = (nodeId: NodeId, nodeConfig: NodeConfig = {}): Node =
onActiveChildChange: nodeConfig.onActiveChildChange,
onBlur: nodeConfig.onBlur,
onFocus: nodeConfig.onFocus,
onBlurWithin: nodeConfig.onBlurWithin,
onFocusWithin: nodeConfig.onFocusWithin,
onMove: nodeConfig.onMove
}
}
Expand Down