diff --git a/packages/live_ui/lib/live_ui/renderer.ex b/packages/live_ui/lib/live_ui/renderer.ex index 1203e39c..a299c704 100644 --- a/packages/live_ui/lib/live_ui/renderer.ex +++ b/packages/live_ui/lib/live_ui/renderer.ex @@ -1705,6 +1705,7 @@ defmodule LiveUi.Renderer do value: value, label: Map.get(option, :label, Map.get(option, "label", "")), disabled?: disabled?, + count: Map.get(option, :count, Map.get(option, "count")), attrs: segmented_button_group_option_attrs(element, event_target, value, disabled?) } end) diff --git a/packages/live_ui/lib/live_ui/widgets/segmented_button_group.ex b/packages/live_ui/lib/live_ui/widgets/segmented_button_group.ex index 706736e5..bdeb505c 100644 --- a/packages/live_ui/lib/live_ui/widgets/segmented_button_group.ex +++ b/packages/live_ui/lib/live_ui/widgets/segmented_button_group.ex @@ -48,6 +48,11 @@ defmodule LiveUi.Widgets.SegmentedButtonGroup do {option_attrs(option)} > {option_label(option)} + """ @@ -56,6 +61,7 @@ defmodule LiveUi.Widgets.SegmentedButtonGroup do defp option_value(option), do: fetch_option(option, :value) defp option_label(option), do: fetch_option(option, :label, "") defp option_attrs(option), do: fetch_option(option, :attrs, %{}) + defp option_count(option), do: fetch_option(option, :count) defp option_disabled?(option) do fetch_option(option, :disabled?) || fetch_option(option, :disabled) || false diff --git a/packages/live_ui/test/live_ui/widgets/segmented_button_group_test.exs b/packages/live_ui/test/live_ui/widgets/segmented_button_group_test.exs index 7687f240..af4c0f1d 100644 --- a/packages/live_ui/test/live_ui/widgets/segmented_button_group_test.exs +++ b/packages/live_ui/test/live_ui/widgets/segmented_button_group_test.exs @@ -203,4 +203,83 @@ defmodule LiveUi.Widgets.SegmentedButtonGroupTest do assert metadata.mountable? end end + + describe "count badge" do + test "option with count renders count badge span" do + html = + render_component(&SegmentedButtonGroup.component/1, %{ + id: "test-sbg", + options: [ + %{value: "adrs", label: "ADRs", count: 12}, + %{value: "specs", label: "Specs", count: 8} + ], + label: "Document type" + }) + + assert html =~ ~s(class="live-ui-segmented-button-group-option-count") + assert html =~ "12" + assert html =~ "8" + end + + test "count badge has aria-hidden=true" do + html = + render_component(&SegmentedButtonGroup.component/1, %{ + id: "test-sbg", + options: [%{value: "a", label: "All", count: 5}], + label: "Test group" + }) + + assert html =~ ~s(aria-hidden="true") + end + + test "option without count does not render count badge span" do + html = + render_component(&SegmentedButtonGroup.component/1, %{ + id: "test-sbg", + options: @options, + label: "Test group" + }) + + refute html =~ ~s(live-ui-segmented-button-group-option-count) + end + + test "option with count nil does not render count badge span" do + html = + render_component(&SegmentedButtonGroup.component/1, %{ + id: "test-sbg", + options: [%{value: "a", label: "Option A", count: nil}], + label: "Test group" + }) + + refute html =~ ~s(live-ui-segmented-button-group-option-count) + end + + test "count of zero renders a badge with '0'" do + html = + render_component(&SegmentedButtonGroup.component/1, %{ + id: "test-sbg", + options: [%{value: "plans", label: "Plans", count: 0}], + label: "Test group" + }) + + assert html =~ ~s(live-ui-segmented-button-group-option-count) + assert html =~ ">0<" + end + + test "mixed options: some with count, some without" do + html = + render_component(&SegmentedButtonGroup.component/1, %{ + id: "test-sbg", + options: [ + %{value: "adrs", label: "ADRs", count: 12}, + %{value: "specs", label: "Specs"} + ], + label: "Document type" + }) + + assert html =~ "12" + assert html =~ "ADRs" + assert html =~ "Specs" + end + end end diff --git a/packages/unified_iur/lib/unified_iur/widgets/components.ex b/packages/unified_iur/lib/unified_iur/widgets/components.ex index e030b894..c0ac5fbb 100644 --- a/packages/unified_iur/lib/unified_iur/widgets/components.ex +++ b/packages/unified_iur/lib/unified_iur/widgets/components.ex @@ -851,9 +851,19 @@ defmodule UnifiedIUR.Widgets.Components do |> maybe_put(:value, option(option_value, :value)) |> maybe_put(:label, option(option_value, :label)) |> maybe_put(:disabled?, option(option_value, :disabled?)) + |> maybe_put(:count, normalize_option_count(option(option_value, :count))) end) end + defp normalize_option_count(nil), do: nil + + defp normalize_option_count(count) when is_integer(count) and count >= 0, do: count + + defp normalize_option_count(count) do + raise ArgumentError, + "option :count must be a non-negative integer or nil, got: #{inspect(count)}" + end + defp normalize_maps(values) when is_list(values) do Enum.map(values, &normalize_map/1) end diff --git a/packages/unified_iur/test/unified_iur/widgets/components_test.exs b/packages/unified_iur/test/unified_iur/widgets/components_test.exs index 0a3b26b0..510c7112 100644 --- a/packages/unified_iur/test/unified_iur/widgets/components_test.exs +++ b/packages/unified_iur/test/unified_iur/widgets/components_test.exs @@ -379,4 +379,59 @@ defmodule UnifiedIUR.Widgets.ComponentsTest do assert [%{slot: :default, element: %Element{id: :sources_body, kind: :text}}] = rail.children end + + describe "segmented_button_group count prop" do + test "passes count through to normalized options when present" do + element = + Components.segmented_button_group( + [ + %{value: :adrs, label: "ADRs", count: 12}, + %{value: :specs, label: "Specs", count: 8}, + %{value: :plans, label: "Plans"} + ], + active_value: :adrs + ) + + [adrs, specs, plans] = element.attributes.selection.options + + assert adrs == %{value: :adrs, label: "ADRs", count: 12} + assert specs == %{value: :specs, label: "Specs", count: 8} + # no count key when absent (maybe_put drops nil) + refute Map.has_key?(plans, :count) + end + + test "count of zero is preserved in normalized options" do + element = + Components.segmented_button_group([%{value: :empty, label: "Empty", count: 0}]) + + [opt] = element.attributes.selection.options + assert opt.count == 0 + end + + test "explicit count nil is omitted from normalized options" do + element = + Components.segmented_button_group([%{value: :a, label: "A", count: nil}]) + + [opt] = element.attributes.selection.options + refute Map.has_key?(opt, :count) + end + + test "negative count raises ArgumentError" do + assert_raise ArgumentError, ~r/non-negative integer/, fn -> + Components.segmented_button_group([%{value: :a, label: "A", count: -1}]) + end + end + + test "non-integer count raises ArgumentError" do + assert_raise ArgumentError, ~r/non-negative integer/, fn -> + Components.segmented_button_group([%{value: :a, label: "A", count: "12"}]) + end + end + + test "float count raises ArgumentError" do + assert_raise ArgumentError, ~r/non-negative integer/, fn -> + Components.segmented_button_group([%{value: :a, label: "A", count: 3.5}]) + end + end + end end