Skip to content

Add a disabled option to Listbox trigger - #32

Closed
RobertJoonas wants to merge 2 commits into
mainfrom
listbox-final-touches
Closed

RobertJoonas wants to merge 2 commits into
mainfrom
listbox-final-touches

Conversation

@RobertJoonas

@RobertJoonas RobertJoonas commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Adds a disabled attribute to the listbox trigger.

Allows disabling the entire listbox with one flag instead of having to reach into Prima's internals to hand-roll aria-disabled/tabindex="-1".

<.listbox_trigger id="plan-trigger" disabled={@plan_locked?}>
  <.listbox_value>{@selected_plan}</.listbox_value>
</.listbox_trigger>

A disabled trigger can't open the listbox and is removed from the tab order — the same behavior as a native <select disabled>. Prima only sets data-disabled on it. Styling the disabled state (opacity, cursor, etc.) is left entirely to the consumer.

Why `disabled` lives on `listbox_trigger`, not on listbox (the wrapper)?
The intuitive place to put this flag is on the outer `<.listbox>`, since that's what a consumer thinks of as "the field." That doesn't work because:
  1. Listbox itself can't reach into <.listbox_trigger>s markup -- it's written by the caller as :slot content. There's no React-like mechanism in Phoenix components for a parent to inject attributes into a child component invoked inside its own slot, so <.listbox disabled={true}> has no way to make the trigger's <button> actually carry aria-disabled/tabindex="-1" in the server-rendered HTML.

  2. ARIA states and tabindex don't cascade. Unlike CSS, aria-disabled on an ancestor has no effect on a descendant's accessible state — a screen reader announces the state of whatever element has focus, not an ancestor's attributes. And tabindex only ever affects the element it's set on. So the signal that actually matters for accessibility and keyboard behavior can only ever live on the itself, not on a wrapper

    .

The alternative — having the JS hook copy a disabled flag from the wrapper down onto the button at mount time would leave a brief window after first paint, before the hook mounts, where the trigger is technically still focusable and clickable. Putting disabled directly on listbox_trigger means the correct attributes exist in the very first HTML response, with no JS dependency.

Why not implement the same ability for Dropdown?

Skipped this because there's no use case for it yet in our own application, and disabling a whole action menu sounds like a corner-case anyway. Disabling a select field is more common and finds use already in the analytics repo.

@RobertJoonas

Copy link
Copy Markdown
Contributor Author

Closing in favour of #34

@RobertJoonas
RobertJoonas deleted the listbox-final-touches branch October 2, 2026 16:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant