Skip to content

Commit

Permalink
Add a 'focusPanTargetBoundsProperty in the local coordinate frame for…
Browse files Browse the repository at this point in the history
… observable bounds changes for the animatedPanZoomSingleton, see #1558
  • Loading branch information
jessegreenberg committed Oct 12, 2023
1 parent e64ae32 commit aa42c79
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 30 deletions.
47 changes: 25 additions & 22 deletions js/accessibility/pdom/ParallelDOM.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ const ACCESSIBILITY_OPTION_KEYS = [
'ariaDescribedbyAssociations',
'activeDescendantAssociations',

'createFocusPanTargetBounds',
'focusPanTargetBoundsProperty',
'limitPanDirection',

'positionInPDOM',
Expand Down Expand Up @@ -288,7 +288,7 @@ export type ParallelDOMOptions = {
ariaDescribedbyAssociations?: Association[]; // sets the list of aria-describedby associations between from this node to others (including itself)
activeDescendantAssociations?: Association[]; // sets the list of aria-activedescendant associations between from this node to others (including itself)

createFocusPanTargetBounds?: ( () => Bounds2 ) | null; // A function that sets the global bounds for an AnimatedPanZoomListener to keep in view
focusPanTargetBoundsProperty?: TReadOnlyProperty<Bounds2> | null; // A Property with bounds that describe the bounds of this Node that should remain displayed by the global AnimatedPanZoomListener
limitPanDirection?: LimitPanDirection | null; // A constraint on the direction of panning when interacting with this Node.

positionInPDOM?: boolean; // Sets whether the node's DOM elements are positioned in the viewport
Expand Down Expand Up @@ -513,7 +513,7 @@ export default class ParallelDOM extends PhetioObject {

// If this is provided, the AnimatedPanZoomListener will attempt to keep this Node in view as long as it has
// focus
private _createFocusPanTargetBounds: ( () => Bounds2 ) | null;
private _focusPanTargetBoundsProperty: TReadOnlyProperty<Bounds2> | null;

// If provided, the AnimatedPanZoomListener will ONLY pan in the specified direction
private _limitPanDirection: LimitPanDirection | null;
Expand Down Expand Up @@ -608,7 +608,7 @@ export default class ParallelDOM extends PhetioObject {
this._pdomOrder = null;
this._pdomParent = null;
this._pdomTransformSourceNode = null;
this._createFocusPanTargetBounds = null;
this._focusPanTargetBoundsProperty = null;
this._limitPanDirection = null;
this._pdomDisplaysInfo = new PDOMDisplaysInfo( this as unknown as Node );
this._pdomInstances = [];
Expand Down Expand Up @@ -2581,39 +2581,42 @@ export default class ParallelDOM extends PhetioObject {
}

/**
* Sets a function on this Node that will be used by the animatedPanZoomSingleton. It will try to keep these global
* bounds visible in the viewport when this Node (or any ancestor) has a transformation change while actively
* focused. This is useful if the bounds of your focusable Node do not accurately surround the conceptual interactive
* component.
* Used by the animatedPanZoomSingleton. It will try to keep these bounds visible in the viewport when this Node
* (or any ancestor) has a transform change while focused. This is useful if the bounds of your focusable
* Node do not accurately surround the conceptual interactive component. If null, this Node's local bounds
* are used.
*
* @param createFocusPanTargetBounds - returns bounds in the global coordinate frame
* At this time, the Property cannot be changed after it is set.
*/
public setCreateFocusPanTargetBounds( createFocusPanTargetBounds: null | ( () => Bounds2 ) ): void {
this._createFocusPanTargetBounds = createFocusPanTargetBounds;
}
public setFocusPanTargetBoundsProperty( boundsProperty: null | TReadOnlyProperty<Bounds2> ): void {

// We may call this more than once with mutate
if ( boundsProperty !== this._focusPanTargetBoundsProperty ) {
assert && assert( !this._focusPanTargetBoundsProperty, 'Cannot change focusPanTargetBoundsProperty after it is set.' );
this._focusPanTargetBoundsProperty = boundsProperty;
}
}

/**
* Returns the function for creating global bounds to keep in the viewport while the component has focus, see the
* setCreateFocusPanTargetBounds function for more information.
* setFocusPanTargetBoundsProperty function for more information.
*/
public getCreateFocusPanTargetBounds(): null | ( () => Bounds2 ) {
return this._createFocusPanTargetBounds;
public getFocusPanTargetBoundsProperty(): null | TReadOnlyProperty<Bounds2> {
return this._focusPanTargetBoundsProperty;
}

/**
* See setCreateFocusPanTargetBounds for more information.
* @param createFocusPanTargetBounds
* See setFocusPanTargetBoundsProperty for more information.
*/
public set createFocusPanTargetBounds( createFocusPanTargetBounds: null | ( () => Bounds2 ) ) {
this.setCreateFocusPanTargetBounds( createFocusPanTargetBounds );
public set focusPanTargetBoundsProperty( boundsProperty: null | TReadOnlyProperty<Bounds2> ) {
this.setFocusPanTargetBoundsProperty( boundsProperty );
}

/**
* See getCreateFocusPanTargetBounds for more information.
* See getFocusPanTargetBoundsProperty for more information.
*/
public get createFocusPanTargetBounds(): null | ( () => Bounds2 ) {
return this.getCreateFocusPanTargetBounds();
public get focusPanTargetBoundsProperty(): null | TReadOnlyProperty<Bounds2> {
return this.getFocusPanTargetBoundsProperty();
}

/**
Expand Down
43 changes: 35 additions & 8 deletions js/listeners/AnimatedPanZoomListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ import platform from '../../../phet-core/js/platform.js';
import EventType from '../../../tandem/js/EventType.js';
import isSettingPhetioStateProperty from '../../../tandem/js/isSettingPhetioStateProperty.js';
import PhetioAction from '../../../tandem/js/PhetioAction.js';
import { EventIO, Focus, FocusManager, globalKeyStateTracker, Intent, KeyboardDragListener, KeyboardUtils, KeyboardZoomUtils, KeyStateTracker, Mouse, MultiListenerPress, Node, LimitPanDirection, PanZoomListener, PanZoomListenerOptions, PDOMPointer, PDOMUtils, Pointer, PressListener, scenery, SceneryEvent, Trail, TransformTracker } from '../imports.js';
import { EventIO, Focus, FocusManager, globalKeyStateTracker, Intent, KeyboardDragListener, KeyboardUtils, KeyboardZoomUtils, KeyStateTracker, LimitPanDirection, Mouse, MultiListenerPress, Node, PanZoomListener, PanZoomListenerOptions, PDOMPointer, PDOMUtils, Pointer, PressListener, scenery, SceneryEvent, Trail, TransformTracker } from '../imports.js';
import optionize, { EmptySelfOptions } from '../../../phet-core/js/optionize.js';
import Tandem from '../../../tandem/js/Tandem.js';
import BooleanProperty from '../../../axon/js/BooleanProperty.js';
import { PropertyLinkListener } from '../../../axon/js/TReadOnlyProperty.js';

// constants
const MOVE_CURSOR = 'all-scroll';
Expand Down Expand Up @@ -111,6 +112,10 @@ class AnimatedPanZoomListener extends PanZoomListener {
// the targetNode in view during animation.
private _transformTracker: TransformTracker | null = null;

// A listener on the focusPanTargetBoundsProperty of the focused Node that will keep those bounds displayed in
// the viewport.
private _focusBoundsListener: PropertyLinkListener<Bounds2> | null = null;

private readonly disposeAnimatedPanZoomListener: () => void;

/**
Expand Down Expand Up @@ -568,13 +573,24 @@ class AnimatedPanZoomListener extends PanZoomListener {

/**
* Handle a change of focus by immediately panning so that the focused Node is in view. Also sets up the
* TransformTracker which will automatically keep the target in the viewport as it is animates.
* TransformTracker which will automatically keep the target in the viewport as it is animates, and a listener
* on the focusPanTargetBoundsProperty (if provided) to handle Node other size or custom changes.
*/
public handleFocusChange( focus: Focus | null ): void {
public handleFocusChange( focus: Focus | null, previousFocus: Focus | null ): void {

// Remove listeners on the previous focus watching transform and bounds changes
if ( this._transformTracker ) {
this._transformTracker.dispose();
this._transformTracker = null;
}
if ( previousFocus && previousFocus.trail.lastNode() && previousFocus.trail.lastNode().focusPanTargetBoundsProperty ) {
const previousBoundsProperty = previousFocus.trail.lastNode().focusPanTargetBoundsProperty!;
assert && assert( this._focusBoundsListener && previousBoundsProperty.hasListener( this._focusBoundsListener ),
'Focus bounds listener should be linked to the previous Node'
);
previousBoundsProperty.unlink( this._focusBoundsListener! );
this._focusBoundsListener = null;
}

if ( focus ) {
const lastNode = focus.trail.lastNode();
Expand All @@ -590,14 +606,16 @@ class AnimatedPanZoomListener extends PanZoomListener {
}

this._transformTracker = new TransformTracker( trailToTrack );
this._transformTracker.addListener( () => {

const focusMovementListener = () => {
if ( this.getCurrentScale() > 1 ) {

let globalBounds: Bounds2;
if ( lastNode.createFocusPanTargetBounds ) {
if ( lastNode.focusPanTargetBoundsProperty ) {

// This Node has a custom global bounds area that we need to keep in view
globalBounds = lastNode.createFocusPanTargetBounds();
// This Node has a custom bounds area that we need to keep in view
const localBounds = lastNode.focusPanTargetBoundsProperty.value;
globalBounds = focus.trail.localToGlobalBounds( localBounds );
}
else {

Expand All @@ -608,7 +626,16 @@ class AnimatedPanZoomListener extends PanZoomListener {

this.keepBoundsInView( globalBounds, true, lastNode.limitPanDirection );
}
} );
};

// observe changes to the transform
this._transformTracker.addListener( focusMovementListener );

// observe changes on the client-provided local bounds
if ( lastNode.focusPanTargetBoundsProperty ) {
this._focusBoundsListener = focusMovementListener;
lastNode.focusPanTargetBoundsProperty.link( this._focusBoundsListener );
}

// Pan to the focus trail right away if it is off-screen
this.keepTrailInView( focus.trail, lastNode.limitPanDirection );
Expand Down

0 comments on commit aa42c79

Please sign in to comment.