Skip to content

Actions ​

WxAction is the square icon button at the end of a row — edit, duplicate, delete. WxActions is the row they sit in, and it can fold itself into a dropdown when the column gets too narrow.

The kinds of action

Last clicked: —

In a list — a row that cannot be deleted keeps the space
It is a long established fact that a reader
Our team has grown twice this year
Collapsing — drag to narrow the container

Usage ​

vue
<template>
  <wx-actions aria-label="Row actions">
    <wx-action type="edit" :href="`/admin/pages/${page.id}/edit`" />
    <wx-action type="copy" @click="duplicate(page)" />
    <wx-action type="remove" @click="confirmRemove(page)" />
  </wx-actions>
</template>

type is what the action does, and it picks three things at once: the icon, the colour and the accessible name. That is what keeps a delete red and an edit blue on every screen without anyone having to remember.

typeIconColourtypeIconColour
addplusprimaryuploaduploadprimary
editeditprimarydownloaddownloadprimary
removetrashdangersortdragneutral
copycopyprimarysearchsearchprimary
linklinkprimaryrestorerefreshsuccess
gotoexternal-linkprimarysettingssettingsneutral
detailsinfoprimarysendmailprimary
vieweyeprimarymoremore-horizontalneutral
hideeye-offneutral

Any of the three can be overridden — icon takes any name from the icon set, including one you registered yourself:

vue
<template>
  <wx-action type="add" icon="folder" title="New folder" />
  <wx-action type="edit" tone="warning" />
</template>

An action with href renders an <a>; as renders it through another component, which is how it becomes a router link:

vue
<template>
  <wx-action type="edit" :as="RouterLink" :to="{ name: 'pages.edit', params: { id: page.id } }" />
</template>

Keeping the row aligned ​

hidden draws nothing but keeps the square. A list where one record may not be deleted still lines up with the rows where it may — which is the whole point of a column of actions:

vue
<template>
  <wx-actions>
    <wx-action type="edit" />
    <wx-action type="remove" :hidden="!page.can_delete" />
  </wx-actions>
</template>

disabled is the other half of that choice: the action is visible, greyed and inert, which says "not now" rather than "not for this record".

Asking first ​

confirm is the question to ask before the action goes ahead. The button opens a small Popconfirm beside itself, and click is reported only on a yes — so a row that was removed on click asks first by gaining one prop, and its handler stays as it was.

A row asks by its button before it goes
New
Bestseller
On sale
vue
<template>
  <wx-action
    type="remove"
    title="Take the label off"
    confirm="Delete?"
    confirm-text="Yes"
    cancel-text="Cancel"
    @click="remove(label)"
  />
</template>

The focus lands on Cancel; Enter goes ahead from anywhere in the popover, Escape and a click outside put it away. A danger action — remove is one — gets a red yes.

This is for one row of a list: a label off a product, a choice out of a field, a recipient out of a channel. Deleting a whole record — something with a page, a history, a place in other lists — is worth a dialog instead.

In the admin panel the words come from useRemoveConfirm() of @webx-ui/module-admin, in the panel's language: <wx-action type="remove" v-bind="askRemove" @click="…" />.

The tooltip ​

title is drawn by WxTooltip, not by the browser: one shape across a panel, a delay of its own and a side that can be turned away from the edge of a dialog with tooltipSide. The attribute is gone from the markup, so an action is found by its name rather than by [title=…].

A greyed action keeps its tooltip, which is when an icon needs it most. That is why disabled puts aria-disabled on the control instead of the attribute: a disabled button receives no pointer events at all, so nothing would ever open. Clicks and keys are turned away all the same, and the control stays out of the tab order.

Accessibility ​

Every action has an accessible name — label if given, otherwise title, otherwise the English name of the type. In a localised admin panel, pass title: it is both the tooltip and the name a screen reader reads.

The tooltip comes with a portal beside the control, so an action is not a single root node: in a test, reach for the button inside the wrapper rather than for the wrapper itself.

Collapsing ​

collapse measures the row against its container and folds it into a dropdown as soon as it no longer fits. The collapsed slot is what the menu shows:

vue
<template>
  <wx-actions collapse align="end" aria-label="Page actions">
    <wx-action type="edit" title="Edit" />
    <wx-action type="copy" title="Duplicate" />
    <wx-action type="remove" title="Delete" />

    <template #collapsed>
      <wx-dropdown-item icon="edit">Edit</wx-dropdown-item>
      <wx-dropdown-item icon="copy">Duplicate</wx-dropdown-item>
      <wx-dropdown-item icon="trash" tone="danger">Delete</wx-dropdown-item>
    </template>
  </wx-actions>
</template>

Leave #collapsed out and the menu shows the same actions the row holds. #trigger replaces the … button.

The row is never unmounted, only taken out of the flow — that is how the component knows there is room for it again. The container has to have a width of its own (a table cell, a card, a grid column); a container that shrinks to fit its content has nothing to measure against.

collapse="always" skips the measuring: the actions are a menu at every width, and the row is never built. That is what a list wants when every record in a panel has to open the same way — one place to look, whether a row offers one action or seven — and it is why the container's width stops mattering.

vue
<wx-actions collapse="always" align="end" aria-label="Page actions">
  <template #collapsed>
    <wx-dropdown-item icon="edit">Open</wx-dropdown-item>
    <hr class="wx-dropdown__divider" />
    <wx-dropdown-item icon="trash" tone="danger">Delete</wx-dropdown-item>
  </template>
</wx-actions>

WxAction props ​

PropTypeDefaultDescription
typeActionType'edit'What the action does — see the table above
iconstringby typeIcon to draw instead
tone'primary' | 'danger' | 'success' | 'warning' | 'neutral'by typeColour to use instead
titlestring—Tooltip, and the accessible name
labelstringby typeAccessible name on its own
hrefstring—Renders an <a>
targetstring—Target of that link
asstring | Component—Render through another component
disabledbooleanfalseVisible, greyed and inert
hiddenbooleanfalseDraws nothing, keeps the square
size'sm' | 'md' | 'lg'group's30, 36 or 42 pixels
tooltipSide'top' | 'right' | 'bottom' | 'left''top'Where the tooltip prefers to sit
confirmstring—Asks this in a popover first; click only on a yes
confirmTextstring'Yes'The button of that question that goes ahead
cancelTextstring'Cancel'The one that does not

Events: click (MouseEvent) — with confirm, after the yes. Slot: default — replaces the icon.

WxActions props ​

PropTypeDefaultDescription
align'start' | 'center' | 'end''start'Where the row sits in its space
size'sm' | 'md' | 'lg''md'Size for actions that set none
collapseboolean | 'always'falseFold into a dropdown when it does not fit; 'always' never draws the row
ariaLabelstring—Accessible name of the group
moreLabelstring'More'Name and tip of the button the folded-up menu opens from

Models: v-model:menuOpen — whether the folded-up menu is showing.

Events: collapse (boolean) — the row folded, or came back out.

Slots: default — the actions; collapsed — what the menu shows; trigger — the button that opens it.

menuOpen is there because the menu's panel is teleported. A row that hides itself until the pointer is over it — the buttons on a card, say — loses that pointer the moment the menu opens, and would fade out from under the panel hanging off it. Reading the model is how it knows to stay:

vue
<wx-actions v-model:menu-open="menuOpen" collapse :class="{ 'is-busy': menuOpen }">…</wx-actions>

The gap between actions is a variable:

vue
<template>
  <wx-actions style="--wx-actions-gap: 2px">…</wx-actions>
</template>

Released under the MIT License.