opensearch-project / oui

OpenSearch UI Framework
Apache License 2.0
31 stars 65 forks source link

[RFC][FEATURE] SplitButton for an action "with alternatives" #1187

Closed pjfitzgibbons closed 6 months ago

pjfitzgibbons commented 6 months ago

This feature will re-introduce the SplitButton to the OUI widget library.

The SplitButton is expected to have two immediate use-cases :

The original SplitButton was removed at #805, as the original code has been unused by OpenSearch Dashboards and it's known-plugins.

Anatomy

A split button consists of:

  1. A primary action button containing a label and an optional left icon
  2. A secondary action button that expands a dropdown with options to select from that will impact or modify the primary action to be taken.
  3. These buttons are separated by a 1 pixel border. Screenshot 2023-12-15 at 14 49 32

Primary action button width will be determined by the label (and icon and/or combination) consistent with our existing button pattern. Secondary button width should be proportional and use only an icon to denote action of dropdown expansion (in most cases, a down arrow icon but we can allow customizing if necessary.)

We can have split buttons for all our button states and types:

Technical Design

Documentation

Documentation for Split Button should be added to the documentation site : https://oui.opensearch.org/1.x/navigation/split-button See Split Button below.

Widget construction

SplitButton will consist of a PrimaryControl, and a drop-down of SelectionItems.

PrimaryControl

The primary control of SplitButton will be two buttons, arranged side-by-side, and styled to appear as one continuous control, with a hairline separator between.

The left-side button will be the PrimaryButton. This button is the base of SplitButton's props. SplitButton's function and behaviour is a subset of OuiButton.

The right-side button will control the SplitButton's drop-down display. It is styled using the styling directed by SplitButton's props, matching the PrimaryButton. The RightButton will behave as expected of a drop-down activator -- it's onClick will toggle the display of the drop-down widget.

SelectionItems - drop-down control

The SplitButton drop-down list will be constructed of an OuiInputPopover containing a list of OuiContextMenuItem, each containing items of the options component list.

Action of click on SelectionItems

All SelectionItems, upon click, will be handled by the SplitButton, which will call the onChange callback with a single argument - the integer index of the SelectionItem that was clicked.

Action of click on PrimaryButton

When the PrimaryButton is clicked, the SplitButton control will call the onClick callback, with no arguments.

Selection state

The index of options item that will be the PrimaryButton will be controlled with the selectedIndex property. If undefined, no item will be selected. The selected item will be marked with a "check" OuiIcon.


Split Button (for documentation site)

This replacement for OuiButton provides advanced options for contexts where several actions should be available. Simply pass an array of strings or React components as options :

OuiSplitButton comes in two styles. The fill style should be reserved for the main action and limited in number for a single page. Be sure to read the full button usage guidelines.

Selection Items

The array of options provides a list of selection items, displayed in a drop-down style popover. This popoer is toggled by the SplitButton's styled drop-down button.

When a SelectionItem is clicked, the onChange callback will be called with the index of that item's position in the options list.

A SelectionItem may be marked as selected with the prop selectedIndex. The selected itme will be marked with a "check" OuiIcon

Props

Prop Type Default Value
options string FunctionComponent<{}> ComponentClass<{}>
size
Use s in confined spaces
s, m
fill
Make button a solid color for prominence
boolean false
color
Any of our named colors.
"text", "success", "accent", "danger", "warning", "ghost", "primary" primary
isDisabled
disabled is also allowed
boolean
fullWidth
Extends the button to 100% width
boolean
minWidth
Override the default minimum width
string \| number
dropdownIcon
Use an alternate Icon on the Secondary button
IconType
dropdownIconSize s, m
popoverClassName
Applied to the outermost wrapper (popover)
string
className string
isOpen
Controls whether the options are shown
boolean false
selectedIndex
Index of options that is marked
integer 0
onChange
Callback to notify an item in the drop-down has been selected.
(index: integer) => void
onClick
Callback to notify that the PrimaryButton has been clicked.
() => void
ariaLabel
@see aria-labeledby
string
data-test-subj string

Design Options

These options were considered during the iterative design process of this RFC

"replace" action

In this option, a selectionAction prop would place the SplitButton into different "modes" :

execute (default) -- In execute mode, selecting an item from the drop-down list will immediately execute that item's onClick handler, as provided by that item's definition in the options prop.

select -- in select mode, selecting an item from the drop-down list will select that item to be the SplitButton PrimaryButton. By default, the first item in the options prop is always selected as PrimaryButton.

The select mode is an interaction that was named "replace" in earlier iterations.

Pros :

Cons :

Advanced options props

In earlier iteration, the options array was defined in a way to attempt to support the "replace" action. It was defined as :

Pros :

Cons :

Immediate Secondary Action vs Selection

Two forms of SplitButton appear "in the wild".

  1. Immediate Action

    a. The alternative actions are regular Buttons themselves, and each alternative's `onClick` event takes place immediately upon selecting the alternative.

    [NEED AN EXAMPLE]

  2. Replacement of Primary Button

    a. Navigation on selection of option without button click, or single click as opposed to 2 click function. (Possibly not recommended unless action can be cancelled or delayed.)

    b. Using only an icon for the action button (Possibly not recommended or used sparingly unless icon clearly defines the action.)

    c. Popover v Dropdown container - most examples opt for a dropdown container as opposed to a popover container.

splitbutton

Options advanced features - "disabled", etc

Options items for this iteration are simple string | ReactNode. We want to allow flexible use-cases and alternate states of option items. The reference implementation for this is SuperSelect.

In a future iteration, Options can be extended -- see #1195

FAQ

Conversations of note

As far as things to consider for UX

  1. Use of Icon only for primary action button - we can have this capability but should write guidelines and warnings around using only an icon for an action. We can also set constraints for when it may be appropriate to use only an icon for such cases.
  2. Number of options in the dropdown - we should have a limit for number of items in a split button so it does not become overwhelming. If there is a need for many options, perhaps a component like a dropdown select field is more appropriate in these cases.
  3. Option Selection can A. Change the label and contents of the primary action button (ex. Another action is selected and swapped as the primary action.) B. Label and contents remain static but the type of action is modified depending on what is selected (ex. An execution of an action but executed with varying parameters.)
  4. Action is triggered by clicking on the primary action button.

Prototype https://www.figma.com/proto/m3JvQ6e0pQztyywt8xdJdc/Split-button?page-id=41%3A2795&type=design&node-id=41-2796&viewport=386%2C464%2C0.29&t=b7xU3TuGlmyPR2nR-1&scaling=min-zoom&mode=design

@kgcreative, @shanilpa for visibility

Originally posted by @canascar in https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3621#issuecomment-1852982697

Reference implementations

Definition: A split button is a button with two components: a label and an arrow; clicking on the label selects a default action, and clicking on the arrow opens up a list of other possible actions. (Reference)

anirudha commented 6 months ago

How do we consider managing default state and last selected state

pjfitzgibbons commented 6 months ago

Default state is Primary state. I'll be writing more detail on this shortly.

For Last-selected state, which would be the "replacement" context -- state will not be kept between refreshes or page navigations. Like the ImmediateAction context (the default), the button presents its Primary action on any new render.

ashwin-pc commented 6 months ago

So persisting state across refreshs is possible in only one of two ways, either through the URL (Highly discouraged) or using browser storage. I think OUI has a few components that store state locally but this is a pattern that i dont think OUI should follow and leave state storage up to the application that renders it. This makes OUI more UI focussed while leaving framework specific tasks like this to the application (i.e. like controlled components from react)

@pjfitzgibbons What would the props of this component look like?

pjfitzgibbons commented 6 months ago

@ashwin-pc Thanks for asking about props! I have updated the Description above with a sample documentation of the widget and some technical details of its construction. I look forward to your further thoughts!

pjfitzgibbons commented 6 months ago

@shanilpa Could you look at the updated Description, especially "Primary and Secondarie scan replace each other with secondaryAction"

As you can see, this is very complicated to describe. I'm wondering if it would be much easier to copy the SuperSelect functionality more directly -- meaning that "replace" mode as the UI samples describe it would be the only functionality of SplitButton. This is exactly as SuperSelect works already, and is also how our reference-sample works : image

If you agree, I would like to just have the 'replace' mode, and will update doc to remove the concept of secondaryAction entirely. I will integrate parts of the SuperSelect doc that describe how the selection works.

Your thoughts?

kgcreative commented 6 months ago

Cc: @canascar

ashwin-pc commented 6 months ago

Thanks @pjfitzgibbons, just a few thoughts an questions about the implementation:

Split Button will mainly be a clone of SuperSelect, with the following modifications.

Why are we cloning that component? Its an opinionated component if im not wrong.

Primary and Secondaries can replace each other with secondaryAction

Nice! I like this feature. One comment i have is to make this clear earlier in the design. It wasnt obvious to me until i came down to this section that both immediately triggering a secondary option was possible since you talk mostly about replace in the beginning.

Also you mention secondaryAction props as immediate in that section and action in the props table below. I like immediate more than action but just wanted to call that out

FunctionComponent<{}>, ComponentClass<{}> for the text prop

Why do we need this?


I also noticed here that you are thinking about only having the replace option and the justification seems to be that its easier since its similar to SuperSelect. I do not think thats a good idea since all split button implementations that i have used in the past typically behave similar to the immediate method you suggested here. Even GitHub's split button on this page behaves similarly. So love to have the option to switch between the two modes but if are picking one, i think we should pick immediate over replace as our preferred choice

canascar commented 6 months ago

Would the Immediately trigger the action of clicking the primary action button? If so, you may want to consider "replace" instead and then allowing the user to click once the button action is replaced. By using immediately i feel you run the risk of triggering an unwanted immediate action. This should at least be an optional that SDE can turn on/off.

pjfitzgibbons commented 6 months ago

@ashwin-pc @canascar @kgcreative @shanilpa I have updated the Description, which now includes a full doc-site sample. The whole concept around primary/secondary, selection-action, "replace", etc is nearly entriely new.

Please re-read the Discussion as if this is a first iteration. The comment thread here is of course, still relevant.

shanilpa commented 6 months ago

I think this looks good to me from the UX side @pjfitzgibbons.

I do have a question about the props - does the primary button have a prop for an icon as well? I only see one for the dropdown.

ashwin-pc commented 6 months ago

Having gone though this proposal a second time i'm wondering if we even need the replace feature? Cant we achieve the same with the immediate button?

e.g. When i want to make the button behave in a replace mode, all the secondary options have the same callback, it simply tells the parent component which secondary action was clicked and the parent component is responsible for updating the primary and secondary options of the button. This makes the OUI component a lot simpler and eliminates a lot of unnecessary props.

pjfitzgibbons commented 6 months ago

@ashwin-pc I think I see where you're going with this. Would you suggest we should include a doc example of this? I'ts an important feature and we have a direct use-case for it in Olly/Assistant.
I like the direction... just want to be sure we're covering the required use-cases before I fully commit.

pjfitzgibbons commented 6 months ago

does the primary button have a prop for an icon as well?

@shanilpa the entire content of the PrimaryButton, including an icon, is determined by the input of primaryDisplay prop of that options item. If the container chooses to supply a React component, it may be styled as the see fit.

ashwin-pc commented 6 months ago

Yeah for sure! we can show the ways it can be used in the doc and that can also be used as a reference implementation for Olly :)

pjfitzgibbons commented 6 months ago

@ashwin-pc @shanilpa I updated Description again, with a much simplified SplitButton API. I agree, with a couple of examples, this will be a useful control to have in the library.

ashwin-pc commented 6 months ago

This looks good, a few small suggestions:

  1. For SplitButtonOptionProps just pass an array of string or react components. No need for primary or secondary display items. The children prop for the OuiSplitButton can be the primary display. If we want the primary display option to show up in the secondary options too, that can be again passed in the same array as the other options for the secondary display.
  2. If selectionIndex is undefined, none of the options should be selected. This is because in some cases the secondary options will be actions other than the primary action. In that case selecting the first action in the secondary actions list does not make sense.