Focus

Published: September 03, 2019
Edited: September 03, 2019

Lightning needs to know which component is the active component - and should handle key events. This component and its descendants (including the App itself) are called the focus path.

The focus path is determined by calling the _getFocused() method of the app object. By default, or if undefined is returned, the focus path stops here and the app is the active component (and the focus path only contains the app itself). When _getFocused() returns a child component however, that one is also added to the focus path, and its _getFocused() method is also invoked. This process may repeat recursively until the active component is found. To put it another way: the components may delegate focus to descendants.

The focus path is only recalculated at specific events:

  • when any component's state is changed
  • when a key has been pressed
  • when _refocus() has been called on any component

A common way to delegate focus is to use states, overriding _getFocused() method within each state class:

static _states() {
    return [
        class Buttons extends this {
            _getFocused() {
                return this.tag('Buttons')
            }
        },
        class List extends this {
            _getFocused() {
                return this.tag('List')
            }
        }
    ]
}

If you have a state named based on your components, you can also use a generic method to control it:

_getFocused() {
    return this.tag(this.state)
}

For dynamically-generated components (like a list with items served by an API), it's advised to create an index variable to delegate your "focus". Then you can bind some keys for the user to change the focus to a differed component:

_init() {
    this.index = 0
}

_handleLeft() {
    if(this.index > 0) {
        this.index--;
    }
}

_handleRight() {
    if(this.index < this.children.length - 1) {
        this.index++;
    }
}

reset() {
    this.index = 0;
    this._refocus(); // We need to force focus recalc.
}

_getFocused() {
    return this.children[this.index]
}

Lightning fires _focus() and _unfocus() events on Components when the "focus" changes. These methods can be used to change the appearance or state of the component.

_focus() {
    //add code to do something when your Component receives focus
}

_unfocus() {
    //add code to do something when your Component loses focus
}

Live Demo:

class FocusDemo extends lng.Application {
    static _template() {
        return {
            x: 20, y: 20,
            Buttons: {
                LeftButton: { type: ExampleButton, buttonText: 'Left' },
                RightButton: { x: 200, type: ExampleButton, buttonText: 'Right' }
            },
            List: { y: 100, type: ExampleList }
        }
    }
    _init() {
        this.buttonIndex = 0
        this.tag('List').items = [1,2,3,4].map((i) => ({label: i }))
        this._setState('Buttons')
    }
    _handleUp() {
        this._setState('Buttons')
    }
    _handleDown() {
        this._setState('List')
    }
    static _states() {
        return [
            class Buttons extends this {
                _handleLeft() {
                    this.buttonIndex = 0
                }
                _handleRight() {
                    this.buttonIndex = 1
                }
                _getFocused() {
                    return this.tag('Buttons').children[this.buttonIndex]
                }
            },
            class List extends this {
                _getFocused() {
                    return this.tag('List')
                }
            }
        ]
    }
}

class ExampleButton extends lng.Component {
    static _template() {
        return {
            color: 0xff1f1f1f,
            texture: lng.Tools.getRoundRect(150, 40, 4),
            Label: {
                x: 75, y: 22, mount: .5, color: 0xffffffff, text: { fontSize: 20 }
            }
        }
    }
    _init() {
        this.tag('Label').patch({ text: { text: this.buttonText }})
    }
    _focus() {
        this.color = 0xffffffff
        this.tag('Label').color = 0xff1f1f1f
    }
    _unfocus() {
        this.color = 0xff1f1f1f
        this.tag('Label').color = 0xffffffff
    }
}

class ExampleList extends lng.Component {
    static _template() {
        return {}
    }
    _init() {
        this.index = 0
    }
    set items(items) {
        this.children = items.map((item, index) => {
            return {
                ref: 'ListItem-' + index, //optional, for debug purposes
                type: ExampleListItem,
                x: index * 70, //item width + 20px margin
                item //passing the item as an attribute
            }
        })
    }
    _getFocused() {
        return this.children[this.index]
    }
    _handleLeft() {
        if(this.index > 0) {
            this.index--
        }
    }
    _handleRight() {
        // we don't know exactly how many items the list can have
        // so we test it based on this component's child list
        if(this.index < this.children.length - 1) {
            this.index++
        }
    }
}

class ExampleListItem extends lng.Component {
    static _template() {
        return {
            rect: true, w: 50, h: 50, color: 0xffff00ff, alpha: 0.8,
            Label: {
                x: 25, y: 30, mount: .5
            }
        }
    }
    _init() {
        this.patch({ Label: { text: { text: this.item.label }}})
    }
    _focus() {
        this.patch({ smooth: { alpha: 1, scale: 1.2 }})
    }
    _unfocus() {
        this.patch({ smooth: { alpha: 0.8, scale: 1 }})
    }
}

const app = new FocusDemo({stage: {w: window.innerWidth, h: window.innerHeight, useImageWorker: false}});
document.body.appendChild(app.stage.getCanvas());