Skip to content

fix(Select): support item-aligned position mode#6358

Merged
benjamincanac merged 4 commits intov4from
fix/select-item-aligned-position
Apr 17, 2026
Merged

fix(Select): support item-aligned position mode#6358
benjamincanac merged 4 commits intov4from
fix/select-item-aligned-position

Conversation

@benjamincanac
Copy link
Copy Markdown
Member

🔗 Linked issue

❓ Type of change

  • 📖 Documentation (updates to the documentation or readme)
  • 🐞 Bug fix (a non-breaking change that fixes an issue)
  • 👌 Enhancement (improving an existing functionality)
  • ✨ New feature (a non-breaking change that adds functionality)
  • 🧹 Chore (updates to the build process or auxiliary tools and libraries)
  • ⚠️ Breaking change (fix or feature that would cause existing functionality to change)

📚 Description

Fix position: 'item-aligned' on USelect by wrapping the trigger value in SelectValue and the viewport in SelectViewport (which Reka UI requires for its layout measurement), plus scope the popper-only open/close animations to data-[position=popper]: in the theme.

📝 Checklist

  • I have linked an issue or discussion.
  • I have updated the documentation accordingly.

@github-actions github-actions Bot added the v4 #4488 label Apr 16, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 16, 2026

📝 Walkthrough

Walkthrough

Adds a position option for Select content (popper default, item-aligned), documents the option with examples and notes, and updates contributor guidance/agent rules about a Soon badge. Runtime: resolves position from props → appConfig → default, exposes position to content props, derives isItemAligned, replaces inline value/placeholder markup with RSelectValue, conditionally uses SelectViewport for item-aligned viewports, and adjusts viewportRef exposure to return the underlying element. Theme: moves animation classes into a variants.position config. Playground: example using content.position: 'item-aligned'.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and specifically describes the main change: adding support for item-aligned position mode in the Select component.
Description check ✅ Passed The PR description provides relevant context about the bug fix, explaining what changes were made and why they were necessary for Reka UI's layout measurement requirements.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/select-item-aligned-position

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
docs/content/docs/2.components/select.md (1)

226-255: ⚠️ Potential issue | 🟡 Minor

The example conflates item-aligned with popper-mode options.

Reka UI's Select component treats position="item-aligned" and position="popper" as distinct modes. The item-aligned mode has "limited side control," while side, align, and sideOffset are controls specific to position="popper". The current example lists side as an available option despite setting position: item-aligned, which is misleading. Switch this snippet to position="popper" or remove side from the options when demonstrating item-aligned.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/content/docs/2.components/select.md` around lines 226 - 255, The example
incorrectly mixes item-aligned behavior with popper-specific controls: update
the example's content prop so it uses position: popper (or alternatively remove
popper-only props like content.side/content.sideOffset from the item-aligned
example); specifically modify the YAML block that defines items/content.position
and the content props (look for content.position, content.align, content.side
and the props: content section) so that either position is changed to "popper"
when listing side/sideOffset options or remove side/sideOffset from the
item-aligned variant to avoid the misleading combination.
src/runtime/components/Select.vue (1)

348-405: ⚠️ Potential issue | 🟠 Major

viewportRef is exposed inconsistently depending on content position.

When position === 'item-aligned', the template switches to <SelectViewport> (a Vue component), causing the exposed viewportRef to be a component instance instead of an HTMLElement. This breaks the public API contract with consumers expecting Ref<HTMLDivElement | null>. Normalize by extracting $el from component instances, matching the pattern already used for triggerRef:

Suggested change
 defineExpose({
   triggerRef: toRef(() => triggerRef.value?.$el as HTMLButtonElement),
-  viewportRef: toRef(() => viewportRef.value)
+  viewportRef: toRef(() => {
+    const el = viewportRef.value as { $el?: HTMLDivElement } | HTMLDivElement | null
+    return (el && typeof el === 'object' && '$el' in el ? el.$el : el) as HTMLDivElement | null
+  })
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/components/Select.vue` around lines 348 - 405, The exposed
viewportRef becomes a component instance when position === 'item-aligned'
because the template uses <SelectViewport>; update the logic that exposes
viewportRef (same place triggerRef is normalized) to unwrap component instances
by assigning viewportRef.value = (instance && '$el' in instance) ? instance.$el
as HTMLElement : instance as HTMLElement | null so consumers always get HTML
element (reference SelectViewport, viewportRef and the normalization approach
used for triggerRef).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/runtime/components/SelectMenu.vue`:
- Line 622: The DOM attribute data-position on the ComboboxContent is hardcoded
to "popper" while contentProps may carry a different position; update the
ComboboxContent usage (in SelectMenu.vue) to bind data-position dynamically from
contentProps.position (similar to Select.vue) so the attribute reflects the
actual component state when spreading contentProps; ensure you keep the existing
:class="ui.content({ class: uiProp?.content })" and v-bind="contentProps" but
replace the static data-position with a dynamic binding to
contentProps.position.

---

Outside diff comments:
In `@docs/content/docs/2.components/select.md`:
- Around line 226-255: The example incorrectly mixes item-aligned behavior with
popper-specific controls: update the example's content prop so it uses position:
popper (or alternatively remove popper-only props like
content.side/content.sideOffset from the item-aligned example); specifically
modify the YAML block that defines items/content.position and the content props
(look for content.position, content.align, content.side and the props: content
section) so that either position is changed to "popper" when listing
side/sideOffset options or remove side/sideOffset from the item-aligned variant
to avoid the misleading combination.

In `@src/runtime/components/Select.vue`:
- Around line 348-405: The exposed viewportRef becomes a component instance when
position === 'item-aligned' because the template uses <SelectViewport>; update
the logic that exposes viewportRef (same place triggerRef is normalized) to
unwrap component instances by assigning viewportRef.value = (instance && '$el'
in instance) ? instance.$el as HTMLElement : instance as HTMLElement | null so
consumers always get HTML element (reference SelectViewport, viewportRef and the
normalization approach used for triggerRef).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 711959df-443a-4132-acdc-746ab2a21670

📥 Commits

Reviewing files that changed from the base of the PR and between 5494f6d and 5082cf6.

⛔ Files ignored due to path filters (4)
  • test/components/__snapshots__/Select-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/Select.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/SelectMenu-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/SelectMenu.spec.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (4)
  • docs/content/docs/2.components/select.md
  • src/runtime/components/Select.vue
  • src/runtime/components/SelectMenu.vue
  • src/theme/select.ts

Comment thread src/runtime/components/SelectMenu.vue Outdated
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 16, 2026

npm i https://pkg.pr.new/@nuxt/ui@6358

commit: 7fd1ecb

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/runtime/components/Select.vue (2)

333-333: Optional: replace the single-element v-for with a computed.

Using v-for over [displayValue(modelValue)] just to create a template-scoped displayedModelValue works, but it's an unusual idiom and :key will be undefined when the display value is nullish (Vue will issue a dev warning). A plain computed (e.g. const displayedModelValue = computed(() => displayValue(modelValue ...))) or a <script setup> helper would be clearer and avoid the key-on-undefined case. Since modelValue here comes from SelectRoot's default slot, if you prefer to keep it inline, dropping the :key would be enough to silence the warning.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/components/Select.vue` at line 333, The template uses a
single-element v-for to create displayedModelValue which causes an unnecessary
pattern and a potential undefined :key warning; instead, add a computed
displayedModelValue (e.g. in the component's script or <script setup> compute
displayedModelValue = computed(() => displayValue(modelValue as any))) and
replace the v-for template with a normal binding to that computed, or if you
want the minimal change simply remove the :key from the template line; update
references to use displayedModelValue and keep displayValue and modelValue from
SelectRoot as before.

184-188: Optional: drop the as any cast on defaultVariants lookup.

The fallback chain is correct, but (appConfig.ui?.select as any)?.defaultVariants?.position forfeits type safety. Since Select (the ComponentConfig alias) already models the theme's variants, consider narrowing via the Select['variants']['position'] union rather than any, or declare a small local type so misuse surfaces at compile time.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/components/Select.vue` around lines 184 - 188, The lookup for the
default position uses an `as any` cast which loses type safety; update the
expression that computes `position` (the computed `position` and the
`contentProps` toRef usage) to narrow the type of
`appConfig.ui?.select?.defaultVariants` instead of using `any` — e.g. declare a
local type alias matching the Select component variants (or use the existing
`Select['variants']['position']` / `ComponentConfig` alias) and use that type
when accessing `.defaultVariants?.position` so the fallback chain remains the
same but the compiler catches invalid variant names.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/runtime/components/Select.vue`:
- Line 339: The template currently uses the literal string '&nbsp;' which Vue
will HTML-escape and render as the text "&nbsp;"; update the expression so it
returns an actual non-breaking space character instead (use the Unicode
codepoint, e.g. '\u00A0') so the placeholder preserves layout. Replace the
interpolated fallback in the Select.vue template (the expression using
displayedModelValue and placeholder) to use the actual NBSP (placeholder ??
'\u00A0') so displayedModelValue ?? (placeholder ?? '\u00A0') renders a real
non-breaking space when both are nullish.

---

Nitpick comments:
In `@src/runtime/components/Select.vue`:
- Line 333: The template uses a single-element v-for to create
displayedModelValue which causes an unnecessary pattern and a potential
undefined :key warning; instead, add a computed displayedModelValue (e.g. in the
component's script or <script setup> compute displayedModelValue = computed(()
=> displayValue(modelValue as any))) and replace the v-for template with a
normal binding to that computed, or if you want the minimal change simply remove
the :key from the template line; update references to use displayedModelValue
and keep displayValue and modelValue from SelectRoot as before.
- Around line 184-188: The lookup for the default position uses an `as any` cast
which loses type safety; update the expression that computes `position` (the
computed `position` and the `contentProps` toRef usage) to narrow the type of
`appConfig.ui?.select?.defaultVariants` instead of using `any` — e.g. declare a
local type alias matching the Select component variants (or use the existing
`Select['variants']['position']` / `ComponentConfig` alias) and use that type
when accessing `.defaultVariants?.position` so the fallback chain remains the
same but the compiler catches invalid variant names.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: aa15c55a-8136-4d7d-8223-186684ee4347

📥 Commits

Reviewing files that changed from the base of the PR and between 5082cf6 and 3be3001.

⛔ Files ignored due to path filters (4)
  • test/components/__snapshots__/Select-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/Select.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/SelectMenu-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/SelectMenu.spec.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (6)
  • .github/contributing/documentation.md
  • AGENTS.md
  • docs/content/docs/2.components/select.md
  • playgrounds/nuxt/app/pages/components/select.vue
  • src/runtime/components/Select.vue
  • src/theme/select.ts
✅ Files skipped from review due to trivial changes (2)
  • playgrounds/nuxt/app/pages/components/select.vue
  • docs/content/docs/2.components/select.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/theme/select.ts

Comment thread src/runtime/components/Select.vue
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/runtime/components/Select.vue (1)

339-339: ⚠️ Potential issue | 🟠 Major

'&nbsp;' still renders as literal text.

Vue's {{ }} interpolation HTML-escapes output, so this will render the literal 6-character string &nbsp; rather than a non-breaking space when both displayedModelValue and placeholder are nullish. Use '\u00A0' instead.

🐛 Proposed fix
-            {{ displayedModelValue ?? (placeholder ?? '&nbsp;') }}
+            {{ displayedModelValue ?? (placeholder ?? '\u00A0') }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/components/Select.vue` at line 339, The template currently uses
the literal string '&nbsp;' inside interpolation which Vue escapes and renders
as the six-character sequence; update the interpolation expression in Select.vue
so when both displayedModelValue and placeholder are nullish it returns a real
non-breaking space (use '\u00A0') instead of '&nbsp;'; locate the template
expression that references displayedModelValue and placeholder and replace the
fallback value accordingly so the rendered output contains a true NBSP.
🧹 Nitpick comments (1)
src/runtime/components/Select.vue (1)

295-303: viewportRef unwrapping looks correct, but consider typing the template ref.

The '$el' in instance check correctly handles both branches of <component :is>: when SelectViewport is rendered the ref holds a component instance (unwrap $el), and when the plain div is rendered the ref already holds the HTMLElement. One small improvement — typing useTemplateRef<HTMLElement | ComponentPublicInstance>('viewportRef') would remove the need for the runtime shape check and make the intent explicit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/components/Select.vue` around lines 295 - 303, The template ref
viewportRef should be explicitly typed to HTMLElement | ComponentPublicInstance
to avoid the runtime "$el" shape check; update the useTemplateRef call to
useTemplateRef<HTMLElement | ComponentPublicInstance>('viewportRef') and then
simplify the defineExpose viewportRef to just return viewportRef.value (or a
toRef wrapper) so the unwrap logic and "$el" in instance check can be removed;
keep triggerRef exposure unchanged (triggerRef in defineExpose) and ensure
imports/types for ComponentPublicInstance are present if needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/runtime/components/Select.vue`:
- Line 356: The Select component currently conditionally renders SelectViewport
based on isItemAligned which breaks the required scrolling container when
position='popper'; replace the conditional <component :is="isItemAligned ?
SelectViewport : 'div'"> usage with an unconditional SelectViewport element
using the existing ref="viewportRef" and props (e.g., :class="ui.viewport({
class: uiProp?.viewport })") so the scrolling container is always present; leave
the positioning control to SelectContent's position prop and remove any logic
that swaps to a plain div around SelectViewport or relies on isItemAligned for
structural rendering.

---

Duplicate comments:
In `@src/runtime/components/Select.vue`:
- Line 339: The template currently uses the literal string '&nbsp;' inside
interpolation which Vue escapes and renders as the six-character sequence;
update the interpolation expression in Select.vue so when both
displayedModelValue and placeholder are nullish it returns a real non-breaking
space (use '\u00A0') instead of '&nbsp;'; locate the template expression that
references displayedModelValue and placeholder and replace the fallback value
accordingly so the rendered output contains a true NBSP.

---

Nitpick comments:
In `@src/runtime/components/Select.vue`:
- Around line 295-303: The template ref viewportRef should be explicitly typed
to HTMLElement | ComponentPublicInstance to avoid the runtime "$el" shape check;
update the useTemplateRef call to useTemplateRef<HTMLElement |
ComponentPublicInstance>('viewportRef') and then simplify the defineExpose
viewportRef to just return viewportRef.value (or a toRef wrapper) so the unwrap
logic and "$el" in instance check can be removed; keep triggerRef exposure
unchanged (triggerRef in defineExpose) and ensure imports/types for
ComponentPublicInstance are present if needed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 10d705a6-547c-4c48-a195-e62f2419ac0c

📥 Commits

Reviewing files that changed from the base of the PR and between 3be3001 and 7fd1ecb.

📒 Files selected for processing (1)
  • src/runtime/components/Select.vue

Comment thread src/runtime/components/Select.vue
@benjamincanac benjamincanac merged commit 255807a into v4 Apr 17, 2026
22 checks passed
@benjamincanac benjamincanac deleted the fix/select-item-aligned-position branch April 17, 2026 09:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v4 #4488

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant