-
Notifications
You must be signed in to change notification settings - Fork 6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
restructure home screen buttons #601
Comments
I'm hesitant to use the exact term "radio button group" because clicking on a selected element a 2nd time has a different effect than a traditional radio button group. Maybe just call it I agree with the main idea of this issue. Last time we worked on this, we didn't have time to revise joist to have a single type for each of the buttons (where each type has 2 sizes), but perhaps now we can make time for it. There is also a lot of a11y code in HomeScreenView.js, we may need help being able to test that we don't disrupt it. Also, it would be nice to bring HomeScreenView to ES6. |
I took a quick look at the implementation... Thoughts:
|
From #597 (comment):
CK: This item was improved by this issue but not solved. I'm moving it back to #597 for discussion. |
progress patch: Index: js/HomeScreenView.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- js/HomeScreenView.js (revision 9586a3026456abab9f0daefe533a882144b74b17)
+++ js/HomeScreenView.js (date 1582776491000)
@@ -17,7 +17,6 @@
const merge = require( 'PHET_CORE/merge' );
const Node = require( 'SCENERY/nodes/Node' );
const PhetFont = require( 'SCENERY_PHET/PhetFont' );
- const Property = require( 'AXON/Property' );
const ScreenButton = require( 'JOIST/ScreenButton' );
const ScreenView = require( 'JOIST/ScreenView' );
const StringUtils = require( 'PHETCOMMON/util/StringUtils' );
@@ -80,77 +79,43 @@
this.addChild( title );
title.scale( Math.min( 1, 0.9 * this.layoutBounds.width / title.width ) );
- // Keep track of which screen is highlighted so the same screen can remain highlighted even if nodes are replaced
- // (say when one grows larger or smaller).
- // TODO: This will be eliminated when we combine the buttons, see https://github.com/phetsims/joist/issues/601
- const highlightedScreenIndexProperty = new Property( -1 );
+ const screenButtonGroupTandem = tandem.createTandem( 'screenButtonGroup' );
const screenElements = _.map( model.simScreens, function( screen, index ) {
assert && assert( screen.name, 'name is required for screen ' + model.simScreens.indexOf( screen ) );
assert && assert( screen.homeScreenIcon, 'homeScreenIcon is required for screen ' + screen.name );
- // Even though in the user interface the small and large buttons seem like a single UI component that has grown
- // larger, it would be quite a headache to create a composite button for the purposes of tandem, so instead the
- // large and small buttons are registered as separate instances. See https://github.com/phetsims/phet-io/issues/99
- const largeTandem = tandem.createTandem( screen.tandem.name + 'LargeButton' );
+ const screenButton = new ScreenButton(
+ screen,
+ model, {
+
+ // Don't resize the VBox or it will shift down when the border becomes thicker
+ resize: false,
+ cursor: 'pointer',
- // a11y
- const a11yScreenButtonOptions = {
- tagName: 'button',
- innerContent: screen.name,
- descriptionContent: screen.descriptionContent,
- appendDescription: true,
- containerTagName: 'li'
- };
-
- const largeScreenButton = new ScreenButton(
- true,
- screen,
- model,
- highlightedScreenIndexProperty,
- merge( a11yScreenButtonOptions, {
-
- // Don't resize the VBox or it will shift down when the border becomes thicker
- resize: false,
- cursor: 'pointer',
- tandem: largeTandem
- } ) );
-
- // Even though in the user interface the small and large buttons seem like a single UI component that has grown
- // larger, it would be quite a headache to create a composite button for the purposes of tandem, so instead the
- // large and small buttons are registered as separate instances. See https://github.com/phetsims/phet-io/issues/99
- const smallTandem = tandem.createTandem( screen.tandem.name + 'SmallButton' );
+ // a11y
+ tagName: 'button',
+ innerContent: screen.name,
+ descriptionContent: screen.descriptionContent,
+ appendDescription: true,
+ containerTagName: 'li',
- const smallScreenButton = new ScreenButton(
- false,
- screen,
- model,
- highlightedScreenIndexProperty,
- merge( a11yScreenButtonOptions, {
- spacing: 3,
- cursor: 'pointer',
- showUnselectedHomeScreenIconFrame: screen.showUnselectedHomeScreenIconFrame,
- tandem: smallTandem
- } ) );
-
- smallScreenButton.addInputListener( smallScreenButton.highlightListener );
- largeScreenButton.addInputListener( smallScreenButton.highlightListener );
+ // phet-io
+ tandem: screenButtonGroupTandem.createTandem( screen.tandem.name + 'Button' )
+ } );
// a11y support for click listeners on the screen buttons
const toggleListener = function() {
- smallScreenButton.visible && smallScreenButton.focus();
- largeScreenButton.visible && largeScreenButton.focus();
+ screenButton.visible && screenButton.focus();
};
- smallScreenButton.addInputListener( { focus: toggleListener } );
- largeScreenButton.addInputListener( { click: toggleListener } );
- // largeScreenButton.mouseArea = largeScreenButton.touchArea = Shape.bounds( largeScreenButton.bounds ); // cover the gap in the vbox
+ screenButton.addInputListener( { focus: toggleListener } );
+ screenButton.addInputListener( { click: toggleListener } );
// a11y - add the right aria attributes to the buttons
- smallScreenButton.setAccessibleAttribute( 'aria-roledescription', simScreenString );
- largeScreenButton.setAccessibleAttribute( 'aria-roledescription', simScreenString );
+ screenButton.setAccessibleAttribute( 'aria-roledescription', simScreenString );
- return { screen: screen, small: smallScreenButton, large: largeScreenButton, index: index };
+ return { screen: screen, button: screenButton };
} );
// a11y this is needed to create the right PDOM structure, the phet menu shouldn't be a child of this 'nav', so
@@ -197,9 +162,9 @@
// check for the current screen
if ( screenElement.screen === selectedScreen ) {
- self.highlightedScreenButton = screenElement.large;
+ self.highlightedScreenButton = screenElement.button;
}
- return screenElement.screen === selectedScreen ? screenElement.large : screenElement.small;
+ return screenElement.button;
} );
hBox = new HBox( {
spacing: spacing,
Index: js/ScreenButton.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- js/ScreenButton.js (revision 9586a3026456abab9f0daefe533a882144b74b17)
+++ js/ScreenButton.js (date 1582776058000)
@@ -10,6 +10,7 @@
'use strict';
// modules
+ const DerivedProperty = require( 'AXON/DerivedProperty' );
const EventType = require( 'TANDEM/EventType' );
const FireListener = require( 'SCENERY/listeners/FireListener' );
const Frame = require( 'JOIST/Frame' );
@@ -31,14 +32,12 @@
const LARGE_ICON_HEIGHT = 140;
/**
- * @param {boolean} large - whether or not this is a large or small screenButton
* @param {Screen} screen
* @param {HomeScreenModel} model REVIEW: `model` threw me, since it's a model specifically for the homeScreen. https://github.com/phetsims/joist/issues/602
- * @param {Property} highlightedScreenIndexProperty REVIEW: Why is this still based on screen index? https://github.com/phetsims/joist/issues/602
* @param {Object} [options]
* @constructor
*/
- function ScreenButton( large, screen, model, highlightedScreenIndexProperty, options ) {
+ function ScreenButton( screen, model, options ) {
options = merge( {
showUnselectedHomeScreenIconFrame: false, // put a frame around unselected home screen icons
@@ -48,72 +47,73 @@
phetioDocumentation: 'A pressable button in the simulation, in the home screen'
}, options );
- const index = model.simScreens.indexOf( screen );
+ const isSelectedScreenIconProperty = new DerivedProperty( [ model.selectedScreenProperty ], selectedScreen => {
+ return selectedScreen === screen;
+ } );
+
+ // Text for the screen button
+ const text = new Text( screen.name, {
+ font: new PhetFont( isSelectedScreenIconProperty.value ? 42 : 18 ),
+ tandem: options.tandem.createTandem( 'text' )
+ } );
+
+ // The children are needed in the VBox constructor, but the rest of the options should be mutated later.
+ VBox.call( this, {
+ children: [ text ]
+ } );
+
+ isSelectedScreenIconProperty.link( isSelectedScreenIcon => {
- // Maps the number of screens to a scale for the small icons. The scale is percentage of LARGE_ICON_HEIGHT.
- let smallIconScale = Utils.linear( 2, 4, 0.875, 0.50, model.simScreens.length );
- if ( model.simScreens.length >= 5 ) {
- smallIconScale = 0.4;
- }
+ // Maps the number of screens to a scale for the small icons. The scale is percentage of LARGE_ICON_HEIGHT.
+ let smallIconScale = Utils.linear( 2, 4, 0.875, 0.50, model.simScreens.length );
+ if ( model.simScreens.length >= 5 ) {
+ smallIconScale = 0.4;
+ }
- // Use the small icon scale if this is a small screen button
- const height = large ? LARGE_ICON_HEIGHT : smallIconScale * LARGE_ICON_HEIGHT;
+ // Use the small icon scale if this is a small screen button
+ const height = isSelectedScreenIcon ? LARGE_ICON_HEIGHT : smallIconScale * LARGE_ICON_HEIGHT;
- // Wrap in a Node because we're scaling, and the same icon will be used for small and large icon, and may be used by
- // the navigation bar.
- const icon = new Node( {
- opacity: options.opacity,
- children: [ screen.homeScreenIcon ],
- scale: height / screen.homeScreenIcon.height
- } );
+ // Wrap in a Node because we're scaling, and the same icon will be used for small and large icon, and may be used by
+ // the navigation bar.
+ const icon = new Node( {
+ opacity: options.opacity,
+ children: [ screen.homeScreenIcon ],
+ scale: height / screen.homeScreenIcon.height
+ } );
- // Frame for small (unselected) home screen icons
- const frame = large ? new Frame( icon ) : new Rectangle( 0, 0, icon.width, icon.height, {
- stroke: options.showUnselectedHomeScreenIconFrame ? PhetColorScheme.SCREEN_ICON_FRAME : null,
- lineWidth: 0.7
- } );
+ // Frame for small (unselected) home screen icons
+ const frame = isSelectedScreenIcon ? new Frame( icon ) : new Rectangle( 0, 0, icon.width, icon.height, {
+ stroke: options.showUnselectedHomeScreenIconFrame ? PhetColorScheme.SCREEN_ICON_FRAME : null,
+ lineWidth: 0.7
+ } );
- // Create the icon with the frame inside
- const iconWithFrame = new Node( {
- opacity: options.opacity,
- children: [ frame, icon ]
- } );
+ frame.setHighlighted && frame.setHighlighted( isSelectedScreenIcon );
+ icon.opacity = isSelectedScreenIcon ? 1 : 0.5;
+ text.fill = isSelectedScreenIcon ? 'white' : 'gray';
+
+ // Create the icon with the frame inside
+ const iconWithFrame = new Node( {
+ opacity: options.opacity,
+ children: [ frame, icon ]
+ } );
- // Text for the screen button
- const text = new Text( screen.name, {
- font: new PhetFont( large ? 42 : 18 ),
- fill: large ? PhetColorScheme.BUTTON_YELLOW : 'gray',
- tandem: options.tandem.createTandem( 'text' )
- } );
+ text.font = new PhetFont( isSelectedScreenIcon ? 42 : 18 );
- // Shrink the text if it goes beyond the edge of the image
- text.maxWidth = iconWithFrame.width;
+ // Shrink the text if it goes beyond the edge of the image
+ text.maxWidth = iconWithFrame.width;
- // Only link if a large button
- const highlightListener = function( highlightedIndex ) {
- const highlighted = highlightedIndex === index;
- frame.setHighlighted && frame.setHighlighted( highlighted );
- icon.opacity = ( large || highlighted ) ? 1 : 0.5;
- text.fill = ( large || highlighted ) ? 'white' : 'gray';
- };
- highlightedScreenIndexProperty.link( highlightListener );
-
- // The children are needed in the VBox constructor, but the rest of the options should be mutated later.
- VBox.call( this, {
- children: [
- iconWithFrame,
- text
- ]
+ this.children = [ iconWithFrame, text ];
} );
// Input listeners after the parent call depending on if the ScreenButton is large or small
- const buttonDown = large ? () => {
- model.screenProperty.value = screen;
- highlightedScreenIndexProperty.value = -1;
- } :
- () => {
- model.selectedScreenProperty.value = screen;
- };
+ const buttonDown = () => {
+ if ( isSelectedScreenIconProperty.value ) {
+ model.screenProperty.value = screen;
+ }
+ else {
+ model.selectedScreenProperty.value = screen;
+ }
+ };
const fireListener = new FireListener( {
fireOnDown: true, // to match prior behavior, but I'm not sure why we have this exceptional behavior
@@ -121,45 +121,24 @@
tandem: options.tandem.createTandem( 'inputListener' )
} );
this.addInputListener( fireListener );
- this.addInputListener( { click: function() { large && fireListener.fire( null ); } } );
- this.addInputListener( { focus: function() { !large && fireListener.fire( null ); } } );
- this.addInputListener( {
- focus: function() {
- highlightedScreenIndexProperty.value = index;
- },
- blur: function() {
- highlightedScreenIndexProperty.value = -1;
- }
- } );
+ this.addInputListener( { click: function() { isSelectedScreenIconProperty.value && fireListener.fire( null ); } } );
+ this.addInputListener( { focus: function() { !isSelectedScreenIconProperty.value && fireListener.fire( null ); } } );
- // Set highlight listeners to the small screen button
- if ( !large ) {
-
- // @public (joist-internal)
- this.highlightListener = {
- over: function( event ) {
- highlightedScreenIndexProperty.value = index;
- },
- out: function( event ) {
- highlightedScreenIndexProperty.value = -1;
- }
- };
- // On the home screen if you touch an inactive screen thumbnail, it grows. If then without lifting your finger
- // you swipe over to the next thumbnail, that one would grow.
- this.addInputListener( {
- over: function( event ) {
- if ( event.pointer instanceof Touch ) {
- model.selectedScreenProperty.value = screen;
- }
- }
- } );
- }
+ // On the home screen if you touch an inactive screen thumbnail, it grows. If then without lifting your finger
+ // you swipe over to the next thumbnail, that one would grow.
+ this.addInputListener( {
+ over: function( event ) {
+ if ( event.pointer instanceof Touch ) {
+ model.selectedScreenProperty.value = screen;
+ }
+ }
+ } );
this.mouseArea = this.touchArea = Shape.bounds( this.bounds ); // cover the gap in the vbox
this.disposeScreenButton = function() {
- highlightedScreenIndexProperty.unlink( highlightListener );
+ isSelectedScreenIconProperty.dispose();
};
this.mutate( options ); I need to fix the text layout, do more cleanup, and recover some lost logic relating to hover behavior - it looks like I misunderstood the purpose of Other than those things, the behavior seems normal and the Studio looks nice. Once they are complete, I'll need assistance to check on a11y behavior from the changes. |
Implemented above. Tested with screens 1,2,3,4,5,6, screens 1,2,3,4,5, screens 1,2,3,4 etc. of Test links:
I ended up just going with @zepumph please review. I apologize - I forgot that renaming the file in the same commit omits a helpful commit diff. I pasted the before file below if you'd like to compare the new version with clipboard. // Copyright 2017-2020, University of Colorado Boulder
/**
* A ScreenButton is displayed on the HomeScreen. There are small and large ScreenButtons, that can be toggled through
* to select the desired sim screen to go to. See HomeScreenView.js for more information.
*
* @author Michael Kauzmann (PhET Interactive Simulations)
*/
import Utils from '../../dot/js/Utils.js';
import Shape from '../../kite/js/Shape.js';
import inherit from '../../phet-core/js/inherit.js';
import merge from '../../phet-core/js/merge.js';
import PhetColorScheme from '../../scenery-phet/js/PhetColorScheme.js';
import PhetFont from '../../scenery-phet/js/PhetFont.js';
import Touch from '../../scenery/js/input/Touch.js';
import FireListener from '../../scenery/js/listeners/FireListener.js';
import Node from '../../scenery/js/nodes/Node.js';
import Rectangle from '../../scenery/js/nodes/Rectangle.js';
import Text from '../../scenery/js/nodes/Text.js';
import VBox from '../../scenery/js/nodes/VBox.js';
import EventType from '../../tandem/js/EventType.js';
import Tandem from '../../tandem/js/Tandem.js';
import Frame from './Frame.js';
import joist from './joist.js';
// constants
const LARGE_ICON_HEIGHT = 140;
/**
* @param {boolean} large - whether or not this is a large or small screenButton
* @param {Screen} screen
* @param {HomeScreenModel} model REVIEW: `model` threw me, since it's a model specifically for the homeScreen. https://github.com/phetsims/joist/issues/602
* @param {Property} highlightedScreenIndexProperty REVIEW: Why is this still based on screen index? https://github.com/phetsims/joist/issues/602
* @param {Object} [options]
* @constructor
*/
function ScreenButton( large, screen, model, highlightedScreenIndexProperty, options ) {
options = merge( {
showUnselectedHomeScreenIconFrame: false, // put a frame around unselected home screen icons
opacity: 1, // The small screen's nodes have an opacity of .5
tandem: Tandem.REQUIRED, // To be passed into mutate, but tandem should be a required param in joist
phetioEventType: EventType.USER,
phetioDocumentation: 'A pressable button in the simulation, in the home screen'
}, options );
const index = model.simScreens.indexOf( screen );
// Maps the number of screens to a scale for the small icons. The scale is percentage of LARGE_ICON_HEIGHT.
let smallIconScale = Utils.linear( 2, 4, 0.875, 0.50, model.simScreens.length );
if ( model.simScreens.length >= 5 ) {
smallIconScale = 0.4;
}
// Use the small icon scale if this is a small screen button
const height = large ? LARGE_ICON_HEIGHT : smallIconScale * LARGE_ICON_HEIGHT;
// Wrap in a Node because we're scaling, and the same icon will be used for small and large icon, and may be used by
// the navigation bar.
const icon = new Node( {
opacity: options.opacity,
children: [ screen.homeScreenIcon ],
scale: height / screen.homeScreenIcon.height
} );
// Frame for small (unselected) home screen icons
const frame = large ? new Frame( icon ) : new Rectangle( 0, 0, icon.width, icon.height, {
stroke: options.showUnselectedHomeScreenIconFrame ? PhetColorScheme.SCREEN_ICON_FRAME : null,
lineWidth: 0.7
} );
// Create the icon with the frame inside
const iconWithFrame = new Node( {
opacity: options.opacity,
children: [ frame, icon ]
} );
// Text for the screen button
const text = new Text( screen.name, {
font: new PhetFont( large ? 42 : 18 ),
fill: large ? PhetColorScheme.BUTTON_YELLOW : 'gray',
tandem: options.tandem.createTandem( 'text' )
} );
// Shrink the text if it goes beyond the edge of the image
text.maxWidth = iconWithFrame.width;
// Only link if a large button
const highlightListener = function( highlightedIndex ) {
const highlighted = highlightedIndex === index;
frame.setHighlighted && frame.setHighlighted( highlighted );
icon.opacity = ( large || highlighted ) ? 1 : 0.5;
text.fill = ( large || highlighted ) ? 'white' : 'gray';
};
highlightedScreenIndexProperty.link( highlightListener );
// The children are needed in the VBox constructor, but the rest of the options should be mutated later.
VBox.call( this, {
children: [
iconWithFrame,
text
]
} );
// Input listeners after the parent call depending on if the ScreenButton is large or small
const buttonDown = large ? () => {
model.screenProperty.value = screen;
highlightedScreenIndexProperty.value = -1;
} :
() => {
model.selectedScreenProperty.value = screen;
};
const fireListener = new FireListener( {
fireOnDown: true, // to match prior behavior, but I'm not sure why we have this exceptional behavior
fire: buttonDown,
tandem: options.tandem.createTandem( 'inputListener' )
} );
this.addInputListener( fireListener );
this.addInputListener( { click: function() { large && fireListener.fire( null ); } } );
this.addInputListener( { focus: function() { !large && fireListener.fire( null ); } } );
this.addInputListener( {
focus: function() {
highlightedScreenIndexProperty.value = index;
},
blur: function() {
highlightedScreenIndexProperty.value = -1;
}
} );
// Set highlight listeners to the small screen button
if ( !large ) {
// @public (joist-internal)
this.highlightListener = {
over: function( event ) {
highlightedScreenIndexProperty.value = index;
},
out: function( event ) {
highlightedScreenIndexProperty.value = -1;
}
};
// On the home screen if you touch an inactive screen thumbnail, it grows. If then without lifting your finger
// you swipe over to the next thumbnail, that one would grow.
this.addInputListener( {
over: function( event ) {
if ( event.pointer instanceof Touch ) {
model.selectedScreenProperty.value = screen;
}
}
} );
}
this.mouseArea = this.touchArea = Shape.bounds( this.bounds ); // cover the gap in the vbox
this.disposeScreenButton = function() {
highlightedScreenIndexProperty.unlink( highlightListener );
};
this.mutate( options );
}
joist.register( 'ScreenButton', ScreenButton );
export default inherit( VBox, ScreenButton, {
// @public
dispose: function() {
this.disposeScreenButton();
VBox.prototype.dispose.call( this );
}
} ); |
This looks awesome in Studio, so much nicer. Thanks for tackling this @chrisklus. |
... and |
I compared https://phet.colorado.edu/sims/html/number-line-integers/1.0.0/number-line-integers_en.html and found that, as I moved between the two tabs in different home screen states, they look exactly identical (from what my eyes can see). Good work!
Good work! Things are looking so much better than before. I appreciate you taking the lead on this one. |
|
I reverted a line that shouldn't have been removed during my refactor and the problem described in #601 (comment) appears to be fixed. @zepumph then gave this a look and we also added events to the keyboard input listeners.
|
…bject for toggling between large state and small state, see #601
Thanks for great feedback @zepumph. Please review my responses in #601 (comment) and the commits above. I copied one item down here that I would like more input on:
I looked into this a bit and think that it's simpler to understand and implement in the way it is (over using ButtonModel.js) - let me know if you disagree. If we leave it implemented as is, do you think it'd be worth splitting the model pieces into their own file like HomeScreenButtonModel.js? @arouinfar could you review these two comments from @zepumph?
|
I think that it is organized pretty well as is, and don't think more work is needed. The loose structure is something like:
That feels normal and nice to me. Good work! |
That also seems fine to me, but not sure if this is moot given the conclusion to the question below...
Thanks for the clarification @chrisklus. I don't think either of these properties need to be instrumented. If clients want to know which button is "big" on the HomeScreen, they can see this in |
@chrisklus let me know if I can be of more help here. |
Thanks @zepumph and @arouinfar, sounds good. There was one remaining item from that was improved, but not fixed, by the work in this issue. Since the remainder of that problem doesn't relate to the home screen buttons, I'm going to close this issue and move that back to #597 for further discussion. |
This came up in a Zoom discussion with @arouinfar, @kathy-phet, @zepumph, and @chrisklus.
ph-scale has 3 screen: Macro, Micro, and My Solution. Here's the Studio tree structure for the home screen:
Exposing the large and small buttons separately doesn't seem like the right approach. There should be 1 button for each screen, with a Property that indicates whether it's selected. Or better yet, these should act like (and be structured like) a radio button group.
Maybe something like this:
The text was updated successfully, but these errors were encountered: