public
Last active

YUI 3 Button Proposal

  • Download Gist
gistfile1.md
Markdown

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.Buttoninstances 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
    });
    

Absolutely make ARIA core. Not optional.

I don't understand what Y.Buttons is. And Y.ButtonGenerator seems like it could be a static method on Y.Button. I'd suggest trying to avoid creating too many new properties on Y. Y.Button and Y.ButtonGroup seems like enough.

Do you think it would make sense to have ButtonGroup be an ArrayList implementation?

A push button class should have a configurable click/pressed handler. Were you planning on this being handled by selected (seems awkward)?

So ButtonGroup would bring custom CSS to the page, or is that also in cssbuttons? I'm thinking about the typical button cluster looks.

FYI, the keyboard demo's delete key ignores tabs and deletes the character before the tab ;)

Push, toggle, and radio seem like they hit the 80% use case, but I foresee a request for a multi-state button. Keep that in mind when building the arch, so you don't make it harder to implement that down the line.

So are you imagining all DOM rendering will be via the ButtonGenerator? This is clearly a step to avoid the overhead of Widget (which I understand the reasoning for). There's overlap here with Satyen's WidgetStringRenderer gist (https://gist.github.com/1386836) and my DataTable design gist (https://gist.github.com/1356355) insofar as each of us is trying to make rendering lighter and/or more configurable. Specifically, Satyen is focusing on modifying Widget behavior, where I'm aiming at a similar approach to what you've laid out with a separate DOM/markup rendering mechanism. We need to come to consensus on preferred rendering patterns.

I think extending from Attribute instead of from Base is an interesting choice. Can you explain that? Base also brings plugin support and lifecycle, so you'd have to recreate the wheel if someone wanted to destroy() a Button instance. Extending View instead of Base also might offer some benefits. I wonder if it's possible to formulate the class logic into extensions, allowing implementers to Base.create a Widget button for the additional attributes/configurability of Widget. But still provide a lighter Y.Button class that included the class extension. That would require it to subclass Base, though.

Also, in the aim to reduce instantiation time, perhaps Y.extend is preferable to Base.create, since BuiltClass construction is certainly more complex than basic superclass chaining.

The focus on instantiation and rendering cost is supported largely by the use case of building a UI with a ton of buttons. In these scenarios, those buttons will almost certainly be in groups or clusters (UI region-wise, not co-joined buttons-wise), which suggests UI event handling is best done by delegation. This supports the notion of a ButtonGroup class capable of rendering the DOM of many buttons, but handling all the UX itself. That should be an instance-based solution to the desire to minimize instantiation and Widget's render() overhead. This could work in conjunction with a static DOM/markup factory method or methods. This is also reminiscent of the ModelList flyweight pattern for rendering discussion.

An invasive idea that will likely be shot down on philosophical grounds, but worth raising: augment the button Node with Attribute and add extra attributes directly. I imagine the Button API will be fairly small, so maybe mix it onto the Node instance directly. There are issues in the details here, of course.

"I don't understand what Y.Buttons is."

It's just a way to turn a NodeList into Y.Button instances. This design isn't optimal for ease of use and Y.Buttons functionality will likely be merged into Y.Button in the form of accepting a selection string, Node, or NodeList. Bigger fish to fry before I figure out a few issues with that though.

"And Y.ButtonGenerator seems like it could be a static method on Y.Button."

Sure. Y.ButtonGenerator vs Y.Button.generate is pretty trivial, and if we want to avoid creating new Y properties where possible, this seems like a good move.

"Do you think it would make sense to have ButtonGroup be an ArrayList implementation?"

I'll look into it.

"A push button class should have a configurable click/pressed handler."

Currently you can listen for any events with Button.get('srcNode').on. That certainly isn't ideal though, and improving that is just part of the sugar that I intend to add. My hesitation with baking it in during prototyping was what events do you support, and which ones does the user have to access the source Node to be able to use? There's also an issue with on('click', ...) which it (by nature) fires too slow on touch devices (iPad 1, 200ms-500ms too slow by visual observation). So I think a Button onClick method should take this into consideration. See my mention of using event-touch in the Notes section.

"So ButtonGroup would bring custom CSS to the page, or is that also in cssbuttons?"

I haven't yet decided if cssbuttons.css is included/required with any of the JS modules. Do you include CSS by default, or not? We'll need to investigate this more. I'd lean towards we should if they use('button'), and if they don't want CSS, they use('button-base').

"So are you imagining all DOM rendering will be via the ButtonGenerator?"

It seems like that is the best way to go about it. Y.ButtonGenerator is very basic and really only there as a convenience. Ideally, the user would create the Node themselves and pass it into Y.Button, but it seems like there should be some abstraction for generating a button.

"I think extending from Attribute instead of from Base is an interesting choice. Can you explain that?"

Initially I didn't even extend Attribute, but eventually ran into some issues that Attribute solved for me. I haven't run into any issues yet that Base solves, but I can certainly see that being the case as Y.Button is used with more complicated applications. The Keyboard example, for instance, was the latest one I created, and that really helped me understand how Y.Button could be used in real-world applications. There are still some changes I'd like to make because of things I discovered while hacking together that demo. I'd like to come up with a few more of these types of example applications, and I bet in doing so, I'll see the need for using Base. If you have any ideas, let me know.

" I imagine the Button API will be fairly small, so maybe mix it onto the Node instance directly. "

Mixing new properties directly into Node was my initial approach, and it worked nicely, but like you said, there are likely some philosophical issues with doing that. Also, perhaps some underlying issues that I'm not aware of. Maybe I'll whip up a prototype and send it out to the team to gather thoughts. As I mentioned in the gist, Y.Button is really just a Attribute-based wrapper around Node, so if it makes sense to just add additional properties onto Node (and grouping-related properties onto NodeList), that would be ideal.

I guess Button is fairly unique in that for at least push, toggle, and radio style buttons, it is controlling only one node rather than a collection of them as with most Widgets. And it is not introducing a new control. What unique state is it managing apart from the Node it is wrapping? Or is it just API and change events that are added?

It seems like it would be DWIW ideal that any Node instance that wrapped a buttonable element that was appropriately classed (class="yui3-button") would automatically have a button-like API. You could expand this to all Widgets, of course, that var overlay = Y.one('#foo .yui3-overlay'); -- but now I'm just thinking out loud.

I suppose if you're not going with a Button Widget, then we're already in new territory, considering there is an object that manages UI state. Widget is philosophically the appropriate superclass for and instantiable Button class. The impediment being performance and memory consumption from managing several Buttons. Again, we're in a similar situation as DataTable and TreeView wrt trying to avoid a lot of object creation, except Button has the additional crux of lacking a central object that users would instantiate that would then result in a lot of child objects. I wonder how likely this is in practice--that instead of a different Widget rendering a bunch of child buttons as part of its own rendering process, that an implementer simply renders and wires up a ton of individual buttons. This circles back to my point about a ButtonGroup class as a strategy to avoid instance explosion.

button.get('srcNode').on('click', fn) is awful. Class instances encapsulate core functionality and click subscription is core functionality. This must be handled by the instance. There is precedent with Widget's UI events (I think Overlay supports some?), but I'm not too keen on the implementation, as subscribing to widget.on('click', fn) is subscribing to a custom event, not a DOM event, so e.preventDefault() doesn't DWIW. Whatever direction you go, make sure to reconcile this with the existing system.

I agree that the delay for click events on iOS is an issue, but I think Button is the wrong place to solve it. It affects every widget, and indeed everything in the page, so it should be solved closer to the metal, similar to how DD works for Slider by using the appropriate events as decided by a conditional module.

@derek I think the goals are right on target, and I really like how the examples are looking.


My gut reaction to seeing the list of Y.Button attributes was that Y.Buttons do not need to exist as instance objects; Y.Node instances seem like they will represent buttons just fine, and almost everything is covered by standard DOM attributes. I think a specific button's state can be driven by the combination of CSS classes and using Y.Node's getData/setData support.

So what I see is something more like a Y.button object with static utility methods. Y.button.create() can generate a new Y.Node instance for the button, or take an existing Y.Node instance and modify it by adding the ARIA attributes and updating whatever else was passed into the config.

I could also see more utility methods for things like:

  • Y.button.select()
  • Y.button.unselect()
  • Y.button.disable()
  • Y.button.enable()

These methods could have some sugar and also accept Y.NodeLists. They could be implemented by either toggling CSS classes on the node instance, or using getData/setData for things which aren't well expressed as CSS classes. This way the API is nicer than having people remember which CSS classes to add and remove, etc.

@derek and @lsmith I found your discussion about adding things to Y.Node.prototype interesting. Y.Transition does this; and 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. This would be similar to how Y.DOM's static methods are added to Y.Node.prototype via Y.Node.importMethod();

What about something like this?

YUI().use('button', function (Y) {
    var myButton = Y.button.create({text: 'Foo Button'});

    myButton.btn.disable();
    // or...
    Y.button.disable(myButton);
});

I think if people want more fancy with their buttons they will probably use Y.ButtonGroup, I think this should be a widget that interacts with the button Y.Node instances as I described above, and be the things that maintains the shared state like which button in the group is selected.


The touch even support stuff is a really interesting problem-space that would be great to address. @davglass has been working on some Loader stuff that might make it easier to do some conditional loading of the event-touch modules; but that's the extend of what I know about Loader's conditional module support :)


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

@ericf Even namespaces are uppercase, so it would be Y.Button, though this would admittedly be misleading (but in the same way that Y.DataTable currently is misleading).

You can't put namespaced method on a prototype, because 'this' is lost. Alternately, putting button methods directly on the Node prototype is API pollution for 95%+ of Nodes created/referenced on the page. Also, select is a native DOM method on input elements, so we can't muck with that.

@derek @ericf The more I think about it, the more I like ButtonGroup as an instantiable class, but I am having a hard time justifying a class for individual buttons. I like the idea of a static factory method to produce Nodes with classes and ARIA roles etc. Though perhaps confusing, in that it fails instanceof, an option here would be to have new Y.Button(...) double as the factory and return a Node instance. Another issue with this would be that it wouldn't host a render() method for creating buttons off DOM, then attaching them in the usual way for widget classes (even though this wouldn't be a Widget class).

You can't put namespaced method on a prototype, because 'this' is lost. Alternately, putting button methods directly on the Node prototype is API pollution for 95%+ of Nodes created/referenced on the page. Also, select is a native DOM method on input elements, so we can't muck with that.

Good point! :)

Would it make sense for the button module override the select() method and add a setter for the disabled attribute on Y.Node and would perform an is-a-button check by calling this.hasClass('yui3-button') and if so, also add/remove yui3-button-selected and yui3-button-disabled CSS classes?

YUI().use('button', function (Y) {
    // => `<button class="yui3-button">Foo Button</button>
    var myButton = Y.Button.create({text: 'Foo Button'});

    // Adds `disabled` attrbiute to the button node, and adds `yui3-button-disabled` CSS class.
    // => `<button class="yui3-button yui3-button-disabled" disabled>Foo Button</button>
    myButton.set('disabled', true);
});

Practically speaking, I think those changes would be ok, not likely to impact performance in any measurable way because the extra logic would only be a cost when implementers touched those attributes on other Nodes, which there shouldn't be hundreds of. I don't think we should override select, though, since it means something different and doesn't have an unselect counterpart anyway. It would be a better API to have those methods, IMO, but it seems unwise.

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.

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.

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?"

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?

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.

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

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.

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

@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.

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

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

@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.

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.

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>

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/

@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

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.