Skip to content

Commit

Permalink
alt input support for momentary buttons, see #796
Browse files Browse the repository at this point in the history
  • Loading branch information
jessegreenberg committed Sep 25, 2024
1 parent cf35ec8 commit 163e2a4
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 17 deletions.
7 changes: 7 additions & 0 deletions js/buttons/ButtonModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ export default class ButtonModel extends EnabledComponent {
// will be true if and PressListeners' looksOverProperty is true, see PressListener for that definition.
public readonly looksOverProperty: Property<boolean>;

// True when the button is being clicked via the PDOM. You can use this Property if
// custom behavior is needed that is specific to alternative input.
public readonly pdomClickingProperty: Property<boolean>;

// (read-only by users, read-write in subclasses) - emitter that is fired when sound should be produced
public readonly produceSoundEmitter: TEmitter;

Expand Down Expand Up @@ -110,6 +114,7 @@ export default class ButtonModel extends EnabledComponent {
// model Properties
this.overProperty = new BooleanProperty( false );
this.downProperty = new BooleanProperty( false, { reentrant: true } );
this.pdomClickingProperty = new BooleanProperty( false );
this.focusedProperty = new BooleanProperty( false );
this.looksPressedProperty = new BooleanProperty( false );
this.looksOverProperty = new BooleanProperty( false );
Expand Down Expand Up @@ -155,6 +160,7 @@ export default class ButtonModel extends EnabledComponent {
// This will unlink all listeners, causing potential issues if listeners try to unlink Properties afterwards
this.overProperty.dispose();
this.downProperty.dispose();
this.pdomClickingProperty.dispose();
this.produceSoundEmitter.dispose();

this.looksPressedMultilink && this.looksPressedMultilink.dispose();
Expand Down Expand Up @@ -192,6 +198,7 @@ export default class ButtonModel extends EnabledComponent {
} );
pressListener.isOverProperty.lazyLink( this.overProperty.set.bind( this.overProperty ) );
pressListener.isFocusedProperty.lazyLink( this.focusedProperty.set.bind( this.focusedProperty ) );
pressListener.pdomClickingProperty.lazyLink( this.pdomClickingProperty.set.bind( this.pdomClickingProperty ) );

// dispose the previous multilink in case we already created a PressListener with this model
this.looksPressedMultilink && this.looksPressedMultilink.dispose();
Expand Down
7 changes: 0 additions & 7 deletions js/buttons/ButtonNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,13 +280,6 @@ export default class ButtonNode extends Sizable( Voicing( Node ) ) {
public pdomClick(): void {
this._pressListener.click( null );
}

/**
* Is the button currently firing because of accessibility input coming from the PDOM?
*/
public isPDOMClicking(): boolean {
return this._pressListener.pdomClickingProperty.get();
}
}

/**
Expand Down
13 changes: 7 additions & 6 deletions js/buttons/MomentaryButtonInteractionStateProperty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,19 @@
* @author Chris Malley (PixelZoom, Inc.)
*/

import { DerivedProperty2 } from '../../../axon/js/DerivedProperty.js';
import { DerivedProperty3 } from '../../../axon/js/DerivedProperty.js';
import sun from '../sun.js';
import ButtonInteractionState from './ButtonInteractionState.js';
import MomentaryButtonModel from './MomentaryButtonModel.js';

export default class MomentaryButtonInteractionStateProperty<T> extends DerivedProperty2<ButtonInteractionState, boolean, boolean> {
export default class MomentaryButtonInteractionStateProperty<T> extends DerivedProperty3<ButtonInteractionState, boolean, boolean, T> {
public constructor( buttonModel: MomentaryButtonModel<T> ) {
super(
[ buttonModel.looksOverProperty, buttonModel.looksPressedProperty ],
( looksOver, looksPressed ) => {
return looksOver && !looksPressed ? ButtonInteractionState.OVER :
looksPressed ? ButtonInteractionState.PRESSED : // remain pressed regardless of whether 'over' is true
[ buttonModel.looksOverProperty, buttonModel.looksPressedProperty, buttonModel.valueProperty ],
( looksOver, looksPressed, buttonValue ) => {
const pressedOrLooksPressed = ( buttonValue === buttonModel.valueOn ) || looksPressed;
return looksOver && !pressedOrLooksPressed ? ButtonInteractionState.OVER :
pressedOrLooksPressed ? ButtonInteractionState.PRESSED : // remain pressed regardless of whether 'over' is true
ButtonInteractionState.IDLE;
},
{ valueType: ButtonInteractionState }
Expand Down
59 changes: 55 additions & 4 deletions js/buttons/MomentaryButtonModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export default class MomentaryButtonModel<T> extends ButtonModel {

private readonly disposeMomentaryButtonModel: () => void;

// (sun-internal)
public readonly valueProperty: TProperty<T>;
public readonly valueOn: T;

/**
* @param valueOff - value when the button is in the off state
* @param valueOn - value when the button is in the on state
Expand All @@ -40,20 +44,66 @@ export default class MomentaryButtonModel<T> extends ButtonModel {

super( options );

// For 'toggle' like behavior for alternative input, tracks the state for the button because it should remain on
// even when the ButtonModel is not down.
let wasClickedWhileOn = false;

const downListener = ( down: boolean ) => {

// turn on when pressed (if enabled)
if ( down ) {
if ( this.enabledProperty.get() ) {
// If clicking with alternative input, the button should behave like a toggle button. Activating it once will
// set to the on value, and activating it again will set to the off value. This is different from pointer input,
// where the button is only on while the mouse is down. To match the 'momentary' behavior of pointer input,
// the button is released when it loses focus.
if ( this.pdomClickingProperty.value ) {
if ( down && valueProperty.value === valueOff ) {

// Button is down from alt input while off, turn on.
valueProperty.set( valueOn );

// In this activation the downProperty is going to be set to false right away. This flag prevents the
// button from turning off the button until the next click.
wasClickedWhileOn = false;
}
if ( !down && valueProperty.value === valueOn ) {
if ( wasClickedWhileOn ) {

// Button is up from alt input while on, and it was clicked while on, turn off.
valueProperty.set( valueOff );
}
else {

// Button is up from alt input and it was not pressed while on, so it should remain on. Set
// the flag so that it will turn off in the next click.
wasClickedWhileOn = true;
}
}
}
else {
valueProperty.set( valueOff );

// turn on when pressed (if enabled)
if ( down ) {
if ( this.enabledProperty.get() ) {
valueProperty.set( valueOn );
}
}
else {
valueProperty.set( valueOff );
}
}
};
this.downProperty.lazyLink( downListener );

// Turn off when focus is lost.
const focusedListener = ( focused: boolean ) => {
if ( !focused ) {
valueProperty.set( valueOff );
}
};
this.focusedProperty.lazyLink( focusedListener );

this.valueProperty = valueProperty;
this.valueOn = valueOn;

// if valueProperty set externally, signify to ButtonModel
const valuePropertyListener = ( value: T ) => {
this.downProperty.set( value === valueOn );
Expand All @@ -62,6 +112,7 @@ export default class MomentaryButtonModel<T> extends ButtonModel {

this.disposeMomentaryButtonModel = () => {
this.downProperty.unlink( downListener );
this.focusedProperty.unlink( focusedListener );
valueProperty.unlink( valuePropertyListener );
};
}
Expand Down

0 comments on commit 163e2a4

Please sign in to comment.