Page MenuHomePhabricator

Add Button component to Codex
Closed, ResolvedPublic

Description

Existing components

MediaWiki community:

External libraries:

Wikimedia Design Style Guide links:

Implementation decisions

Primary style
  • WVUI: Primary is one of three possible values for the type param (the others are "normal" and "quiet"). These types are captured in a ButtonType enum.
  • Wikit: styles primary buttons exactly as prescribed in the design style guide, but it ignores type="progressive" or type="destructive" if the button doesn't also have variant="primary" or variant="quiet"
  • MediaSearch: allows progressive/destructive buttons to be primary (white text on blue/red background) or non-primary (blue/red text on white background).
  • CX: primary by default, but it supports the non-primary style through type="text"
  • Wikidata Bridge: button is always primary when it is progressive.
Setting the action type and quietness
  • WVUI: the button takes an action prop that can be set to default, progressive or destructive, and defines a TypeScript enum for this. As mentioned above, quiet is one of the three ButtonTypes that can be set via the type prop.
  • Wikit: the button takes a type prop that can be set to neutral, progressive or destructive, and a variant prop that can be set to normal, primary or quiet. Even though Wikit is written in TypeScript, it doesn't use TypeScript types for these, they just take a string and use a validator.
  • MediaSearch: similar to CX, the button takes progressive, destructive, and primary as boolean props, as well as a boolean frameless prop (which appears to be the same concept as "quiet").
  • CX: the button takes progressive and destructive as boolean props; it doesn't have quiet buttons as such, but buttons with type="text" or type="icon" look similar (there is also an outlined prop but I'm not sure what it does).
  • Wikidata Bridge: the button takes type="primaryProgressive", and does not support destructive (or non-primary progressive); it supports something similar to quiet buttons with type="link" (and type="close" and type="back"` are icon-only quiet buttons).
Other display modes
  • Wikit: the button takes an icon-only prop.
  • CX: the button takes type="icon" to make a button quiet and icon-only, type="text" to make it quiet and text-only, type="toggle" (which doesn't seem to do anything); it also takes a boolean prop large to make it bigger, and boolean outlined and depressed props whose behavior is unclear to me.
  • Wikidata Bridge: the button takes a size prop (set to M, L or XL) and a squary prop (unclear what that does).
Which HTML tag to use / link support
  • WVUI, Wikit, and MediaSearch: always use <button>
  • CX: uses an <a> if the href prop is set, and a <span> otherwise
  • Wikidata Bridge: always uses an <a> with a <span> inside it, and takes an href prop and and props to make the link open in a new tab (opens-in-new-tab) or prevent the href from being followed (prevent-default).
Setting the disabled state
  • WVUI and Wikit: these implementations don't have a disabled prop, so setting them to disabled sets the native attribute on the <button>, which they then style through the :disabled selector.
  • MediaSearch: has a disabled prop, but it's superfluous since it's only used to set the native attribute.
  • CX and Wikidata Bridge: these implementations have a disabled prop, and set a CSS class on the button when it's disabled, in order to style it differently.
How content is injected
  • WVUI: allows any content to be injected through a slot, and does not have explicit support for icons; a button with an icon can be made by manually passing in an icon as part of the slot content.
  • Wikit: works the same way, but it also takes an icon-only prop that styles the button differently.
  • MediaSearch: accepts arbitrary content via a slot, but also takes an icon prop that adds the specified icon before the slot content.
  • CX: renders an icon, a plain text label and an indicator (smaller secondary icon) by default, but allows arbitrary content to be injected through a slot, which overrides all of that.
  • Wikidata Bridge: only accepts text, through the message prop; it does not support arbitrary content, and support for icons is limited (type="back" and type="close" make the button icon-only with a pre-set icon).
How the icon color is controlled
  • WVUI: button doesn't manage icons explicitly, so color-coordinating the icons is left up to the caller
  • WiKit, MediaSearch, and CX: icon component uses inline SVG with fill="currentColor", so setting the text color (to blue for progressive, red for destructive, or white for primary) also changes the icon color.
  • Wikidata Bridge: doesn't support changing the color of the icon.
Invisible label text
  • WVUI and Wikit: don't put in their own icons or labels, so this feature isn't relevant for them.
  • MediaSearch: takes an invisibleLabel prop that causes the label to be rendered but hidden (this is desirable for icon-only buttons, for accessibility reasons).
  • CX: doesn't have this feature, the label isn't rendered at all for icon-only buttons.
  • Wikidata Bridge: does this automatically when type="back" or type="close" are used (both result in an icon-only button).

Event Timeline

Thanks for compiling all of this, @Catrope! Some initial thoughts:

  • Naming things: In WBMI, we've been following these guidelines:
    • Try to carry over OOUI/design system language as much as possible to decrease the cognitive load on developers switching to the new library, unless we see a clear opportunity to simplify or clarify something.
    • Try to use the most accurately descriptive word(s) possible, so that their meaning can be understood with minimal additional explanation or context.
    • For boolean props, default to false, and opt for adjectives ("frameless") over negative descriptors ("noFrame") (kudos to @egardner for preventing me from introducing double negatives into the code several times with this one, lol).
  • In the case of quiet/text/framed/frameless, we chose "frameless" because we found it the most descriptive (over "quiet," which only becomes understandable after you see a quiet button), and it's similar to the framed property of the OOUI Button Widget. However, we went from "framed" to "frameless" because "framed" is the default state, and we want our boolean props to default to false.
  • Icons: In other libraries, I've been seeing the pattern of preferring child components in named slots over props (see Vuetify's Input component as an example).
  • HTML element: If we want to stick to the paradigm that a button does something on the page when clicked, we should only use a <button> element. However, use cases for links styled like buttons do come up (we have one in MediaSearch). We could handle this with a prop that changes the element in the template, or we could create a separate component called LinkButton or something.
  • disabled: I like the simplicity of setting the disabled attribute on the component and using an attribute selector to handle styles.

We should also think about how to handle toggle buttons and toggle switches. I don't think we have Vue implementations of these yet, but OOUI has them. Toggle buttons look exactly like normal buttons, except that they stay pressed after you click them (and become unpressed again when you click them again). We could consider adding a prop to the regular button component that enables this mode. Toggle switches are a bit more akin to checkboxes (see T279714), and I think it would make sense for them to be a separate component.

Change 681188 had a related patch set uploaded (by Catrope; author: Catrope):

[wvui@master] Fix appearance of progressive/destructive buttons

https://gerrit.wikimedia.org/r/681188

Change 681188 had a related patch set uploaded (by Catrope; author: Catrope):

[wvui@master] Fix appearance of progressive/destructive buttons

https://gerrit.wikimedia.org/r/681188

This addresses #1 (lack of primary style in WVUI)

Change 681188 abandoned by Catrope:

[wvui@master] [button] Fix appearance of progressive/destructive buttons

Reason:

https://gerrit.wikimedia.org/r/681188

Catrope removed Catrope as the assignee of this task.

We should also think about how to handle toggle buttons and toggle switches. I don't think we have Vue implementations of these yet, but OOUI has them. Toggle buttons look exactly like normal buttons, except that they stay pressed after you click them (and become unpressed again when you click them again). We could consider adding a prop to the regular button component that enables this mode. Toggle switches are a bit more akin to checkboxes (see T279714), and I think it would make sense for them to be a separate component.

I've been thinking about this some more and it raises a major question that we should answer early on in this process: when do we make a component more complex, and when do we create a new component?

I generally tend to support additional, separate components for the following reasons:

  • It keeps components simpler, which benefits library authors (easier to write code iteratively and maintain code) and users (fewer props/etc. to keep track of per component)
  • It makes components' purposes clearer to the library user

Of course, there are lots of shades of gray here. It'd be excessive to have separate components for PrimaryButton, ProgressiveButton, and DestructiveButton. Those things are pretty clearly different states of the Button component. ToggleButton is much more open for interpretation: is it a button that has a different type or state, noted via a prop? Is it essentially a button (or a checkbox?), but a separate component called ToggleButton?

Maybe the criteria look something like this:

  1. At the most basic level, what is the component? What is its main purpose? What should the markup be, in order to best represent what this component actually is?
  2. Is this a simple variation that results in a minor change, like a CSS class/visual style, HTML attribute, icon, whether or not to show something, etc.? Or does it have a larger impact (different HTML element altogether, significant visual changes, new data attribute or computed property)?

For a toggle button, I'd say:

  1. It is most essentially a button, although it has on/off behavior like a checkbox. <button> is the most semantically correct element, in my opinion
  2. The purpose of a toggle button, the need to keep track of its on/off state, and the major style variations are impactful enough to necessitate a separate component

For a toggle switch:

  1. It is essentially a checkbox, since there is no HTML element that better represents its purpose or style semantically
  2. It is different enough from a checkbox in style and purpose that it should be a separate component

I'm interested to hear your and others' thoughts!

Also, from a code organization standpoint, we could lean on the composition API to contain reusable props, computed properties, methods, etc. I need to make sure that props pulled in from a composable display as expected in Storybook, to make sure separating code doesn't make it harder to read components, but I'm hoping this won't be an issue.

I've been thinking about this some more and it raises a major question that we should answer early on in this process: when do we make a component more complex, and when do we create a new component?

I generally tend to support additional, separate components for the following reasons:

  • It keeps components simpler, which benefits library authors (easier to write code iteratively and maintain code) and users (fewer props/etc. to keep track of per component)
  • It makes components' purposes clearer to the library user

I agree with everything @AnneT says here about erring on the side of smaller components. I want to add one additional consideration here:

Vue provides some ways to share code that concerns the "logic" part of the components: Mixins, Composition API, etc. Markup is not covered by this however. So I'd say that needing to present significantly different markup should generally be grounds for breaking out a new component file, even if the underlying behavior is pretty much the same. In that case, different components with different templates can include a common mixin. I think this is preferable to over-burdening the template with a ton of hard-to-follow conditional statements, etc.

To put this another way, Vue gives us fewer tools to manage complex templates than it does to manage complex component logic. So keeping templates clean and legible should be a priority when deciding when to create new components versus extending existing ones.

AnneT renamed this task from Unify button implementations to Add Button component to Codex.Oct 12 2021, 2:35 PM
AnneT edited projects, added Codex; removed WVUI.
AnneT updated the task description. (Show Details)

We have ported over WVUI's Button implementation as a test component, but we probably should revisit it soon and review the different approaches described here to see if anything should change.

Marking as resolved; please add future tasks related to the Button component to the Triage column on the Design Systems Team board