phetsims / sun

User-interface components for PhET simulations, built on top of Scenery.
MIT License
4 stars 12 forks source link

RectangularRadioButtonItem API should allow createNode/tandemName #787

Closed samreid closed 1 year ago

samreid commented 2 years ago

From https://github.com/phetsims/sun/issues/746#issuecomment-1234801112, @pixelzoom said:

API-wise, this is looking nice. But AquaRadioButtonGroupItem and RectangularRadioButtonItem now have very different APIs. Can/should they be more similar?

samreid commented 2 years ago

I got very far on this but it is very complicated and I wanted to call for help. Current status:

```diff Index: main/circuit-construction-kit-common/js/view/ViewRadioButtonGroup.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/circuit-construction-kit-common/js/view/ViewRadioButtonGroup.ts b/main/circuit-construction-kit-common/js/view/ViewRadioButtonGroup.ts --- a/main/circuit-construction-kit-common/js/view/ViewRadioButtonGroup.ts (revision 7bfc9ae148271ef0e9607da3880662fff717a074) +++ b/main/circuit-construction-kit-common/js/view/ViewRadioButtonGroup.ts (date 1662088624728) @@ -20,6 +20,7 @@ import CircuitElementViewType from '../model/CircuitElementViewType.js'; import Vertex from '../model/Vertex.js'; import BatteryNode from './BatteryNode.js'; +import { Text } from '../../../scenery/js/imports.js'; // constants const BATTERY_LENGTH = CCKCConstants.BATTERY_LENGTH; @@ -70,15 +71,14 @@ isIcon: true, scale: SCALE } ); - const lifelikeIcon = createBatteryNode( CircuitElementViewType.LIFELIKE, tandem.createTandem( 'lifelikeIcon' ) ); - const schematicIcon = createBatteryNode( CircuitElementViewType.SCHEMATIC, tandem.createTandem( 'schematicIcon' ) ); super( viewTypeProperty, [ { value: CircuitElementViewType.LIFELIKE, - node: lifelikeIcon, + createNode: tandem => createBatteryNode( CircuitElementViewType.LIFELIKE, tandem.createTandem( 'lifelikeIcon' ) ), + createLabel: tandem => new Text( 'HELLO', { tandem: tandem.createTandem( 'labelNode' ) } ), tandemName: 'lifelikeRadioButton' }, { value: CircuitElementViewType.SCHEMATIC, - node: schematicIcon, + createNode: tandem => createBatteryNode( CircuitElementViewType.SCHEMATIC, tandem.createTandem( 'schematicIcon' ) ), tandemName: 'schematicRadioButton' } ], options ); } Index: main/sun/js/ChildComponentOptions.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/ChildComponentOptions.ts b/main/sun/js/ChildComponentOptions.ts --- a/main/sun/js/ChildComponentOptions.ts (revision 287175e0fbb94a6817750468c6b3b4de63fb18d3) +++ b/main/sun/js/ChildComponentOptions.ts (date 1662088367051) @@ -28,6 +28,15 @@ tandemName?: string; }; +export type NodeOrCreateNode = { + node: Node; + createNode?: never; +} | { + node?: never; + createNode: ( tandem: Tandem ) => Node; + tandemName?: string; +}; + export default ChildComponentOptions; /** @@ -35,7 +44,7 @@ * TODO: https://github.com/phetsims/sun/issues/746 should this be renamed? Should it just be a top level `export function`, * or does it belong elsewhere? */ -export function getNodes( array: ChildComponentOptions[], tandem: Tandem ): Node[] { +export function getNodes( array: NodeOrCreateNode[], tandem: Tandem ): Node[] { return array.map( item => { if ( item.node ) { return item.node; Index: main/sun/js/buttons/RectangularRadioButtonGroup.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/sun/js/buttons/RectangularRadioButtonGroup.ts b/main/sun/js/buttons/RectangularRadioButtonGroup.ts --- a/main/sun/js/buttons/RectangularRadioButtonGroup.ts (revision 287175e0fbb94a6817750468c6b3b4de63fb18d3) +++ b/main/sun/js/buttons/RectangularRadioButtonGroup.ts (date 1662088748644) @@ -22,6 +22,7 @@ import Property from '../../../axon/js/Property.js'; import optionize, { combineOptions } from '../../../phet-core/js/optionize.js'; import StrictOmit from '../../../phet-core/js/types/StrictOmit.js'; +import { getNodes } from '../ChildComponentOptions.js'; // pdom - Unique ID for each instance of RectangularRadioButtonGroup. Used to create the 'name' option that is passed // to each RectangularRadioButton in the group. All buttons in the group must have the same 'name', and that name @@ -34,16 +35,29 @@ // Describes one radio button export type RectangularRadioButtonItem = { - node: Node; // primary depiction for the button value: T; // value associated with the button - label?: Node; // optional label that appears outside the button phetioDocumentation?: string; // optional documentation for PhET-iO tandemName?: string; // optional tandem for PhET-iO tandem?: never; // use tandemName instead of a Tandem instance labelContent?: PDOMValueType; // optional label for a11y (description and voicing) voicingContextResponse?: VoicingResponse; descriptionContent?: PDOMValueType; // optional label for a11y -}; +} & ( { + node: Node; + createNode?: never; +} | { + node?: never; + createNode: ( tandem: Tandem ) => Node; + tandemName: string; +} ) & ( { + + // optional label that appears outside the button + label?: Node; + createLabel?: never; +} | { + label?: never; + createLabel?: ( tandem: Tandem ) => Node; +} ); /** * Identifies a radio button and its layout manager. Pointer areas and focus highlight need to be set on @@ -104,9 +118,6 @@ 'items must have unique values' ); assert && assert( _.find( items, item => ( item.value === property.value ) ), 'one radio button must be associated with property.value' ); - assert && assert( _.every( items, item => !item.node.hasPDOMContent ), - 'Accessibility is provided by RectangularRadioButton and RectangularRadioButtonItem.labelContent. ' + - 'Additional PDOM content in the provided Node could break accessibility.' ); const options = optionize()( { @@ -162,6 +173,11 @@ groupFocusHighlight: true }, providedOptions ); + const nodes = getNodes( items, options.tandem ); + assert && assert( _.every( nodes, node => !node.hasPDOMContent ), + 'Accessibility is provided by RectangularRadioButton and RectangularRadioButtonItem.labelContent. ' + + 'Additional PDOM content in the provided Node could break accessibility.' ); + assert && assert( options.soundPlayers === null || options.soundPlayers.length === items.length, 'If soundPlayers is provided, there must be one per radio button.' ); @@ -174,8 +190,8 @@ ); // Calculate the maximum width and height of the content, so we can make all radio buttons the same size. - const widestContentWidth = _.maxBy( items, item => item.node.width )!.node.width; - const tallestContentHeight = _.maxBy( items, item => item.node.height )!.node.height; + const widestContentWidth = _.maxBy( nodes, node => node.width )!.width; + const tallestContentHeight = _.maxBy( nodes, node => node.height )!.height; // Populated for each radio button in for loop const buttons: Array | FlowBox> = []; @@ -187,9 +203,10 @@ for ( let i = 0; i < items.length; i++ ) { const item = items[ i ]; + const node = nodes[ i ]; const radioButtonOptions = combineOptions( { - content: item.node, + content: node, minWidth: widestContentWidth + 2 * xMargin, minHeight: tallestContentHeight + 2 * yMargin, soundPlayer: options.soundPlayers ? options.soundPlayers[ i ] : @@ -229,10 +246,21 @@ radioButton.addChild( boundingRect ); let button; - if ( item.label ) { + if ( item.label || item.createLabel ) { + + // If a label is provided, the button becomes a FlowBox that manages layout of the button and label. + const label: Node = ( () => { + if ( item.label ) { + return item.label; + } + else if ( item.createLabel ) { + return item.createLabel( options.tandem.createTandem( item.tandemName! ).createTandem( 'labelNode' ) ); + } + else { + throw new Error( 'illegal state' ); + } + } )(); - // If a label is provided, the button becomes a FlowBox that manages layout of the button and label. - const label = item.label; const labelOrientation = ( options.labelAlign === 'bottom' || options.labelAlign === 'top' ) ? 'vertical' : 'horizontal'; const labelChildren = ( options.labelAlign === 'left' || options.labelAlign === 'top' ) ? [ label, radioButton ] : [ radioButton, label ]; button = new FlowBox( { ```

Perhaps after @pixelzoom or @zepumph reviews https://github.com/phetsims/sun/issues/746#issuecomment-1234801112 we can discuss this one?

pixelzoom commented 1 year ago

Based on https://github.com/phetsims/sun/issues/794... RectangularRadioButtonItem should not only allow createNode/tandemName, it should be converted to that form, and allow only that form.

And it looks like RectangularRadioButtonItem has the same problem as reported in https://github.com/phetsims/sun/issues/793 - RectangularRadioButtonItem.tandem is never used. So promoting this issue to a blocking bug.

pixelzoom commented 1 year ago

And it looks like RectangularRadioButtonItem has the same problem as reported in https://github.com/phetsims/sun/issues/793 - RectangularRadioButtonItem.tandem is never used. So promoting this issue to a blocking bug.

Moved to https://github.com/phetsims/sun/issues/795

pixelzoom commented 1 year ago

@samreid said:

I got very far on this but it is very complicated and I wanted to call for help. Current status: ...

@samreid does this get any less complicated by the simpification of GroupItemOptions that was made in #794?

Raising priority of this, since RectangularRadioButtonGroup is the last "group" to be addressed. And there has been good momentum on finishing up #794 and #746.

pixelzoom commented 1 year ago

There are 39 occurrences of new RectangularRadioButtonGroup(, 24 occurrences of extends RectangularRadioButtonGroup {.

samreid commented 1 year ago

Once we eliminate the tandem option, we can eliminate this line https://github.com/phetsims/sun/commit/68e494d841953e4a7edf242ca5ae9ed3f39d0f0e

samreid commented 1 year ago

I'll message slack and the google group:

In https://github.com/phetsims/sun/issues/787 we committed an API breaking change. RectangularRadioButtonItem no longer uses node + tandem, it now uses createNode + tandemName.

samreid commented 1 year ago

Implemented and ready for review. @pixelzoom do you have time to take a look?

pixelzoom commented 1 year ago

Looks good. Memory leak fixed in https://github.com/phetsims/sun/commit/b887e81ed43769c046ef9b1da5e2492db9008e63. Closing.

zepumph commented 1 year ago

What was your metric for ensuring we found all usages here? I just saw

https://github.com/phetsims/circuit-construction-kit-black-box-study/blob/0d3588fff2b7b111d4dca1252ac3af1c347a365f/js/blackbox/view/ModeRadioButtonGroup.js#L26

This should be failing an assertion, but perhaps we took out an assertion, thinking that TypeScript was the solution? I think verbose and redundant assertions are still very valuable in our scenery-phet and sun libraries, as we likely will never convert all our simulations to TypeScript, but will want to continue improving our libraries and common-code APIs.

zepumph commented 1 year ago

Wait, shouldn't this be getting caught with https://github.com/phetsims/sun/blob/cb5784eaf0e86d0039aa83e722dd92a24fbe818a/js/GroupItemOptions.ts#L31? Now I'm just confused. Sorry if I have made a mistake here. Can you give me some clarity?

samreid commented 1 year ago

The Black Box Study screen is commented out, and not exercised. I spent some time trying to get it running again to see if that error was triggered, and I was blocked by too many upstream errors. So I think we can trust the assertion in https://github.com/phetsims/sun/issues/787#issuecomment-1502014693

What was your metric for ensuring we found all usages here?

Maybe the answer to that is CT? And Black Box was an outlier since it is not being exercised?

zepumph commented 1 year ago

What was your metric for ensuring we found all usages here?

And that assertion that I didn't see before in GroupItemOptions.ts. Thanks for the BBS update, I'm ready to close again. I appreciate your quick response.