Skip to content

Commit

Permalink
PDOM labelContent/ariaLabel/ariaValueText improvements for #1442
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathanolson committed Dec 6, 2023
1 parent ec3dc48 commit 5f098f7
Showing 1 changed file with 98 additions and 40 deletions.
138 changes: 98 additions & 40 deletions js/accessibility/pdom/ParallelDOM.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ export default class ParallelDOM extends PhetioObject {

// The label content for this node's DOM element. There are multiple ways that a label
// can be associated with a node's dom element, see setLabelContent() for more documentation
private _labelContent: string | null;
private _labelContent: PDOMValueType | null = null;

// The inner label content for this node's primary sibling. Set as inner HTML
// or text content of the actual DOM element. If this is used, the node should not have children.
Expand All @@ -421,7 +421,8 @@ export default class ParallelDOM extends PhetioObject {

// If provided, "aria-label" will be added as an inline attribute on the node's DOM
// element and set to this value. This will determine how the Accessible Name is provided for the DOM element.
private _ariaLabel: string | null;
private _ariaLabel: PDOMValueType | null = null;
private _hasAppliedAriaLabel = false;

// The ARIA role for this Node's primary sibling, added as an HTML attribute. For a complete
// list of ARIA roles, see https://www.w3.org/TR/wai-aria/roles. Beware that many roles are not supported
Expand All @@ -436,7 +437,8 @@ export default class ParallelDOM extends PhetioObject {

// If provided, "aria-valuetext" will be added as an inline attribute on the Node's
// primary sibling and set to this value. Setting back to null will clear this attribute in the view.
private _ariaValueText: string | null;
private _ariaValueText: PDOMValueType | null = null;
private _hasAppliedAriaValueText = false;

// Keep track of what this Node is aria-labelledby via "associationObjects"
// see addAriaLabelledbyAssociation for why we support more than one association.
Expand Down Expand Up @@ -569,13 +571,19 @@ export default class ParallelDOM extends PhetioObject {

protected _onPDOMContentChangeListener: () => void;
protected _onInputValueChangeListener: () => void;
protected _onAriaLabelChangeListener: () => void;
protected _onAriaValueTextChangeListener: () => void;
protected _onLabelContentChangeListener: () => void;

protected constructor( options?: PhetioObjectOptions ) {

super( options );

this._onPDOMContentChangeListener = this.onPDOMContentChange.bind( this );
this._onInputValueChangeListener = this.invalidatePeerInputValue.bind( this );
this._onAriaLabelChangeListener = this.onAriaLabelChange.bind( this );
this._onAriaValueTextChangeListener = this.onAriaValueTextChange.bind( this );
this._onLabelContentChangeListener = this.invalidatePeerLabelSiblingContent.bind( this );

this._tagName = null;
this._containerTagName = null;
Expand All @@ -587,17 +595,14 @@ export default class ParallelDOM extends PhetioObject {
this._appendDescription = false;
this._pdomAttributes = [];
this._pdomClasses = [];
this._labelContent = null;

this._innerContentProperty = new TinyForwardingProperty<string | null>( null, false );
this._innerContentProperty.lazyLink( this.onInnerContentPropertyChange.bind( this ) );

this._descriptionContent = null;
this._pdomNamespace = null;
this._ariaLabel = null;
this._ariaRole = null;
this._containerAriaRole = null;
this._ariaValueText = null;
this._ariaLabelledbyAssociations = [];
this._nodesThatAreAriaLabelledbyThisNode = [];
this._ariaDescribedbyAssociations = [];
Expand Down Expand Up @@ -661,6 +666,18 @@ export default class ParallelDOM extends PhetioObject {
this._inputValue = null;
}

if ( isTReadOnlyProperty( this._ariaLabel ) && !this._ariaLabel.isDisposed ) {
this._ariaLabel.unlink( this._onAriaLabelChangeListener );
}

if ( isTReadOnlyProperty( this._ariaValueText ) && !this._ariaValueText.isDisposed ) {
this._ariaValueText.unlink( this._onAriaValueTextChangeListener );
}

if ( isTReadOnlyProperty( this._labelContent ) && !this._labelContent.isDisposed ) {
this._labelContent.unlink( this._onLabelContentChangeListener );
}

( this as unknown as Node ).inputEnabledProperty.unlink( this.pdomBoundInputEnabledListener );

// To prevent memory leaks, we want to clear our order (since otherwise nodes in our order will reference
Expand Down Expand Up @@ -1291,6 +1308,20 @@ export default class ParallelDOM extends PhetioObject {
return this._containerTagName;
}

private invalidatePeerLabelSiblingContent(): void {
const labelContent = this.labelContent;

// if trying to set labelContent, make sure that there is a labelTagName default
if ( !this._labelTagName ) {
this.setLabelTagName( DEFAULT_LABEL_TAG_NAME );
}

for ( let i = 0; i < this._pdomInstances.length; i++ ) {
const peer = this._pdomInstances[ i ].peer!;
peer.setLabelSiblingContent( labelContent );
}
}

/**
* Set the content of the label sibling for the this node. The label sibling will default to the value of
* DEFAULT_LABEL_TAG_NAME if no `labelTagName` is provided. If the label sibling is a `LABEL` html element,
Expand All @@ -1301,22 +1332,19 @@ export default class ParallelDOM extends PhetioObject {
*
* Passing a null label value will not clear the whole label sibling, just the inner content of the DOM Element.
*/
public setLabelContent( providedLabel: PDOMValueType | null ): void {
// If it's a Property, we'll just grab the initial value. See https://github.com/phetsims/scenery/issues/1442
const label = unwrapProperty( providedLabel );
public setLabelContent( labelContent: PDOMValueType | null ): void {
if ( labelContent !== this._labelContent ) {
if ( isTReadOnlyProperty( this._labelContent ) && !this._labelContent.isDisposed ) {
this._labelContent.unlink( this._onLabelContentChangeListener );
}

if ( this._labelContent !== label ) {
this._labelContent = label;
this._labelContent = labelContent;

// if trying to set labelContent, make sure that there is a labelTagName default
if ( !this._labelTagName ) {
this.setLabelTagName( DEFAULT_LABEL_TAG_NAME );
if ( isTReadOnlyProperty( labelContent ) ) {
labelContent.lazyLink( this._onLabelContentChangeListener );
}

for ( let i = 0; i < this._pdomInstances.length; i++ ) {
const peer = this._pdomInstances[ i ].peer!;
peer.setLabelSiblingContent( this._labelContent );
}
this.invalidatePeerLabelSiblingContent();
}
}

Expand All @@ -1328,7 +1356,7 @@ export default class ParallelDOM extends PhetioObject {
* Get the content for this Node's label sibling DOM element.
*/
public getLabelContent(): string | null {
return this._labelContent;
return unwrapProperty( this._labelContent );
}

/**
Expand Down Expand Up @@ -1475,23 +1503,38 @@ export default class ParallelDOM extends PhetioObject {
return this._containerAriaRole;
}

private onAriaValueTextChange(): void {
const ariaValueText = unwrapProperty( this._ariaValueText );

if ( ariaValueText === null ) {
if ( this._hasAppliedAriaLabel ) {
this.removePDOMAttribute( 'aria-valuetext' );
this._hasAppliedAriaLabel = false;
}
}
else {
this.setPDOMAttribute( 'aria-valuetext', ariaValueText );
this._hasAppliedAriaLabel = true;
}
}

/**
* Set the aria-valuetext of this Node independently from the changing value, if necessary. Setting to null will
* clear this attribute.
*/
public setAriaValueText( providedAriaValueText: PDOMValueType | null ): void {
// If it's a Property, we'll just grab the initial value. See https://github.com/phetsims/scenery/issues/1442
const ariaValueText = unwrapProperty( providedAriaValueText );

public setAriaValueText( ariaValueText: PDOMValueType | null ): void {
if ( this._ariaValueText !== ariaValueText ) {
if ( isTReadOnlyProperty( this._ariaValueText ) && !this._ariaValueText.isDisposed ) {
this._ariaValueText.unlink( this._onAriaLabelChangeListener );
}

this._ariaValueText = ariaValueText;

if ( ariaValueText === null ) {
this.removePDOMAttribute( 'aria-valuetext' );
}
else {
this.setPDOMAttribute( 'aria-valuetext', ariaValueText );
if ( isTReadOnlyProperty( ariaValueText ) ) {
ariaValueText.lazyLink( this._onAriaLabelChangeListener );
}

this.onAriaLabelChange();
}
}

Expand All @@ -1504,7 +1547,7 @@ export default class ParallelDOM extends PhetioObject {
* has not been set on the primary sibling.
*/
public getAriaValueText(): string | null {
return this._ariaValueText;
return unwrapProperty( this._ariaValueText );
}

/**
Expand Down Expand Up @@ -1542,26 +1585,41 @@ export default class ParallelDOM extends PhetioObject {
return this._pdomNamespace;
}

private onAriaLabelChange(): void {
const ariaLabel = unwrapProperty( this._ariaLabel );

if ( ariaLabel === null ) {
if ( this._hasAppliedAriaLabel ) {
this.removePDOMAttribute( 'aria-label' );
this._hasAppliedAriaLabel = false;
}
}
else {
this.setPDOMAttribute( 'aria-label', ariaLabel );
this._hasAppliedAriaLabel = true;
}
}

/**
* Sets the 'aria-label' attribute for labelling the Node's primary sibling. By using the
* 'aria-label' attribute, the label will be read on focus, but can not be found with the
* virtual cursor. This is one way to set a DOM Element's Accessible Name.
*
* @param providedAriaLabel - the text for the aria label attribute
* @param ariaLabel - the text for the aria label attribute
*/
public setAriaLabel( providedAriaLabel: PDOMValueType | null ): void {
// If it's a Property, we'll just grab the initial value. See https://github.com/phetsims/scenery/issues/1442
const ariaLabel = unwrapProperty( providedAriaLabel );

public setAriaLabel( ariaLabel: PDOMValueType | null ): void {
if ( this._ariaLabel !== ariaLabel ) {
if ( isTReadOnlyProperty( this._ariaLabel ) && !this._ariaLabel.isDisposed ) {
this._ariaLabel.unlink( this._onAriaLabelChangeListener );
}

this._ariaLabel = ariaLabel;

if ( ariaLabel === null ) {
this.removePDOMAttribute( 'aria-label' );
}
else {
this.setPDOMAttribute( 'aria-label', ariaLabel );
if ( isTReadOnlyProperty( ariaLabel ) ) {
ariaLabel.lazyLink( this._onAriaLabelChangeListener );
}

this.onAriaLabelChange();
}
}

Expand All @@ -1573,7 +1631,7 @@ export default class ParallelDOM extends PhetioObject {
* Get the value of the aria-label attribute for this Node's primary sibling.
*/
public getAriaLabel(): string | null {
return this._ariaLabel;
return unwrapProperty( this._ariaLabel );
}

/**
Expand Down

0 comments on commit 5f098f7

Please sign in to comment.