Skip to content

Instantly share code, notes, and snippets.

@derek
Created November 23, 2011 18:09
Show Gist options
  • Save derek/1389403 to your computer and use it in GitHub Desktop.
Save derek/1389403 to your computer and use it in GitHub Desktop.
YUI 3 Button Proposal

YUI Button

Goal

To introduce a new Button component for YUI 3 that addresses the following user stories

  • "I want buttons on my site to look consistent & attractive."
  • "I want to be able to programmatically control buttons in my application."
  • "I want to my buttons to be intelligent and interact with one another in groups."
  • "I want my application to be able to dynamically generate buttons."

Requirements

  • ARIA / Accessibility
  • Modern styles (using CSS3), but degrades well to legacy browsers
  • Customizable colors with minimal fuss
  • Something lighter than Y.Widget, but similar in features to YUI2 buttons

Modules

  • cssbuttons (CSS) - Some lite CSS skins to give the buttons a nice look & feel. This is ideal for someone who just wants a nice looking button, without needing programatic control.
  • button-base (JS) - A Y.Attribute-driven wrapper around a button-like DOM node, and some helper utilities
  • button-group (JS) - A manager that listens for Y.Button events and can fire notifications when the selection has changed

Design

Buttons can be used in many different ways. Some users just want the buttons to be aesthetically pleasing, others want to listen for clicks/events, others want to programmatically control them, and some will use them for core navigation groups. Because of these variety of use cases, it's important to have functionality logically and modularly separate, while keeping them simple to use & control.

The lightest possible implementation is just including the button stylesheet and adding the yui3-button class to any element you would like to be a button. As requirements start to increase, you can start adding on JS modules to provide the required functionality. The JS portion of Button is very Y.Attribute-driven. The idea is that it that Y.Button is basically a wrapper around a DOM node that fills in missing functionality and keeps the UI in sync with the button state. Y.ButtonGroup is also Y.Attribute driven that knows about groups of Y.Button instances and manages them as a whole.

Module Exports

button-base.js

  • Y.Button - The Y.Attribute-driven Button object
  • Y.Buttons - A way to generate an array of Y.Button instances given a NodeList
  • Y.ButtonGenerator - A way to dynamically generate a Y.Button instance with an unattached DOM node

button-group.js

  • Y.ButtonGroup - A way to connect Y.Button instances together and has a memory of selection states

Y.Button

public methods:

  • onClick
  • getDOMNode

private methods:

  • _colorToHex (static)
  • _getContrastYIQ (static)

attributes

  • type - specifies the type of button (push/toggle)
  • disabled - a setter for the node's 'disabled' attribute
  • selected - a setter that handles the node's 'selected' state
  • backgroundColor - The background color for the button

events:

  • typeChange
  • selectedChange
  • backgroundColorChange
  • disabledChange

CSS classes

  • yui3-button
  • yui3-button:hover
  • yui3-button:active
  • yui3-button-selected
  • yui3-button-focused
  • yui3-button-disabled

Y.ButtonGroup

attributes

  • type - The type of group (default:push/radio/checkbox)
  • buttons - An array of Y.Button instances that are in this group
  • selection - The array of Y.Button instances that are currently selected

ARIA support

  • role=button
  • aria-pressed
  • aria-selected

I haven't come up with a good reason for making ARIA support optional (like YUI 2 Button), so it's just baked in for the time being.

Examples / Demos

You can find some demos here.

To Do / Notes

  • Y.Button - Add sugar & properties to not require users to use .get() & .set() all the time. This will improve usability & performance.

  • Y.Button - Support aria-label/aria-labeledby

  • Y.Button - Support icons & image buttons

  • Y.Button - Determine if the color contrast calculation should belong in Y.Button, or elsewhere

  • Y.Buttons - Combine with Y.Button?

  • Y.ButtonGenerator - Allow an optional container element that the node is appended to?

  • Y.ButtonGroup - Support aria-multiselectable for radio groups

  • Y.ButtonGroup - Possibly support aria-owns if Y.Button instance relationship is not parent-children

  • Y.ButtonGroup - 'selection' is probably inefficient.

  • cssbuttons - Add basic Sam & Night skins

  • Allow using selector strings as opposed to requiring a Node/NodeList to instantiate.

  • Investigate state on legacy browsers

  • Investigate state on tablets

  • Investigate lazy attributes

  • Use the event-touch module to be more responsive on touchscreen devices

      Y.all('.yui3-button').on(['touchstart' /* <- if applicable */, 'click'], function (e) {
          e.halt(); // Stop event propagation
          // do something
      });
    
