openui / open-ui

Maintain an open standard for UI and promote its adherence and adoption.
https://open-ui.org
Other
3.39k stars 183 forks source link

Toggle Button Proposal #1058

Open lukewarlow opened 1 month ago

lukewarlow commented 1 month ago

Toggle Buttons

Firstly I'd like to acknowledge that the term "toggle button" is incredibly overloaded, so the TLDR is a native "aria-pressed" toggle button.

Problem

It's common in UI design to have buttons which toggle a binary state. These are implemented in varying ways across the web, some involve JS (aria-pressed, or changing the button label), some are semantically questionable (checkbox, switch), and some are both (styled divs).

In aria there's the concept of a pressed state that buttons can have. https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-pressed this requires JS to toggle on and off currently.

Use cases

See https://github.com/openui/open-ui/issues/1039

Basically any button where a single state (which should be reflected by a fixed label) is being toggled. e.g. a mute button.

Proposed Solution

New "toggle" value for the button element's type attribute.

Functionality this would provide atop the "button" value:

New :pressed pseudo class

This would apply to toggle buttons which are in the pressed state. This would be used by the UA stylesheet to provide the aforementioned styling indicator.

When in the mixed state this element would match :indeterminate and not pressed.

New defaultpressed DOM attribute

This would be an enum attribute and would allow the default to be aria-pressed="true" or aria-pressed="mixed" rather than "falss".

It would be reflected to a new defaultPressed IDL attribute.

Ergonomically it might be good if the missing value default was "true"?

New pressed IDL property

This would be a read/write IDL property, with no corresponding DOM attribute. It would get/set the internal pressed state of the button. Accepted values would be "true", "false", "mixed".

We could alternatively make pressed be boolean and use a separate indeterminate property (like checkbox) but I've heard this might not be desirable DX.

Alternatives

Reuse checkbox

HTML already has a sort of state toggle, the checkbox input. For the switch component, the HTML proposal (And recently shipped implementation in Safari) uses a new attribute on the checkbox input. Why don't we follow the same path here?

  1. A switch and a checkbox are generally intended to be form participants, toggle buttons are NOT (as such my proposal introduces no form value concept for toggle buttons)

  2. Toggle buttons are also buttons for this reason it doesn't make sense to use input as the base element.

  3. (opinion) The switch should probably have been a new input type, a button type/attribute addition or a new element altogether. The selling point is that it degrades gracefully to a checkbox but a checkbox and switch have different semantics (delayed action vs immediate action)

New toggle attribute on <button type="button">

Why am I proposing a new type rather than a new attribute for buttons?

One downside to a new button type is that invalid button types resolve to submit not button, this means in older browsers these toggle buttons could erroneously submit forms.

However, as previously stated toggle buttons aren't form participants and so might not be used inside of a form element as much. They also (currently) still require some JS to do an action and so you can prevent default the submission in older browsers.

Aside from the potential form submission in unsupporting browsers the new type feels like an ergonomic win compared to a new attribute.

If we take a default pressed button as an example:

<button type="toggle" defaultpressed="true"> vs <button type="button" toggle defaultpressed>

Using a new type also means that we can't try and turn reset or submit or future button into a toggle button accidentally.

New pressed enum attribute?

This is the strongest alternative imo. It would alleviate the requirement to have a defaultpressed attribute and IDL property.

<button type="button" pressed="true"> where pressed takes "true", "false", and "mixed", this would reflect the IDL property (and change when the button is pressed too).

While it's potentially clearer (no use of the term toggle), it does rely on state reflection, which is undesirable imo, it also doesn't match most other elements on the web (aside from dialog and details).

Questions

Should :pressed also apply if an explicit aria-pressed attribute is applied to elements with a role button?

Should this fire an event? If so, which?

How should defaultpressed attribute work if you change it once parsed? Should pressed update to match until the user clicks the button? (this is I believe how checkbox works)

Should defaultpressed instead be called initiallypressed? This might depend on the previous question. default is more consistent?

How does this work with invokers?

What should the default styling be?

What should the IDL attributes do for non-toggle buttons?

If the button is mixed what should clicking it do

Old Version # Toggle Buttons Firstly I'd like to acknowledge that the term "toggle button" is incredibly overloaded, so the TLDR is a native "aria-pressed" toggle button. ## Problem It's common in UI design to have buttons which toggle a binary state. These are implemented in varying ways across the web, some involve JS (aria-pressed, or changing the button label), some are semantically questionable (checkbox, switch), and some are both (styled divs). In aria there's the concept of a pressed state that buttons can have. https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-pressed this requires JS to toggle on and off currently. ## Use cases See https://github.com/openui/open-ui/issues/1039 Basically any button where a single state (which should be reflected by a fixed label) is being toggled. e.g. a mute button. ## Proposed Solution ### New `"toggle"` value for the button element's type attribute. Functionality this would provide atop the "button" value: - Implicit pressed state, defaulting to false. This corresponds to aria-pressed attribute. - Pressing the button would toggle the pressed state (e.g. false -> true for first click) - The buttons default styling would include an indicator of this pressed state. Note: I'm intentionally ignoring the "mixed" value of the aria-pressed attribute as I'm not sure what actual utility is provides and how much it's used. ### New `:pressed` pseudo class This would apply to toggle buttons which are in the pressed state. This would be used by the UA stylesheet to provide the aforementioned styling indicator. ### New `defaultpressed` DOM attribute This would be a boolean attribute and would change the default to aria-pressed="true" rather than false. It would be reflected to a new defaultPressed boolean IDL attribute. ### New `pressed` IDL property This would be a read/write boolean IDL property, with no corresponding DOM attribute. It would get/set the internal pressed state of the button. ## Alternatives ### Reuse checkbox HTML already has a sort of state toggle, the checkbox input. For the switch component, the HTML proposal (And recently shipped implementation in Safari) uses a new attribute on the checkbox input. Why don't we follow the same path here? 1. A switch and a checkbox are generally intended to be form participants, toggle buttons are *NOT* (as such my proposal introduces no form value concept for toggle buttons) 2. Toggle buttons are also *buttons* for this reason it doesn't make sense to use input as the base element. ### New toggle attribute on `
lukewarlow commented 1 month ago

See https://demo.lukewarlow.dev/toggle-button.html for a basic demo on what this could look like. The demo uses <toggle-button> instead of <button type="toggle"> for ease of implementation.

Edit: This is out of sync with the new proposal

muan commented 1 month ago

Would this potentially interact with invoker? like setting defaultpressed on the button given the invoke target is muted by default?

<button type="toggle" invoketarget="video-el" invokeaction="mute">mute</button>
<video id="video-el" muted></video>
lukewarlow commented 1 month ago

Great question, yes the intention is that this can work with invokers. We need to work out the specifics. Like should invoking a popover work with a toggle button? My gut is that this maybe shouldn't work because else you'd have two states fighting each other (expanded vs pressed). On the flip side there's some actions that maybe should require a toggle button, like video mute?

Perhaps invokers could handle the pressed state separately and not require type toggle at all. All great questions that I hope to work through a bit during today's OpenUI meeting, and further ongoing discussions.

mfreed7 commented 1 month ago

See also: https://github.com/openui/open-ui/issues/700

css-meeting-bot commented 1 month ago

The Open UI Community Group just discussed Toggle Button Proposal.

The full IRC log of that discussion <hdv> Luke: this is something that has come up previously… there is a lot of ways to toggle state on the web and the term toggle is incredibly overloaded… there's checkboxes, switches (like checkboxes but not quite), and then there's buttons, including the aria-pressed variery that requires JS, and changing accessible name
<hdv> Luke: some of these are semantically questionable for specific use cases
<hdv> Luke: I feel adding a toggle button to the platform would be a quick win
<hdv> Luke: the proposal is fairly simple
<hdv> Luke: it's a new button type, which would be a non submit button with the added semnatics of a toggle button, when you click it its toggle state changes to pressed or non pressed, label doesn't change
<hdv> q+
<hdv> Luke: and then there's a pseudo to do styling
<masonf> q+
<gregwhitworth> ack hdv
<masonf> q+ naman
<gregwhitworth> hdv: one quick question, is there a way to set the toggle state on page load
<gregwhitworth> hdv: does that not make any sense
<gregwhitworth> Luke: yes there is a declarative way which is defaultpressed
<gregwhitworth> Luke: for checkbox there is checked attribute, but I think that's dumb API design :)
<gregwhitworth> Luke: I think it also helps that React and other frameworks have issues with attr and props aligning
<gregwhitworth> Luke: you would be able to say the attr is pressed by default
<gregwhitworth> hdv: I like it
<gregwhitworth> ack masonf
<hdv> q+ muan
<hdv> masonf: question on naming… can we use initially instead of default, that should be a lot clearer… we did the same for popover
<keithamus> q- muan
<keithamus> q+
<hdv> masonf: another question… can you contrast this with checkbox switch?
<hdv> Luke: diffference with checkbox switch is that that is a form participant… this new button type would explicitly not be a form participant
<hdv> masonf: should input type switc maybe not be a form participant? I think that's aship not sailed because it hasn't been specced
<hdv> Luke: semantically I don't think so, they are different things in ARIA
<hdv> masonf: that could be changed based on their being a switch attribute
<hdv> Luke: it changes the role to be a switch role with aria checked state
<hdv> Luke: whereas for toggle buttons it's ariapressed
<hdv> keithamus: the difference between them is that a switch is like a preference, screenreader would read it as whatever the name is, dark mode, or toggle, or 'on'
<hdv> keithamus: a button that is a toggle is not so much a preference… its use cases include things like pause and play, or bold / italic buttons
<hdv> keithamus: so a switch reads whether it is checked, and a button would read that it is active, which is a subtle difference
<hdv> masonf: these things seem presentational… I have a light switch in my house, that I can flip, but I could install a button that does the same… is there a semantic different?
<gregwhitworth> +1 to mason freed
<hdv> masonf: I don't know how to choose between them
<hdv> q+
<gregwhitworth> ack naman
<hdv> naman: mostly wanted to say the same thing like Mason… what I like about the switch is that it is backwards compatible
<hdv> nmn: would love this kind of thing to work for more inputs
<hdv> nmn: we already made switches with an attrbute, cant we make toggles with an attribute
<gregwhitworth> q+
<hdv> Luke: we could a change based on attribute… but if it's on a button things would be more verbose
<hdv> nmn: I meant on a checkbox, not button
<hdv> Luke: ultimately it is not a checkbox, it is a button
<hdv> masonf: we're all talking about boolean values?
<hdv> Luke: checkbox can be indeterminate
<hdv> Luke: issue is in the details
<hdv> gregwhitworth: maybe it shouldn't have been added to checkbox?
<hdv> masonf: my question is should there be two things?
<hdv> Luke: they're different concepts
<hdv> Luke: switch wasn't necessary because there is a checkbox
<hdv> masonf: because it doesn't have indeterminate state and is not a button?
<hdv> Luke: yes
<masonf> q?
<hdv> Luke: checkboxes are sometihng you're submitting somewhere, like 'i accept terms' or 'i want to receive marketing email'
<hdv> Luke: this button is something like, you're on social media and press a like button and it can be pressed or not
<hdv> Luke: the alternative way to do it is to apply styling to a class and update the label, which would let teh screenreader know something has changed
<flackr> q+
<gregwhitworth> ack dbaron
<hdv> Luke: but aria-pressed with a static label is what you should really be doing
<hdv> dbaron: slightly different topic… it looks like this is a proposal to add things to button but not to input type=button, not sure if there's precedent, everything that works on one should work on the other
<hdv> masonf: except arbitrary HTML inside button
<hdv> dbaron: true
<hdv> Luke: this could be a new input type
<hdv> Luke: my understanding is that input type button is more of the legacy way of doing it
<hdv> dbaron: don't have a strong opinion on it but probably worth checking on it
<gregwhitworth> ack keithamus
<gregwhitworth> q+ naman
<jamesnw> q+
<hdv> Luke: re 'is this related to invokers', no, not directly, this is a standalone thing
<keithamus> q+
<hdv> Luke: eg an invoker not near a popover changes aria expanded state
<hdv> Luke: one way could be that popover invokers don't work on toggle buttons
<hdv> Luke: there could be future invoker action that work for these
<gregwhitworth> ack hdv
<gregwhitworth> hdv: I agree that checkboxes and new button proposal seem similar. Since they have different mappings in the aria spec to have different solutions for it
<gregwhitworth> hdv: because they exist as different things in ARIA they probably behave in ATs as well and so they both do exist
<gregwhitworth> ack keithamus
<hdv> keithamus: re about invokers… this is related for things like mute… a lot of actions we have described for future invoker stuff would rely on mutating the pressed state or the accessible name
<hdv> keithamus: the pressed state is something that is quite easy for us to do on the browser side of things, we can map that in C
<hdv> keithamus: but for a web developer there is no easy way that this thing is a toggle button that also has this invoke command and have those two aligned. And I think this adds that. It would make that type of button simpler to implement if you're doing custom behaviours
<masonf> q+ muan
<keithamus> q+ muan
<hdv> gregwhitworth: my point is… I still have no when to use this… I don't know when to eeach for this stuff
<hdv> s/eeach/reach
<hdv> gregwhitworth: outside of aesthetics I can't really see the difference
<hdv> keithamus: the difference is down to the accessible name: if you have a button that has an icon, the name would be the icon… but to convey to assistive tech, to know whether it's selected or not, while there's no name change, ariapressed conveys that
<hdv> gregwhitworth: so I guess I would not add a new control to some form to solve the name computation problem you refer to?
<gregwhitworth> ack gregwhitworth
<gregwhitworth> Zakim, close the queue
<Zakim> ok, gregwhitworth, the speaker queue is closed
<hdv> Luke: think of a shuffle button in Spotify, there's three different states in there, types of shuffles… that something you can't really do with a checkbox, if there's multiple state
<hdv> Luke: if that's where you change the name… that might be better placed for some things
<gregwhitworth> ack flackr
<gregwhitworth> ack naman
<hdv> flackr: as a developer I would rather have a generic way to use an element outside of a form the browser would interpret it doesn't participate in the form
<hdv> nmn2: toggle buttons also make me think of links, they can have an active state
<hdv> nmn2: what we do here is relevant to that to some extent
<gregwhitworth> ack jamesnw
<hdv> jamesnw: I think there's a difference between button and checkbox. In the button, with the click action you're doing something makes it a button… that would be useful inside of form elements as well
<hdv> jamesnw: another comment… tab buttons could use something like this but with an exclusive 'on' if you have one open at the time
<hdv> jamesnw: but I guess that does broaden the scope
<gregwhitworth> ack muan
<hdv> muan: another difference is that a button that is pressed is a constant state, like play buttons in a video. in a video. Re tabs, aria-current isn't a thing in HTML.
<masonf> q+
<hdv> Luke: I'll come up with some concrete answers
<hdv> gregwhitworth: I feel like somewhat positive sentiment in the group
<gregwhitworth> ack masonf
<gregwhitworth> Zakim, open the queue
<Zakim> ok, gregwhitworth, the speaker queue is open
<hdv> masonf: I'd like to point people to this issue in the invokers issue, are people ok with that?
<hdv> masonf: I also posted a link to issue @@@ is that the same issue?
<dbaron> s/@@@/700/
<hdv> Luke: not the same, this is where the term toggle gets very conflated
<dbaron> https://github.com/openui/open-ui/issues/700
<hdv> Luke: there's aria-checked, aria-selected, aria-current, aria-pressed… different concepts that are similar but apply in different situations
<gregwhitworth> q?
<hdv> Luke: eg tabs use aria-selected, if I remember correctly, and that fits in more with tabs because there is multiple that are exclusive… but aria-pressed, my interpretation is you don't really group them, like a like button or pin button, they exist as separate things
<hdv> Luke: we also want tabs obviously but they are different things
<hdv> Luke: if invokers need to change accessibility sttae, they already do for popovers and they can do for other things
lukewarlow commented 1 month ago

One alternative that came up in discussions is a pressed attribute but rather than a Boolean attribute it's enumerated so it's true, false or mixed (if that's needed still need to work that out).

So that solves the how to signify it's a toggle when off problem mentioned above. Would mean reflecting the value back to the attribute which might not be ideal? But also means no need for the defaultpressed attribute?

scottaohara commented 1 month ago

The indeterminate state should be included with this. Though not frequently used, if it is ever needed and not included then people would have to completely roll their own control, since aria states are meant to be overridden by native features.

Eg a native checkbox with aria-checked=mixed won’t actually work since the “checked” state of the input takes priority over the aria attribute

aardrian commented 1 month ago

This is going to recap some of what we discussed on Masto.

In aria there's the concept of a pressed state that buttons can have. https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-pressed

I encourage you to link to the specs since MDN is sometimes wrong: https://w3c.github.io/aria/#aria-pressed

Note: I'm intentionally ignoring the "mixed" value of the aria-pressed attribute as I'm not sure what actual utility is provides and how much it's used.

If you want parity with aria-pressed, then you cannot ignore mixed. Not accounting for it at this step means you are creating a divergent control.

As for not being sure about its utility nor how much it is used, I feel that is necessary to identify before writing this proposal. In short, this proposal is premature without it.

Quick use cases:

New :pressed pseudo class

And :mixed or lean on the existing :indeterminate.

New defaultpressed DOM attribute This would be a boolean attribute and would change the default to aria-pressed="true" rather than false.

Needs to support mixed.

New pressed IDL property This would be a read/write boolean IDL property, with no corresponding DOM attribute. It would get/set the internal pressed state of the button.

Also not boolean because it also needs to supported mixed.

For the switch component, the HTML proposal (And recently shipped implementation in Safari) uses a new attribute on the checkbox input.

Additional notes on switch beyond what you provided:

However, as previously stated toggle buttons aren't form participants and so probably won't be used inside of a form element.

They will. Partly because many authors go for a control based on look, and if this looks like the control they want then they will stuff it in a form (think of cookie consent forms). If it looks the same as a switch but allows a third state, then even more reason I expect it will get used in a form. As such, it will be critical to provide detailed author guidance.

The button needs to hold a default pressed state (else AT wouldn't announce it as a toggle button unless in the pressed state).

This is an oversimplification.

  1. By "AT" I assume you mean screen readers.
  2. Screen readers get their information from the browser.
  3. That information comes from how the browser maps author-defined properties / attributes / values / states to platform accessibility APIs.
  4. Screen readers also pair with Braille displays (so "announcement" is inaccurate).
  5. Voice control users are also impacted by the control type when speaking it.
  6. System colors will also need to be mapped (so the control type matters here too).
  7. Keyboard interaction.
  8. Screen reader quick navigation keys.
  9. Probably more but it's Sunday.

So the button doesn't need to hold a "pressed" state if you are minting a new control. Instead, you will need to work the HTML-AAM spec and ARIA spec to identify how that maps.

In other words, the statement "else AT wouldn't announce" is only true if you explicitly specify it not to. Or you don't collaborate, of course.

If we just had pressed to indicate aria-pressed="true" then this wouldn't be possible.

So far this proposal assumes a binary control (true/false). But since you are trying to replicate ARIA, you are making a trinary control (true/false/mixed). Which means you might want to consider pressed="true|false|mixed" to mimic ARIA.

lukewarlow commented 1 month ago

Based on that discussion I'll write an updated version of the proposal and post it as a comment here and update the original post.

To clarify when I say the pressed Boolean attribute won't work I mean assuming it's just a standard button under the hood exactly because the aam mappings won't exist for it without some new indicator.

I appreciate the feedback this is exactly the conversation I wanted to spark with this issue.

I feel that is necessary to identify before writing this proposal. In short, this proposal is premature without it.

Worth keeping in mind OpenUI is an incubator group so I feel the barrier for a "proposal" here is lower, it's more about starting the conversation to get to a solid end result. Proposal is perhaps a bad word for it though

aardrian commented 1 month ago

Worth keeping in mind OpenUI is an incubator group so I feel the barrier for a "proposal" here is lower […]

My mistake. I am coming at this from a standards world.