Skip to content

Commit

Permalink
factor out ComboBoxButtonNode, #430
Browse files Browse the repository at this point in the history
Signed-off-by: Chris Malley <[email protected]>
  • Loading branch information
pixelzoom committed Jan 7, 2019
1 parent 1cd60a2 commit 920d63a
Show file tree
Hide file tree
Showing 3 changed files with 196 additions and 173 deletions.
175 changes: 4 additions & 171 deletions js/ComboBox.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ define( require => {

// modules
const AccessiblePeer = require( 'SCENERY/accessibility/AccessiblePeer' );
const ComboBoxButtonNode = require( 'SUN/ComboBoxButtonNode' );
const ComboBoxIO = require( 'SUN/ComboBoxIO' );
const ComboBoxItem = require( 'SUN/ComboBoxItem' );
const ComboBoxItemNode = require( 'SUN/ComboBoxItemNode' );
Expand All @@ -22,12 +23,9 @@ define( require => {
const inherit = require( 'PHET_CORE/inherit' );
const InstanceRegistry = require( 'PHET_CORE/documentation/InstanceRegistry' );
const KeyboardUtil = require( 'SCENERY/accessibility/KeyboardUtil' );
const Line = require( 'SCENERY/nodes/Line' );
const Node = require( 'SCENERY/nodes/Node' );
const Path = require( 'SCENERY/nodes/Path' );
const Property = require( 'AXON/Property' );
const Rectangle = require( 'SCENERY/nodes/Rectangle' );
const Shape = require( 'KITE/Shape' );
const sun = require( 'SUN/sun' );
const Tandem = require( 'TANDEM/Tandem' );
const Vector2 = require( 'DOT/Vector2' );
Expand All @@ -36,24 +34,6 @@ define( require => {
// const
const LIST_POSITION_VALUES = [ 'above', 'below' ];
const ALIGN_VALUES = [ 'left', 'right', 'center' ];
const BUTTON_NODE_DEFAULT_OPTIONS = {

// used by ComboBox and ButtonNode
listPosition: 'below', // {string} where the list is positioned relative to the button, see LIST_POSITION_VALUES
align: 'left', // {string} alignment of items on the button and in the list, see ALIGN_VALUES

// used exclusively by ButtonNode
buttonFill: 'white',
buttonStroke: 'black',
buttonLineWidth: 1,
buttonCornerRadius: 8,
buttonXMargin: 10,
buttonYMargin: 4,

// a11y - used exclusively by ButtonNode
a11yButtonLabel: '' // {string} accessible label for the button that opens this combobox
};
const BUTTON_NODE_KEYS = _.keys( BUTTON_NODE_DEFAULT_OPTIONS );

/**
* @param {*[]} items - must be created using ComboBox.createItem
Expand Down Expand Up @@ -90,7 +70,7 @@ define( require => {
phetioType: ComboBoxIO,
phetioEventType: 'user'

}, BUTTON_NODE_DEFAULT_OPTIONS, options );
}, ComboBoxButtonNode.DEFAULT_OPTIONS, options );

// validate option values
assert && assert( options.disabledOpacity > 0 && options.disabledOpacity < 1,
Expand Down Expand Up @@ -296,7 +276,8 @@ define( require => {
} );

// @private button, will be set to correct value when property observer is registered
this.buttonNode = new ButtonNode( this.getItemNode( property.value ), _.pick( options, BUTTON_NODE_KEYS ) );
this.buttonNode = new ComboBoxButtonNode( this.getItemNode( property.value ),
_.pick( options, ComboBoxButtonNode.DEFAULT_KEYS ) );
this.addChild( this.buttonNode );

// a11y - the list is labeled by the button's label
Expand Down Expand Up @@ -551,153 +532,5 @@ define( require => {
return new ComboBoxItem( node, value, options );
};

/**
* The button that shows the current selection. Clicking on it shows the list of items.
*/
class ButtonNode extends Node {

/**
* @param {Node} itemNode
* @param {Object} [options]
*/
constructor( itemNode, options ) {

options = _.extend( {

// phet-io
tandem: Tandem.optional, // ButtonNode is not currently instrumented

// a11y
tagName: 'button',
labelTagName: 'span',
containerTagName: 'div'

}, BUTTON_NODE_DEFAULT_OPTIONS, options );

super();

// @private
this.buttonXMargin = options.buttonXMargin;
this.buttonYMargin = options.buttonYMargin;

//TODO #314 missing visibility annotation
this.labelContent = options.a11yButtonLabel;

//TODO #314 missing visibility annotation
// the button is labelledby its own label, and then (second) by itself. Order matters!
assert && assert( !options.ariaLabelledbyAssociations, 'ButtonNode sets ariaLabelledbyAssociations' );
this.ariaLabelledbyAssociations = [
{
otherNode: this,
otherElementName: AccessiblePeer.LABEL_SIBLING,
thisElementName: AccessiblePeer.PRIMARY_SIBLING
},
{
otherNode: this,
otherElementName: AccessiblePeer.PRIMARY_SIBLING,
thisElementName: AccessiblePeer.PRIMARY_SIBLING
}
];

// signify to AT that this button opens a menu
this.setAccessibleAttribute( 'aria-haspopup', 'listbox' );

//TODO #314 this should be private, and its value should be passed set via options or a setter methods
// @public - if assigned, it will be removed on disposal.
this.a11yListener = null;

// up or down arrow
const arrow = new Path( null, {
fill: 'black'
} );
const arrowWidth = 0.5 * itemNode.height;
const arrowHeight = arrowWidth * Math.sqrt( 3 ) / 2; // height of equilateral triangle
if ( options.listPosition === 'above' ) {
arrow.shape = new Shape().moveTo( 0, arrowHeight ).lineTo( arrowWidth / 2, 0 ).lineTo( arrowWidth, arrowHeight ).close(); // up arrow
}
else {
arrow.shape = new Shape().moveTo( 0, 0 ).lineTo( arrowWidth, 0 ).lineTo( arrowWidth / 2, arrowHeight ).close(); // down arrow
}

// button background
const backgroundWidth = itemNode.width + ( 4 * this.buttonXMargin ) + arrow.width;
const backgroundHeight = itemNode.height + ( 2 * this.buttonYMargin );

// @private
this.background = new Rectangle( 0, 0, backgroundWidth, backgroundHeight, {
cornerRadius: options.buttonCornerRadius,
fill: options.buttonFill,
stroke: options.buttonStroke,
lineWidth: options.buttonLineWidth
} );

// vertical separator to left of arrow
const separator = new Line( 0, 0, 0, backgroundHeight, {
stroke: 'black',
lineWidth: options.buttonLineWidth
} );

// @private parent for the selected ComboBoxItemNode
this.itemNodeParent = new Node();

// rendering order
this.addChild( this.background );
this.addChild( arrow );
this.addChild( separator );
this.addChild( this.itemNodeParent );

this.setItemNode( itemNode );

// layout
separator.left = this.itemNodeParent.right + this.buttonXMargin;
separator.top = this.background.top;
arrow.left = separator.right + this.buttonXMargin;
arrow.centerY = this.background.centerY;

// @private
this.disposeButtonNode = () => {
this.a11yListener && this.removeInputListener( this.a11yListener );
};

this.mutate( options );
}

/**
* Sets the item that is displayed on the button.
* @param {ComboBoxItemNode} itemNode
*/
setItemNode( itemNode ){

// Remove the previous itemNode
assert && assert( this.itemNodeParent.getChildrenCount() <= 1,
'itemNodeParent should never have more than 1 child' );
this.itemNodeParent.removeAllChildren();

// Add the new itemNode and adjust layout. Do not transform itemNode, because it's shared with the list.
this.itemNodeParent.addChild( itemNode );
this.itemNodeParent.left = this.background.left + this.buttonXMargin;
this.itemNodeParent.top = this.background.top + this.buttonYMargin;

// TODO sun#314 is there a better way to do this?
itemNode.a11yShowItem( false );

// Only set if defined, since it is an option, see ComboBox.createItem
if ( itemNode.a11yLabel ) {
this.innerContent = itemNode.a11yLabel;
}
}

/**
* @public
* @override
*/
dispose() {
this.disposeButtonNode();
super.dispose();
}
}

sun.register( 'ComboBox.ButtonNode', ButtonNode );

return ComboBox;
} );
Loading

0 comments on commit 920d63a

Please sign in to comment.