@derek
Copy link
Author

derek commented Nov 29, 2011

What unique state is it managing apart from the Node it is wrapping? Or is it just API and change events that are added?

That's really it.

button.get('srcNode').on('click', fn) is awful.

Agreed. Adding a convenience method to button for this, and others (select, disable, etc...) will come later.

I think a specific button's state can be driven by the combination of CSS classes and using Y.Node's getData/setData support.

Very likely. Y.Button needs some caching properties to store button state as opposed to querying the DOM every time. If we go the direction of extending Y.Node.prototype, getData/setData could be the best way to do that.

I don't think it would be crazy to add the above Y.button utility functions on to the Y.Node.prototype name-spaced under Y.Node.prototype.button or Y.Node.prototype.btn

As Luke mentioned, that won't work, however you can still namespace it with something like mybutton.button_disable();, essentially add a number of button_foo/btn_foo methods/properties onto Y.Node buttons. Do-able, kinda seems goofy though.

I profiled the code in the generate example with 10 buttons and saw: 19.4ms, 6,866 fn calls which is really high, imo.

I haven't spent much time on perf until we get the overall design figured out, so there is likely lots of room for improvement.

@derek
Copy link
Author

derek commented Nov 29, 2011

The thing I am concerned about, though not very, is setting a precedent of injecting per-element specific logic or adding element specific methods to Node's prototype since that can be a very tempting slippery slope that can be abused to the detriment of the entire system.

That's why I've leaned towards a light wrapper around Y.Node instead of injecting properties into Y.Node.prototype. The downside of this approach is that you have to map up various methods from Y.Button.prototype to Y.Node.prototype for any that you wish to share. I initially started out by doing this with Y.Button.on -> Y.Node.on, but that led to some issues when I integrated Y.Attribute and relied on the on('fooChange', fn) events.

In JS, there's the rule to not extend native prototypes. I guess I almost view things like Y.Node like that. Sure, it is convenient, but you can get yourself into trouble if you aren't careful. It also leads to confusion for developers who might not be so familiar with what exactly is going on. "Why do I see a foobar method on this object? Where did it come from? This is a Y.Node instance, why isn't in the documentation?"

@ericf
Copy link

ericf commented Nov 29, 2011

What unique state is it managing apart from the Node it is wrapping? Or is it just API and change events that are added?

That's really it.

I guess I see this selectedChange event becoming more useful with a Y.ButtonGroup. I can always listen for the click event on an individual button and check its selected state.

@derek what do you think about my comment related to implementing setters for the Y.Node attributes?

@juandopazo
Copy link

Shouldn't Y.Button be a WidgetChild and Y.ButtonGroup a WidgetParent? I know you're trying to avoid parent/child relationships because of performance, but in case of buttons it really makes sense because you don't usually have too many buttons, and specially when you consider a Y.ToggleButton class. In that case, the selected status of the button can interact very easily with the other buttons in the group through the multiple attribute of WidgetParent.

@juandopazo
Copy link

Hmm I guess an Editor can hold easily about 50-60 buttons.
WYSIWYG editor

@derek
Copy link
Author

derek commented Dec 3, 2011

The current implementation I've been working with for Y.Button is similar to what you'd get from Widget, but doesn't have (or need) everything that Widget provides. The 80% use-case for button is to take a element, make it look nice, and listen for an event when pressed. Because of that, it was suggested that I should avoid using Widget for button instances if possible. I haven't come across any real needs for it in Button yet. For groups of buttons, it is typically relying on the Y.Button events and say "Oh, something happened over there, what am I supposed to do?" Which really just consists of de-selecting selected button(s) in the case of a radio group.

@mattparker
Copy link

Sorry if this isn't helpful, but... how does plugin performance compare? Could Button be a Node plugin, and Buttons be a NodeList plugin? The way the discussion's gone they seem to smell a bit, well, plugin-ey.

Matt

@lsmith
Copy link

lsmith commented Dec 6, 2011

@mattparker, NodeLists don't have plugins of their own. NodeList.prototype.plug just calls plug on each of its nodes. That means a new object for each of the Buttons, on top of the Nodes. This, in and of itself, isn't bad necessarily, but plugins add their API and behaviors to the namespace, which seems undesirable (to me at least) for buttons because what defines a button is actually a property or two of the Node itself. Then there's the instantiation step, node.plug(Y.Plugin.Button) which maybe is fine, but it wouldn't be my first thought as an implementer for how to create a button. This is mostly subjective.

@juandopazo
Copy link

Quick question: is the plan to use Y.Button in Y.WidgetButtons?

@derek
Copy link
Author

derek commented Dec 29, 2011

I believe so. @ericf is reviewing that module this sprint.

@ericf
Copy link

ericf commented Jan 5, 2012

@juandopazo Yup, that is the plan! Most of what WidgetButtons currently provides was always intended to be a stop-gap until an official button module was created. I will hopefully have a design-proposal Gist created at the beginning of next week to lay out the initial plans for what WidgetButtons will become for 3.5.0.

@Satyam
Copy link

Satyam commented Jan 13, 2012

After seeing the presentation, it seems to me the same functionality could best be served by two Node plugins in the following way:

Regular push buttons of any kind: no code at all, just use plain old s
Toggle button: a ToggleButtonPlugin for Node which adds the two-state functionality to a regular
ButtonGroup: Only for the radio-type button set, a Node plugin to ensure the exclusivity. For other type of groups, there is nothing to do, really, there is no point in having code for that.

All button types would be supported by the cssbutton style sheet.

This would solve the issue of listening to the click event nicely. Since Y.Button doesn't provide it, you have to go to the

object. Well, if it is a Node plugin then there is nothing but the Node. It also solves the issue of listening to click by using event delegation on the container and finding which button was it. If they are just Node plugins, you get the reference as the target of the event.

Sugar methods could be provided so that all nodes in a given container with a certain className are plugged in with the ToggleButtonPlugin and, likewise, all the containers (usually

s) with a certain className would be plugged with the RadioButtonPlugin.

Aria would be well served as well. Regular buttons would be read as what they are, regular buttons. The ToggleButtonPlugin would add the aria attribute to indicate its state. I don't know if there us anything to be read to the user of a mutually exclusive set of buttons but if there is, the plugin would be able to do it. For any other grouping of buttons, there is really nothing that needs to be read except a title attribute on the container, if needed.

@Satyam
Copy link

Satyam commented Jan 13, 2012

Sorry, in the previous post I put some angled brackets enclosing HTML elements which I assumed this editor would filter out, but it seems it didn't so the text got broken where those HTML tags were found.

In the second paragraph, first line, at the end it said ' plain old <button>s
The second line also ends with a <button>

The fifth paragraph is also broken in two in mid sentence, the offending HTML tag there was <div>

@Satyam
Copy link

Satyam commented Jan 13, 2012

So, I was wrong in referring to this as a pluging, but here is my code:

https://gist.github.com/1605872

The example can be run straight from:

http://www.satyam.com.ar/yui/3.5/button/

@lsmith
Copy link

lsmith commented Feb 3, 2012

@Satyam et al

Here's another POC implementation, but with Y.Button as a subclass of Node:
https://gist.github.com/1726751

Working example here:
http://lsmith.github.com/yui3-gallery/examples/button-node.html

This treats the Button instance as a Node in terms of caching it for future lookups with Y.one(). So Y.one('#btn3') returns a Button instance, which is of course also a Node instance.

I didn't address the attribute issue for lack of time, though I think it would entail overriding the set and get methods to look at Y.Button.ATTRS which would initially be created as Y.Button.ATTRS = Y.Object(Y.Node.ATTRS); to leverage the prototype relationship.

YMMV

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment