defmodule PhiaUi.Components.BulkActionBar do @moduledoc """ BulkActionBar component for PhiaUI. A contextual action toolbar that appears when one or more rows are selected in a table or list. Displays the selection count, a clear-selection button, and a slot for action buttons. The toolbar is hidden automatically when `count` is zero — implemented as two function heads on `bulk_action_bar/1`: the `count: 0` head renders an empty fragment so the DOM element is completely absent, while any non-zero count renders the full toolbar. This avoids CSS `display:none` tricks and keeps the DOM clean. ## When to use Use `BulkActionBar` in data table or list views wherever you want to let users select multiple rows and act on them collectively — for example: - Delete multiple records at once - Archive or restore a batch of items - Export selected rows to CSV - Assign multiple tasks to a team member - Approve / reject a group of requests ## Anatomy | Component | Element | Purpose | |--------------------|-----------|-----------------------------------------------------| | `bulk_action_bar/1`| `div` | Root toolbar (`role="toolbar"`) — hidden when count=0 | | `bulk_action/1` | `button` | Individual action button with optional icon and variant | ## Example — table with bulk actions <%!-- Toolbar sits above (or below) the table and shows when rows are selected --%> <.bulk_action_bar count={@selected_count} label="contacts selected" on_clear="clear_selection" > <.bulk_action label="Send email" on_click="bulk_email" icon="mail" /> <.bulk_action label="Export CSV" on_click="bulk_export" icon="download" /> <.bulk_action label="Archive" on_click="bulk_archive" icon="archive" /> <.bulk_action label="Delete" on_click="bulk_delete" icon="trash" variant="destructive" /> <.data_grid rows={@contacts} columns={@columns} /> ## LiveView handlers def handle_event("clear_selection", _params, socket) do {:noreply, assign(socket, selected_ids: MapSet.new(), selected_count: 0)} end def handle_event("bulk_delete", _params, socket) do ids = MapSet.to_list(socket.assigns.selected_ids) Repo.delete_all(from c in Contact, where: c.id in ^ids) {:noreply, socket |> assign(selected_ids: MapSet.new(), selected_count: 0) |> stream(:contacts, Repo.all(Contact))} end def handle_event("bulk_export", _params, socket) do ids = MapSet.to_list(socket.assigns.selected_ids) csv = Contacts.export_csv(ids) {:noreply, push_event(socket, "download", %{filename: "contacts.csv", content: csv})} end ## Why two function heads instead of CSS `display:none`? When `count` is 0, `bulk_action_bar/1` renders `~H""` — an empty HEEx fragment. This means the DOM element does not exist at all, which: - Prevents screen readers from announcing the toolbar when it is inactive - Avoids accidental tab focus landing on hidden buttons - Keeps the DOM tree small for large data tables - Makes `role="toolbar"` semantics correct (toolbars should not be present in the accessibility tree when they have no applicable context) """ use Phoenix.Component import PhiaUi.ClassMerger, only: [cn: 1] import PhiaUi.Components.Icon, only: [icon: 1] # --------------------------------------------------------------------------- # bulk_action_bar/1 # --------------------------------------------------------------------------- attr(:count, :integer, required: true, doc: """ Number of currently selected items. When `0`, the entire toolbar is removed from the DOM. When positive, the toolbar renders with `count` displayed prominently. """ ) attr(:label, :string, default: "selected", doc: """ Label appended after the count, e.g. `"selected"` → "3 selected", or `"contacts selected"` → "5 contacts selected". """ ) attr(:on_clear, :string, required: true, doc: """ `phx-click` event name for the clear-selection button. The handler should reset selected IDs and count to zero. """ ) attr(:class, :string, default: nil, doc: "Additional CSS classes for the toolbar container" ) attr(:rest, :global, doc: "HTML attributes forwarded to the root div") slot(:inner_block, required: true, doc: "`bulk_action/1` children — the action buttons for this toolbar" ) @doc """ Renders the bulk action toolbar. **When `count` is 0**, renders an empty HEEx fragment (`~H""`), meaning no DOM element is present. This is preferable to CSS visibility toggling for accessibility and DOM cleanliness. **When `count > 0`**, renders a glass-morphism toolbar with: - Selection count + label on the left - A clear (×) button to deselect all rows - A vertical divider - Action buttons from the `:inner_block` slot on the right Uses `role="toolbar"` for correct ARIA semantics and includes a dynamic `aria-label` that reads the count aloud to screen readers. """ # count=0 — render nothing; the DOM element is completely absent def bulk_action_bar(%{count: 0} = assigns) do ~H""" """ end # count > 0 — render the full toolbar def bulk_action_bar(assigns) do ~H"""