Rask
Rask
v0.23.1-alpha.0.440

Components

Dropdown

A button and the menu it opens. The first child of Ui.Dropdown is the trigger, any button; the second is what it opens: a Ui.Menu of actions, a Ui.Navmenu of links, or a Ui.Popover. The browser opens and closes it, and the page hears only a pick.

Drawn and behaving as Flux UI's dropdown does, measured against its live docs.

Dropdown

A menu with a submenu of radios, one of checkboxes and a danger row. It opens with focus on the menu: the arrows walk the rows and stop at the ends, a letter jumps, Right or Enter opens a submenu and Left closes it, Enter or Space picks, Escape and Tab leave.

DropdownExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class DropdownExample : Component
{
    private string _sort = "name";
    private bool _draft = true;
    private bool _published = true;
    private bool _archived;
    private string _last = "nothing yet";

    protected override Component? Render() =>
        Div.Class("flex flex-wrap items-center gap-4")[
            Ui.Dropdown[
                Ui.Button.IconTrailing(Ui.IconName.ChevronDown)["Options"],
                Ui.Menu[
                    Ui.MenuItem.Icon(Ui.IconName.Plus).OnClick(() => { _last = "new post"; })["New post"],
                    Ui.MenuSeparator.Key("s1"),
                    Ui.MenuSubmenu.Heading("Sort by")[
                        Ui.MenuRadioGroup.Value(_sort).OnChange(sort => { _sort = sort; _last = "sorted by " + sort; })[
                            Ui.MenuRadio.Value("name")["Name"],
                            Ui.MenuRadio.Value("date")["Date"],
                            Ui.MenuRadio.Value("popularity")["Popularity"]
                        ]
                    ],
                    Ui.MenuSubmenu.Heading("Filter")[
                        Ui.MenuCheckbox.Value(_draft).OnChange(on => { _draft = on; })["Draft"],
                        Ui.MenuCheckbox.Value(_published).OnChange(on => { _published = on; })["Published"],
                        Ui.MenuCheckbox.Value(_archived).OnChange(on => { _archived = on; })["Archived"]
                    ],
                    Ui.MenuSeparator.Key("s2"),
                    Ui.MenuItem.Danger.Icon(Ui.IconName.Trash).OnClick(() => { _last = "deleted"; })["Delete"]
                ]
            ],
            Ui.Text[$"Last action: {_last}."]
        ];
}
Live result

Last action: nothing yet.

Navigation menus

A Ui.Navmenu is links, not a menu: no menu roles and no cursor, so focus stays on the button and Tab walks the links.

DropdownNavigationExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class DropdownNavigationExample : Component
{
    protected override Component? Render() =>
        Ui.Dropdown.Bottom.End[
            Ui.Button.IconTrailing(Ui.IconName.ChevronDown)["Rosa Vidal"],
            Ui.Navmenu[
                Ui.NavmenuItem.Href("#account").Icon(Ui.IconName.User)["Account"],
                Ui.NavmenuItem.Href("#profile").Icon(Ui.IconName.BuildingStorefront)["Profile"],
                Ui.NavmenuItem.Href("#billing").Icon(Ui.IconName.CreditCard)["Billing"],
                Ui.NavmenuItem.Href("#logout").Icon(Ui.IconName.ArrowRightStartOnRectangle)["Logout"],
                Ui.NavmenuItem.Href("#delete").Icon(Ui.IconName.Trash).Danger["Delete"]
            ]
        ];
}

Positioning

A side and an alignment, each a step. With no room there it takes the other side, and it always stays inside the viewport.

DropdownPositionExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class DropdownPositionExample : Component
{
    protected override Component? Render() =>
        Ui.Dropdown.Top.Start[
            Ui.Button.IconTrailing(Ui.IconName.ChevronDown)["Above"],
            Ui.Navmenu[
                Ui.NavmenuItem.Href("#account")["Account"],
                Ui.NavmenuItem.Href("#profile")["Profile"],
                Ui.NavmenuItem.Href("#billing")["Billing"],
                Ui.NavmenuItem.Href("#logout")["Logout"]
            ]
        ];
}

Offset & gap

Gap is the distance from the button; Offset shifts the menu along it, back past its edge when negative.

DropdownOffsetExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class DropdownOffsetExample : Component
{
    protected override Component? Render() =>
        Ui.Dropdown.Offset(-15).Gap(2)[
            Ui.Button.IconTrailing(Ui.IconName.ChevronDown)["Offset and gap"],
            Ui.Navmenu[
                Ui.NavmenuItem.Href("#account")["Account"],
                Ui.NavmenuItem.Href("#profile")["Profile"],
                Ui.NavmenuItem.Href("#billing")["Billing"],
                Ui.NavmenuItem.Href("#logout")["Logout"]
            ]
        ];
}
Live result

Keyboard hints

Kbd shows a shortcut at the end of a row and Suffix a word; the page binds the key itself. A disabled row is shown and stepped over.

DropdownShortcutsExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class DropdownShortcutsExample : Component
{
    private string _last = "nothing yet";

    protected override Component? Render() =>
        Div.Class("flex flex-wrap items-center gap-4")[
            Ui.Dropdown[
                Ui.Button.IconTrailing(Ui.IconName.ChevronDown)["Shortcuts"],
                Ui.Menu[
                    Ui.MenuItem.Icon(Ui.IconName.PencilSquare).Kbd("⌘S").OnClick(() => { _last = "save"; })["Save"],
                    Ui.MenuItem.Icon(Ui.IconName.DocumentDuplicate).Kbd("⌘D").OnClick(() => { _last = "duplicate"; })["Duplicate"],
                    Ui.MenuItem.Icon(Ui.IconName.ArrowDownTray).Suffix("PDF").Disabled()["Export"],
                    Ui.MenuItem.Icon(Ui.IconName.Trash).Danger.Kbd("⌘⌫").OnClick(() => { _last = "delete"; })["Delete"]
                ]
            ],
            Ui.Text[$"Last action: {_last}."]
        ];
}
Live result

Last action: nothing yet.

Checkbox items

Rows that are on or off: Value with OnChange, or Bind to a model's flag. A pick closes the menu, as in Flux.

DropdownCheckboxExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class DropdownCheckboxExample : Component
{
    private bool _read = true;
    private bool _write = true;
    private bool _delete;

    protected override Component? Render() =>
        Div.Class("flex flex-wrap items-center gap-4")[
            Ui.Dropdown[
                Ui.Button.IconTrailing(Ui.IconName.ChevronDown)["Permissions"],
                Ui.Menu[
                    Ui.MenuCheckbox.Value(_read).OnChange(on => { _read = on; })["Read"],
                    Ui.MenuCheckbox.Value(_write).OnChange(on => { _write = on; })["Write"],
                    Ui.MenuCheckbox.Value(_delete).OnChange(on => { _delete = on; })["Delete"]
                ]
            ],
            Ui.Text[_delete ? "May delete." : "May not delete."]
        ];
}
Live result

May not delete.

Radio items

One of several, in a Ui.MenuRadioGroup whose type is the type of its values.

DropdownRadioExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class DropdownRadioExample : Component
{
    private string _by = "latest";

    protected override Component? Render() =>
        Div.Class("flex flex-wrap items-center gap-4")[
            Ui.Dropdown[
                Ui.Button.IconTrailing(Ui.IconName.ChevronDown)["Sort by"],
                Ui.Menu[
                    Ui.MenuRadioGroup.Value(_by).OnChange(by => { _by = by; })[
                        Ui.MenuRadio.Value("latest")["Latest activity"],
                        Ui.MenuRadio.Value("created")["Date created"],
                        Ui.MenuRadio.Value("popular")["Most popular"]
                    ]
                ]
            ],
            Ui.Text[$"Ordered by {_by}."]
        ];
}
Live result

Ordered by latest.

Groups

Separators set related rows apart.

DropdownGroupsExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class DropdownGroupsExample : Component
{
    protected override Component? Render() =>
        Ui.Dropdown[
            Ui.Button.IconTrailing(Ui.IconName.ChevronDown)["Groups"],
            Ui.Menu[
                Ui.MenuItem["View"],
                Ui.MenuItem["Transfer"],
                Ui.MenuSeparator.Key("s1"),
                Ui.MenuItem["Publish"],
                Ui.MenuItem["Share"],
                Ui.MenuSeparator.Key("s2"),
                Ui.MenuItem.Danger["Delete"]
            ]
        ];
}
Live result

Groups with headings

A Ui.MenuGroup with a Heading names the rows under it.

DropdownHeadingsExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class DropdownHeadingsExample : Component
{
    protected override Component? Render() =>
        Ui.Dropdown[
            Ui.Button.IconTrailing(Ui.IconName.ChevronDown)["Headings"],
            Ui.Menu[
                Ui.MenuGroup.Heading("Account")[
                    Ui.MenuItem["Profile"],
                    Ui.MenuItem["Permissions"]
                ],
                Ui.MenuGroup.Heading("Billing")[
                    Ui.MenuItem["Transactions"],
                    Ui.MenuItem["Payouts"],
                    Ui.MenuItem["Refunds"]
                ],
                Ui.MenuItem["Logout"]
            ]
        ];
}
Live result

Keep open

KeepOpen on the menu keeps it up after every pick, for one someone ticks several of. On the rows alone, the other rows still close it.

DropdownKeepOpenExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class DropdownKeepOpenExample : Component
{
    private bool _draft = true;
    private bool _published = true;
    private bool _archived;
    private string _last = "nothing yet";

    protected override Component? Render() =>
        Div.Class("flex flex-wrap items-center gap-4")[
            // On the menu: every row keeps it open.
            Ui.Dropdown.Key("menu")[
                Ui.Button.IconTrailing(Ui.IconName.ChevronDown)["Keep open"],
                Ui.Menu.KeepOpen()[
                    Ui.MenuCheckbox.Value(_draft).OnChange(on => { _draft = on; })["Draft"],
                    Ui.MenuCheckbox.Value(_published).OnChange(on => { _published = on; })["Published"],
                    Ui.MenuCheckbox.Value(_archived).OnChange(on => { _archived = on; })["Archived"]
                ]
            ],
            // On the rows: they keep it open, and Clear still closes it.
            Ui.Dropdown.Key("rows")[
                Ui.Button.IconTrailing(Ui.IconName.ChevronDown)["Filters"],
                Ui.Menu[
                    Ui.MenuCheckbox.Value(_draft).KeepOpen().OnChange(on => { _draft = on; })["Draft"],
                    Ui.MenuCheckbox.Value(_published).KeepOpen().OnChange(on => { _published = on; })["Published"],
                    Ui.MenuCheckbox.Value(_archived).KeepOpen().OnChange(on => { _archived = on; })["Archived"],
                    Ui.MenuSeparator,
                    Ui.MenuItem.Danger.OnClick(() =>
                    {
                        _draft = _published = _archived = false;
                        _last = "cleared the filters";
                    })["Clear"]
                ]
            ],
            Ui.Text[$"Last action: {_last}."]
        ];
}
Live result

Last action: nothing yet.

Open state

Open hands the state to the page, and OnToggle tells it when the reader opens or closes it. Unset, the browser keeps it and nothing is sent.

DropdownOpenExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class DropdownOpenExample : Component
{
    private bool _open;
    private string _last = "nothing yet";

    protected override Component? Render() =>
        Div.Class("flex flex-wrap items-center gap-4")[
            Ui.Dropdown.Open(_open).OnToggle(open => { _open = open; })[
                Ui.Button.IconTrailing(Ui.IconName.ChevronDown)[_open ? "Close menu" : "Open menu"],
                Ui.Menu[
                    Ui.MenuItem.Kbd("⌘R").OnClick(() => Did("rename"))["Rename"],
                    Ui.MenuItem.OnClick(() => Did("archive"))["Archive"]
                ]
            ],
            Ui.Text[$"Last action: {_last}."]
        ];

    private void Did(string action)
    {
        _last = action;
        _open = false;
    }
}
Live result

Last action: nothing yet.

Behaviour

Who owns what

The browser owns the open state. The menu is a [popover] the trigger opens with popovertarget, so the browser opens and closes it (the top layer, Escape, a click outside) with no runtime at all, and it is placed with CSS anchor positioning.

Where the reader is in an open menu is not the page's state either: the runtime keeps the cursor in the browser (data-rask-menu-cursor on the menu), so a pointer gliding down the rows and an arrow key held down send the server nothing (no event, no render, no patch) on either host.

What the page hears is a row being pressed (its OnClick, a checkbox's or a radio's value) and, only when it gave OnToggle, the menu opening and closing. Without OnToggle, opening and closing a dropdown, a popover or a context menu sends nothing, and neither does a Ui.MenuItem with no OnClick: data-open and aria-expanded are written in the browser.

Placement

Every panel the kit opens against a trigger stays inside the viewport as Flux's does: a menu, a popover, a tooltip, a list of options, a calendar, a list of times. When there is no room where it asked to be it takes the other alignment, then the other side, and is slid back to stay 5px inside the edge; one taller than what is left is held to that height and scrolls inside. It is the runtime's data-rask-place hook, with nothing to write in an app.

While a menu is open the page behind does not scroll and does not take the pointer, so a click outside only closes it (data-rask-lock on the menu, a dropdown's and a context menu's alike, as on Flux), and the runtime hands focus back to the trigger after it. A menu the pointer opened, Hover() (data-rask-hover), locks nothing and takes no focus.

Keyboard

Flux's, key for key. The menu opens with focus on it and no row chosen. The row under the cursor has focus (roving focus, not aria-activedescendant), tabindex="0" and data-active, all three written by the runtime and held against the next render, never rendered.

Key
What happens
ArrowDown on the closed triggerOpens the menu onto its first row.
ArrowDown / ArrowUpThe first press puts the cursor on the first row; after that the cursor moves a row at a time, stops at the ends and steps over disabled rows.
A letterJumps to the row that starts with it.
ArrowRight or Enter on a submenu's rowOpens the submenu onto its first row.
ArrowLeft in a submenuCloses it, with the cursor back on its row.
Enter or SpacePicks: the runtime presses the focused row, then closes the menu.
TabLeaves the menu, which closes it.
EscapeCloses it: the browser's own.
Home, End, PageUp, PageDownNothing: they are not menu keys.

Pointer, and submenus

There is one highlight. A row is lit by data-active and by nothing else, no :hover, whether the pointer or the keyboard lit it. The runtime writes it, in the browser, the moment the pointer enters a row (data-rask-menu-pointer). Focus does not move, as on Flux, so the row the keyboard was on keeps the tab stop.

The arrows count from the lit row: with focus still on the menu the first arrow takes the row the pointer is on, and after that each goes on from whichever row is lit. When the pointer leaves the menu the row it lit goes dark, and the keyboard's row stays lit.

A submenu flies out beside its row the moment a pointer rests on it. It stays while the pointer moves diagonally toward it, across the rows below, because the triangle between the pointer and the flyout still counts as the row (data-rask-safe-area, Flux's safe area). It stays after the pointer has left the menu altogether, and closes when another row is entered.

While the arrows are inside a flyout its submenu's row stays lit with the row they are on. A tap opens a submenu on a touch screen, and the next one closes it. An open flyout is data-open on the submenu, written by the runtime like the rest.

Keep open

A pick closes the menu, a checkbox or a radio too, as in Flux. KeepOpen on the menu, a submenu, a radio group or a single row keeps it up, for the menu someone ticks several of.

Open state

Open is nullable, and its three settings mean three things. Unset leaves it to the reader; true and false hand it to the page, and the runtime shows or hides the popover to match whenever the page changes its mind, which is what lets a dropdown close itself when the action inside it completes: Ui.Dropdown.Open(_open).OnToggle(open => _open = open)[trigger, Ui.Menu[…]].

Rows, links and keys

The first child of a dropdown is the trigger: a Button, a Ui.Button, any element that is a button. The second is what it opens: a Ui.Menu of actions, a Ui.Navmenu of links, or a Ui.Popover.

A Ui.NavmenuItem is a link, with no menu roles and no cursor. Without an Href it is a <button> with OnClick; a string Href is a plain <a>, and a generated route navigates in the app.

Key can go anywhere in a row's chain, Ui.MenuCheckbox.Value(x).Key("k"), and a generic row such as Ui.MenuRadioGroup takes one too.

Style the rows from data-active and data-checked, and from data-open on the dropdown, its trigger and an open submenu.

Reference

Ui.Dropdown

flux:dropdown

A trigger, its first child, and what it opens, its second: a menu, a navigation menu or a popover.

Step
Takes
What it does
.Position(…)
Ui.DropdownPosition
Bottom
Top
Right
Left

Each is a step of its own too, unless an HTML tag has its name.

Which side of the trigger it opens on. Below when unset.
.Align(…)
Ui.DropdownAlign
Start
Center
End

Each is a step of its own too, unless an HTML tag has its name.

Where along that side it sits. Its start edge on the trigger's when unset.
.Gap(…)intThe distance from the trigger, in pixels. 5 when unset.
.Offset(…)intShifts it along its alignment, in pixels; negative moves it back past the trigger's edge.
.Hover()boolOpens it while the pointer is over the trigger or the panel. A press and the keyboard still do.
.Open()boolHands the open state to the page. Unset, the reader opens and closes it.
.OnToggle(…)a handler given the state it is now in: open => …Runs when the reader opens or closes it.

Ui.Menu

flux:menu

The rows a dropdown or a context menu opens, with Flux's keyboard.

Step
Takes
What it does
.KeepOpen()boolKeeps it open after any row in it is picked.

Ui.MenuItem

flux:menu.item

An action. Its children are its words.

Step
Takes
What it does
.Icon(…)Ui.IconName — one of 317 namesAn icon at the start of the row.
.IconTrailing(…)Ui.IconName — one of 317 namesAn icon at the end of the row.
.IconVariant(…)
Ui.IconVariant
Outline
Solid
Mini
Micro

Each is a step of its own too, unless an HTML tag has its name.

Which drawing of the icons. Mini, 20px, when unset.
.Kbd(…)stringA shortcut shown at the end of the row: "⌘S". A hint: the page binds the key.
.Suffix(…)stringWords at the end of the row: a count, a state.
.Variant(…)
Ui.MenuItemVariant
Default
Danger

Each is a step of its own too, unless an HTML tag has its name.

Danger for a destructive action.
.Disabled()boolShown but not pickable. The keyboard steps over it.
.KeepOpen()boolKeeps the menu open after this row is picked.
.Href(…)a route, or a URL stringWhere it leads. A generated route stays in the app; a string is a link the browser follows.
.OnClick(…)a handler, with no argumentWhat picking it does.

Ui.MenuSubmenu

flux:menu.submenu

A row that flies out a menu of its own beside it.

Step
Takes
What it does
.Heading(…)stringThe row's words. Required, and taken first.
.Icon(…)Ui.IconName — one of 317 namesAn icon at the start of the row.
.IconTrailing(…)Ui.IconName — one of 317 namesThe icon at the end of the row. A chevron when unset.
.IconVariant(…)
Ui.IconVariant
Outline
Solid
Mini
Micro

Each is a step of its own too, unless an HTML tag has its name.

Which drawing of the icons. Mini when unset.
.KeepOpen()boolKeeps the menu open after any row in it is picked.

Ui.MenuSeparator

flux:menu.separator

A line between rows.

No steps of its own: what goes inside it is its content.

Ui.MenuGroup

flux:menu.group

Rows set apart from the rest, under a heading or between lines.

Step
Takes
What it does
.Heading(…)stringThe words above the rows. Without it the group is set apart by lines alone.

Ui.MenuCheckbox

flux:menu.checkbox

A row that is on or off.

Step
Takes
What it does
.Bind(…)() => a boolThe model's flag it reads and writes: () => m.Draft. Or Value with OnChange.
.Value(…)boolWhether it is ticked, for a page that holds the flag itself.
.OnChange(…)a handler given the new valueRuns with the new state when it is picked.
.AfterBind(…)a handler given the new valueRuns after Bind has written the model.
.Validate(…)a validatorA rule the value is checked by when it is written.
.Live()boolWrites the model as soon as it is picked, Flux's wire:model.live, not with the next action.
.Disabled()boolShown but not pickable.
.KeepOpen()boolKeeps the menu open after it is picked.

Ui.MenuCheckboxGroup

flux:menu.checkbox.group

Checkbox rows kept together. Each binds its own flag.

No steps of its own: what goes inside it is its content.

Ui.MenuRadioGroup

flux:menu.radio.group

Radio rows of which one is picked. Its type is the type of their values.

Step
Takes
What it does
.Bind(…)() => a TThe model's value it reads and writes: () => m.Sort. Or Value with OnChange.
.Value(…)T, the type of the radios' valuesThe picked value, for a page that holds it itself.
.OnChange(…)a handler given the new valueRuns with the new value when a row is picked.
.AfterBind(…)a handler given the new valueRuns after Bind has written the model.
.Validate(…)a validatorA rule the value is checked by when it is written.
.Live()boolWrites the model as soon as a row is picked, not with the next action.
.KeepOpen()boolKeeps the menu open after a row in it is picked.

Ui.MenuRadio

flux:menu.radio

One choice in a radio group.

Step
Takes
What it does
.Value(…)a value of the group's typeThe value it stands for, compared with the group's.
.Checked()boolWhether it is the chosen one, for a radio outside a group.
.Disabled()boolShown but not pickable.
.KeepOpen()boolKeeps the menu open after it is picked.
.OnClick(…)a handler, with no argumentRuns when it is picked, after its group has taken the value.

Ui.Navmenu

flux:navmenu

A dropdown's list of links: no menu roles, no cursor, and Tab walks it.

No steps of its own: what goes inside it is its content.

Ui.NavmenuItem

flux:navmenu.item

One link. Its children are its words.

Step
Takes
What it does
.Href(…)a route, or a URL stringWhere it leads. Without one the row is a button.
.OnClick(…)a handler, with no argumentWhat pressing a row with no Href does.
.Icon(…)Ui.IconName — one of 317 namesAn icon at the start of the row.
.IconVariant(…)
Ui.IconVariant
Outline
Solid
Mini
Micro

Each is a step of its own too, unless an HTML tag has its name.

Which drawing of the icon. Mini when unset.
.Variant(…)
Ui.MenuItemVariant
Default
Danger

Each is a step of its own too, unless an HTML tag has its name.

Danger for a destructive destination.