Skip to content

Commit

Permalink
Focus on tab (#15232)
Browse files Browse the repository at this point in the history
  • Loading branch information
RaananW authored Jun 27, 2024
1 parent f78c1e7 commit 93d3b09
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 125 deletions.
53 changes: 48 additions & 5 deletions packages/dev/gui/src/2D/advancedDynamicTexture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import type { Scene } from "core/scene";

import { Container } from "./controls/container";
import { Control } from "./controls/control";
import type { IFocusableControl } from "./controls/focusableControl";
import { Style } from "./style";
import { Measure } from "./measure";
import { Constants } from "core/Engines/constants";
Expand Down Expand Up @@ -85,7 +84,7 @@ export class AdvancedDynamicTexture extends DynamicTexture {
private _idealHeight = 0;
private _useSmallestIdeal: boolean = false;
private _renderAtIdealSize = false;
private _focusedControl: Nullable<IFocusableControl>;
private _focusedControl: Nullable<Control>;
private _blockNextFocusCheck = false;
private _renderScale = 1;
private _rootElement: Nullable<HTMLElement>;
Expand Down Expand Up @@ -147,6 +146,12 @@ export class AdvancedDynamicTexture extends DynamicTexture {
* Gets or sets a boolean indicating that the canvas must be reverted on Y when updating the texture
*/
public applyYInversionOnUpdate = true;

/**
* A boolean indicating whether or not the elements can be navigated to using the tab key.
* Defaults to false.
*/
public disableTabNavigation = false;
/**
* Gets or sets a number used to scale rendering size (2 means that the texture will be twice bigger).
* Useful when you want more antialiasing
Expand Down Expand Up @@ -322,10 +327,10 @@ export class AdvancedDynamicTexture extends DynamicTexture {
/**
* Gets or sets the current focused control
*/
public get focusedControl(): Nullable<IFocusableControl> {
public get focusedControl(): Nullable<Control> {
return this._focusedControl;
}
public set focusedControl(control: Nullable<IFocusableControl>) {
public set focusedControl(control: Nullable<Control>) {
if (this._focusedControl == control) {
return;
}
Expand Down Expand Up @@ -411,6 +416,12 @@ export class AdvancedDynamicTexture extends DynamicTexture {
}
});
this._preKeyboardObserver = scene.onPreKeyboardObservable.add((info) => {
// check if tab is pressed
if (!this.disableTabNavigation && info.type === KeyboardEventTypes.KEYDOWN && info.event.code === "Tab") {
this._focusNextElement(!info.event.shiftKey);
info.event.preventDefault();
return;
}
if (!this._focusedControl) {
return;
}
Expand Down Expand Up @@ -984,6 +995,38 @@ export class AdvancedDynamicTexture extends DynamicTexture {
this._attachToOnBlur(scene);
}

private _focusNextElement(forward: boolean = true): void {
// generate the order of tab-able controls
const sortedTabbableControls: Control[] = [];
this.executeOnAllControls((control) => {
if (control.isFocusInvisible || !control.isVisible || control.tabIndex < 0) {
return;
}
sortedTabbableControls.push(control);
});
// if no control is tab-able, return
if (sortedTabbableControls.length === 0) {
return;
}
sortedTabbableControls.sort((a, b) => {
// if tabIndex is 0, put it in the end of the list, otherwise sort by tabIndex
return a.tabIndex === 0 ? 1 : b.tabIndex === 0 ? -1 : a.tabIndex - b.tabIndex;
});
// if no control is focused, focus the first one
if (!this._focusedControl) {
sortedTabbableControls[0].focus();
} else {
const currentIndex = sortedTabbableControls.indexOf(this._focusedControl);
let nextIndex = currentIndex + (forward ? 1 : -1);
if (nextIndex < 0) {
nextIndex = sortedTabbableControls.length - 1;
} else if (nextIndex >= sortedTabbableControls.length) {
nextIndex = 0;
}
sortedTabbableControls[nextIndex].focus();
}
}

/**
* @internal
*/
Expand Down Expand Up @@ -1189,7 +1232,7 @@ export class AdvancedDynamicTexture extends DynamicTexture {
* Move the focus to a specific control
* @param control defines the control which will receive the focus
*/
public moveFocusToControl(control: IFocusableControl): void {
public moveFocusToControl(control: Control): void {
this.focusedControl = control;
this._lastPickedControl = <any>control;
this._blockNextFocusCheck = true;
Expand Down
105 changes: 103 additions & 2 deletions packages/dev/gui/src/2D/controls/control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,18 @@ import { SerializationHelper } from "core/Misc/decorators.serialization";
import type { ICanvasGradient, ICanvasRenderingContext } from "core/Engines/ICanvas";
import { EngineStore } from "core/Engines/engineStore";
import type { IAccessibilityTag } from "core/IAccessibilityTag";
import type { IPointerEvent } from "core/Events/deviceInputEvents";
import type { IKeyboardEvent, IPointerEvent } from "core/Events/deviceInputEvents";
import type { IAnimatable } from "core/Animations/animatable.interface";
import type { Animation } from "core/Animations/animation";
import type { BaseGradient } from "./gradient/BaseGradient";
import type { AbstractEngine } from "core/Engines/abstractEngine";
import type { IFocusableControl } from "./focusableControl";

/**
* Root class used for all 2D controls
* @see https://doc.babylonjs.com/features/featuresDeepDive/gui/gui#controls
*/
export class Control implements IAnimatable {
export class Control implements IAnimatable, IFocusableControl {
/**
* Gets or sets a boolean indicating if alpha must be an inherited value (false by default)
*/
Expand Down Expand Up @@ -365,6 +366,11 @@ export class Control implements IAnimatable {
*/
public onPointerClickObservable = new Observable<Vector2WithInfo>();

/**
* An event triggered when a control receives an ENTER key down event
*/
public onEnterPressedObservable = new Observable<Control>();

/**
* An event triggered when pointer enters the control
*/
Expand Down Expand Up @@ -1304,6 +1310,96 @@ export class Control implements IAnimatable {
*/
animations: Nullable<Animation[]> = null;

// Focus functionality

protected _focusedColor: Nullable<string> = null;
/**
* Border color when control is focused
* When not defined the ADT color will be used. If no ADT color is defined, focused state won't have any border
*/
public get focusedColor(): Nullable<string> {
return this._focusedColor;
}
public set focusedColor(value: Nullable<string>) {
this._focusedColor = value;
}
/**
* The tab index of this control. -1 indicates this control is not part of the tab navigation.
* A positive value indicates the order of the control in the tab navigation.
* A value of 0 indicated the control will be focused after all controls with a positive index.
* More than one control can have the same tab index and the navigation would then go through all controls with the same value in an order defined by the layout or the hierarchy.
* The value can be changed at any time.
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex
*/
public tabIndex: number = -1;
protected _isFocused = false;
protected _unfocusedColor: Nullable<string> = null;

/** Observable raised when the control gets the focus */
public onFocusObservable = new Observable<Control>();
/** Observable raised when the control loses the focus */
public onBlurObservable = new Observable<Control>();
/** Observable raised when a key event was processed */
public onKeyboardEventProcessedObservable = new Observable<IKeyboardEvent>();

/** @internal */
public onBlur(): void {
if (this._isFocused) {
this._isFocused = false;
if (this.focusedColor && this._unfocusedColor != null) {
// Set color back to saved unfocused color
this.color = this._unfocusedColor;
}
this.onBlurObservable.notifyObservers(this);
}
}

/** @internal */
public onFocus(): void {
this._isFocused = true;

if (this.focusedColor) {
// Save the unfocused color
this._unfocusedColor = this.color;
this.color = this.focusedColor;
}
this.onFocusObservable.notifyObservers(this);
}

/**
* Function called to get the list of controls that should not steal the focus from this control
* @returns an array of controls
*/
public keepsFocusWith(): Nullable<Control[]> {
return null;
}

/**
* Function to focus a button programmatically
*/
public focus() {
this._host.moveFocusToControl(this);
}

/**
* Function to unfocus a button programmatically
*/
public blur() {
this._host.focusedControl = null;
}

/**
* Handles the keyboard event
* @param evt Defines the KeyboardEvent
*/
public processKeyboard(evt: IKeyboardEvent): void {
// if enter, trigger the new observable
if (evt.key === "Enter") {
this.onEnterPressedObservable.notifyObservers(this);
}
this.onKeyboardEventProcessedObservable.notifyObservers(evt, -1, this);
}

// Functions

/**
Expand Down Expand Up @@ -2602,6 +2698,11 @@ export class Control implements IAnimatable {
this.onPointerClickObservable.clear();
this.onWheelObservable.clear();

// focus
this.onBlurObservable.clear();
this.onFocusObservable.clear();
this.onKeyboardEventProcessedObservable.clear();

if (this._styleObserver && this._style) {
this._style.onChangedObservable.remove(this._styleObserver);
this._styleObserver = null;
Expand Down
78 changes: 0 additions & 78 deletions packages/dev/gui/src/2D/controls/focusableButton.ts
Original file line number Diff line number Diff line change
@@ -1,91 +1,22 @@
import type { Nullable } from "core/types";
import type { Vector2 } from "core/Maths/math.vector";

import { Button } from "./button";
import type { Control } from "./control";
import { RegisterClass } from "core/Misc/typeStore";
import type { PointerInfoBase } from "core/Events/pointerEvents";
import type { IFocusableControl } from "./focusableControl";
import { Observable } from "core/Misc/observable";
import type { IKeyboardEvent } from "core/Events/deviceInputEvents";

/**
* Class used to create a focusable button that can easily handle keyboard events
* @since 5.0.0
*/
export class FocusableButton extends Button implements IFocusableControl {
/** Highlight color when button is focused */
public focusedColor: Nullable<string> = null;
private _isFocused = false;
private _unfocusedColor: Nullable<string> = null;

/** Observable raised when the control gets the focus */
public onFocusObservable = new Observable<Button>();
/** Observable raised when the control loses the focus */
public onBlurObservable = new Observable<Button>();
/** Observable raised when a key event was processed */
public onKeyboardEventProcessedObservable = new Observable<IKeyboardEvent>();

constructor(public override name?: string) {
super(name);

this._unfocusedColor = this.color;
}

/** @internal */
public onBlur(): void {
if (this._isFocused) {
this._isFocused = false;
if (this.focusedColor && this._unfocusedColor != null) {
// Set color back to saved unfocused color
this.color = this._unfocusedColor;
}
this.onBlurObservable.notifyObservers(this);
}
}

/** @internal */
public onFocus(): void {
this._isFocused = true;

if (this.focusedColor) {
// Save the unfocused color
this._unfocusedColor = this.color;
this.color = this.focusedColor;
}
this.onFocusObservable.notifyObservers(this);
}

/**
* Function called to get the list of controls that should not steal the focus from this control
* @returns an array of controls
*/
public keepsFocusWith(): Nullable<Control[]> {
return null;
}

/**
* Function to focus a button programmatically
*/
public focus() {
this._host.moveFocusToControl(this);
}

/**
* Function to unfocus a button programmatically
*/
public blur() {
this._host.focusedControl = null;
}

/**
* Handles the keyboard event
* @param evt Defines the KeyboardEvent
*/
public processKeyboard(evt: IKeyboardEvent): void {
this.onKeyboardEventProcessedObservable.notifyObservers(evt, -1, this);
}

/**
* @internal
*/
Expand All @@ -97,14 +28,5 @@ export class FocusableButton extends Button implements IFocusableControl {

return super._onPointerDown(target, coordinates, pointerId, buttonIndex, pi);
}

/** @internal */
public override dispose() {
super.dispose();

this.onBlurObservable.clear();
this.onFocusObservable.clear();
this.onKeyboardEventProcessedObservable.clear();
}
}
RegisterClass("BABYLON.GUI.FocusableButton", FocusableButton);
11 changes: 11 additions & 0 deletions packages/dev/gui/src/2D/controls/focusableControl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,15 @@ export interface IFocusableControl {
* Function to unfocus the control programmatically
*/
blur(): void;

/**
* Gets or sets the tabIndex of the control
*/
tabIndex?: number;

/**
* Gets or sets the color used to draw the focus border
* Defaults to "white"
*/
focusBorderColor?: string;
}
Loading

0 comments on commit 93d3b09

Please sign in to comment.