Skip to content

Conversation

@sadeghbarati
Copy link
Collaborator

@sadeghbarati sadeghbarati commented Dec 9, 2025

To follow the radix-ui Select component code for touch devices


#2239 flicker fix is from here (Cherry-picked, Co-Authored applied, but there is no avatar)
#1472

Summary by CodeRabbit

  • New Features

    • Added forceMount option to control component mounting behavior.
  • Bug Fixes

    • Improved touch device interaction handling to prevent unintended selections.
    • Enhanced focus management to activate only when the menu is open.
  • Style

    • Added close and closed state animations for smoother menu transitions.

✏️ Tip: You can customize this high-level summary in your review settings.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Dec 9, 2025

Open in StackBlitz

npm i https://pkg.pr.new/reka-ui@2329

commit: 5f07e30

@sadeghbarati
Copy link
Collaborator Author

sadeghbarati commented Dec 9, 2025

Screen_Recording_20251210_013143_Chrome.mp4
Desktop

Screen.Recording.2025-12-10.at.1.45.21.AM.mov

@sadeghbarati
Copy link
Collaborator Author

Please also help me again update the test files, cause I lowkey don't like tests 😁

@zernonia
Copy link
Member

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jan 13, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link

coderabbitai bot commented Jan 13, 2026

📝 Walkthrough

Walkthrough

The changes refactor the Select component's pointer and touch interaction handling across multiple files by introducing device-type awareness (mouse vs. touch/pen). The SelectContent component is simplified by removing reactive helpers and adding a forceMount prop. Focus trapping behavior is improved, and demo stories are updated with new UI components and animation states.

Changes

Cohort / File(s) Summary
Pointer-Type Interaction Handling
packages/core/src/Select/SelectContentImpl.vue, packages/core/src/Select/SelectItem.vue, packages/core/src/Select/SelectTrigger.vue
Added pointerTypeRef tracking to differentiate mouse from touch/pen inputs across trigger, item, and content components. Implemented conditional event handling: mouse interactions trigger on pointerdown; touch/pen interactions trigger on click. SelectContentImpl also binds focus trap to rootContext.open.value for proper focus management.
SelectContent Reactivity Simplification
packages/core/src/Select/SelectContent.vue
Removed computed/watch reactive helpers and render-time state toggling. Introduced public forceMount prop to control mounting behavior. Simplified Presence usage with direct conditional rendering based on forceMount || rootContext.open.value.
Demo Story Updates
packages/core/src/Select/story/SelectDemo.story.vue
Removed SelectArrow export/usage and Transition wrapper. Added SelectScrollUpButton and SelectItemIndicator components. Expanded structure to include Vegetables section with grouped items and indicators.
Animation State Classes
packages/core/src/Select/story/SelectForm.story.vue
Added data-[state=close]:animate-[slide-in_150ms_cubic-bezier(0.16,1,0.3,1)] and data-[state=closed]:animate-scaleOut classes to SelectContent for close/closed state animations.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant SelectTrigger
    participant SelectItem
    participant SelectContent
    participant FocusScope

    Note over User,FocusScope: Mouse Interaction Flow
    User->>SelectTrigger: pointerdown (mouse)
    SelectTrigger->>SelectTrigger: set pointerTypeRef = 'mouse'<br/>call handleOpen
    SelectTrigger->>SelectContent: open menu
    User->>SelectItem: pointermove (mouse)
    SelectItem->>SelectItem: focus item
    User->>SelectItem: pointerup (mouse)
    SelectItem->>SelectItem: trigger selection sync
    SelectTrigger->>SelectContent: close menu

    Note over User,FocusScope: Touch Interaction Flow
    User->>SelectTrigger: pointerdown (touch)
    SelectTrigger->>SelectTrigger: set pointerTypeRef = 'touch'<br/>skip open on pointerdown
    User->>SelectTrigger: click (touch)
    SelectTrigger->>SelectContent: open menu
    SelectContent->>FocusScope: bind trapped=true
    User->>SelectItem: click (touch)
    SelectItem->>SelectItem: trigger selection sync
    SelectTrigger->>SelectContent: close menu
    SelectContent->>FocusScope: bind trapped=false
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A touch of logic, a pointer's guide,
Mouse and fingers side by side,
Focus trapped where menus gleam,
Selection flows in seamless stream.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes across all files, which focus on improving touch and pointer event handling in the Select component through device-aware interaction logic.
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.


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

Copy link

@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: 3

🤖 Fix all issues with AI agents
In @packages/core/src/Select/SelectItem.vue:
- Around line 174-179: The click handler is unsafely casting MouseEvent to
PointerEvent; update the signature of handleSelectCustomEvent to accept (event:
MouseEvent | PointerEvent) and remove the cast in the @click handler, then audit
all calls to handleSelectCustomEvent to ensure any PointerEvent-only properties
are accessed behind type guards (e.g., narrow by checking 'pointerId' in the
event) so both event types are handled safely while keeping pointerTypeRef logic
intact.

In @packages/core/src/Select/SelectTrigger.vue:
- Around line 76-88: The click handler in SelectTrigger.vue is incorrectly typed
as PointerEvent; change the event parameter type to MouseEvent in the @click
handler and update any related casts to HTMLElement accordingly (refer to the
@click handler, pointerTypeRef and handleOpen identifiers), and make the
analogous change in SelectItem.vue where an event is cast (replace PointerEvent
with MouseEvent for that cast) so the runtime types match the declared types.

In @packages/core/src/Select/story/SelectForm.story.vue:
- Around line 60-61: The SelectContent element has a typo in its state selector:
it uses data-[state=close] which never matches the component's data-state (which
is 'open' or 'closed'); update the class on the SelectContent JSX/Template (the
element named SelectContent in this diff) to replace data-[state=close]:... with
data-[state=closed]:... so the
animate-[slide-in_150ms_cubic-bezier(0.16,1,0.3,1)] rule applies when
SelectContent's state is closed.
🧹 Nitpick comments (1)
packages/core/src/Select/story/SelectDemo.story.vue (1)

114-123: Consider removing unused CSS transition styles.

The .v-enter-active, .v-leave-active, .v-enter-from, and .v-leave-to classes were used with the <Transition> wrapper that has been removed. These styles are now dead code unless there's a plan to reintroduce transitions.

Remove unused styles
 </template>
-
-<style scoped>
-.v-enter-active,
-.v-leave-active {
-  transition: opacity .3s ease;
-}
-
-.v-enter-from,
-.v-leave-to {
-  opacity: 0;
-}
-</style>
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4022163 and 5f07e30.

📒 Files selected for processing (6)
  • packages/core/src/Select/SelectContent.vue
  • packages/core/src/Select/SelectContentImpl.vue
  • packages/core/src/Select/SelectItem.vue
  • packages/core/src/Select/SelectTrigger.vue
  • packages/core/src/Select/story/SelectDemo.story.vue
  • packages/core/src/Select/story/SelectForm.story.vue
🔇 Additional comments (11)
packages/core/src/Select/SelectTrigger.vue (3)

26-28: LGTM on pointer type tracking initialization.

The pointerTypeRef initialization to 'touch' is a safe default that ensures non-mouse behavior until a pointer event explicitly sets the type. The eslint disable is appropriate since the variable must be reassigned.


103-109: Good implementation of device-aware pointer handling.

The logic correctly differentiates between mouse and touch/pen devices:

  • Mouse: opens on pointerdown with left button, no ctrl key
  • Touch/Pen: defers to click handler to avoid opening on scroll

This aligns with radix-ui's approach for handling touch devices appropriately.


116-117: Defensive null check on getItems() is appropriate.

The ?? [] fallback ensures handleTypeaheadSearch won't fail if getItems() returns null/undefined.

packages/core/src/Select/SelectItem.vue (4)

70-72: Consistent pointer type tracking across components.

The initialization pattern matches SelectTrigger.vue, ensuring consistent behavior. Defaulting to 'touch' is safe as it prevents mouse-specific behavior until confirmed.


73-91: Good refactor: async handlers converted to synchronous.

Removing async/await and nextTick from these handlers reduces unnecessary overhead. The early return on ev.defaultPrevented is correctly preserved.


180-190: Device-aware selection logic is well-implemented.

The split behavior is correct:

  • pointerup for mouse enables drag-to-select (pointer down on trigger, drag to item, release to select)
  • click for touch/pen prevents accidental selection during scroll

The pointerdown handler correctly captures the pointer type before other events fire.


93-108: Good refinement: Focus only on mouse pointer move.

Restricting focus behavior to mouse interactions prevents scroll interference on touch devices. The comment correctly notes Safari's limitation with preventScroll.

packages/core/src/Select/SelectContentImpl.vue (2)

140-160: Well-implemented touch-aware pointer handling.

The touch device flow correctly:

  1. Tracks pointer type on pointermove to catch sliding interactions
  2. Exits early for touch on pointerup without checking movement threshold
  3. Cleans up the pointermove listener and resets triggerPointerDownPosRef

This prevents the drag-distance threshold check from interfering with touch selection, which is handled via click in SelectItem.vue.


282-282: Focus trap correctly bound to open state.

Binding :trapped="rootContext.open.value" ensures focus is only trapped while the select is open, preventing focus issues when closed.

packages/core/src/Select/story/SelectDemo.story.vue (1)

55-106: Good demo update showing the refactored Select component.

The restructured demo now demonstrates:

  • Both item-aligned and popper positions
  • Multiple groups (Fruits and Vegetables)
  • Scroll buttons, separators, and item indicators
  • Disabled item handling

This provides better coverage for testing the touch/pointer improvements.

packages/core/src/Select/SelectContent.vue (1)

43-62: Clean refactor of presence/rendering logic.

The updated pattern is cleaner:

  1. Presence with forceMount prop always renders its slot
  2. present prop passed to Presence is computed from forceMount || rootContext.open.value
  3. Slot receives { present } from the usePresence hook, which tracks animation state
  4. SelectContentImpl renders only when present
  5. Teleport handles the closed state to preserve slot content in a fragment

This enables better integration with Vue animation libraries while maintaining proper content lifecycle.

Comment on lines +174 to +179
@click="(event: MouseEvent) => {
// Open on click when using a touch or pen device
if (pointerTypeRef !== 'mouse') {
handleSelectCustomEvent(event as unknown as PointerEvent)
}
}"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Unsafe type cast from MouseEvent to PointerEvent.

The click event provides a MouseEvent, which is cast to PointerEvent. While this works at runtime because handleSelectCustomEvent only accesses common properties, the cast is technically incorrect and could cause issues if the handler later accesses PointerEvent-specific properties like pointerId or pressure.

Suggested fix: Update function signature to accept both types
-function handleSelectCustomEvent(ev: PointerEvent | KeyboardEvent) {
+function handleSelectCustomEvent(ev: PointerEvent | MouseEvent | KeyboardEvent) {
   if (ev.defaultPrevented)
     return
 
   const eventDetail = { originalEvent: ev, value: props.value as T }
   handleAndDispatchCustomEvent(SELECT_SELECT, handleSelect, eventDetail)
 }

Then remove the cast:

       @click="(event: MouseEvent) => {
         // Open on click when using a touch or pen device
         if (pointerTypeRef !== 'mouse') {
-          handleSelectCustomEvent(event as unknown as PointerEvent)
+          handleSelectCustomEvent(event)
         }
       }"
🤖 Prompt for AI Agents
In @packages/core/src/Select/SelectItem.vue around lines 174 - 179, The click
handler is unsafely casting MouseEvent to PointerEvent; update the signature of
handleSelectCustomEvent to accept (event: MouseEvent | PointerEvent) and remove
the cast in the @click handler, then audit all calls to handleSelectCustomEvent
to ensure any PointerEvent-only properties are accessed behind type guards
(e.g., narrow by checking 'pointerId' in the event) so both event types are
handled safely while keeping pointerTypeRef logic intact.

Comment on lines 76 to +88
@click="
(event: MouseEvent) => {
(event: PointerEvent) => {
// Whilst browsers generally have no issue focusing the trigger when clicking
// on a label, Safari seems to struggle with the fact that there's no `onClick`.
// We force `focus` in this case. Note: this doesn't create any other side-effect
// because we are preventing default in `onPointerDown` so effectively
// this only runs for a label 'click'
(event?.currentTarget as HTMLElement)?.focus();
// Open on click when using a touch or pen device
if (pointerTypeRef !== 'mouse') {
handleOpen(event);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Type mismatch: click event is MouseEvent, not PointerEvent.

The @click handler receives a MouseEvent, but it's typed as PointerEvent. While both have pageX/pageY properties, the type annotation is incorrect. Additionally, the cast at line 177 in SelectItem.vue has a similar pattern.

Suggested fix
       @click="
-        (event: PointerEvent) => {
+        (event: MouseEvent) => {
           // Whilst browsers generally have no issue focusing the trigger when clicking
           // on a label, Safari seems to struggle with the fact that there's no `onClick`.
           // We force `focus` in this case. Note: this doesn't create any other side-effect
           // because we are preventing default in `onPointerDown` so effectively
           // this only runs for a label 'click'
           (event?.currentTarget as HTMLElement)?.focus();

           // Open on click when using a touch or pen device
           if (pointerTypeRef !== 'mouse') {
             handleOpen(event);
           }
         }
       "
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@click="
(event: MouseEvent) => {
(event: PointerEvent) => {
// Whilst browsers generally have no issue focusing the trigger when clicking
// on a label, Safari seems to struggle with the fact that there's no `onClick`.
// We force `focus` in this case. Note: this doesn't create any other side-effect
// because we are preventing default in `onPointerDown` so effectively
// this only runs for a label 'click'
(event?.currentTarget as HTMLElement)?.focus();
// Open on click when using a touch or pen device
if (pointerTypeRef !== 'mouse') {
handleOpen(event);
}
@click="
(event: MouseEvent) => {
// Whilst browsers generally have no issue focusing the trigger when clicking
// on a label, Safari seems to struggle with the fact that there's no `onClick`.
// We force `focus` in this case. Note: this doesn't create any other side-effect
// because we are preventing default in `onPointerDown` so effectively
// this only runs for a label 'click'
(event?.currentTarget as HTMLElement)?.focus();
// Open on click when using a touch or pen device
if (pointerTypeRef !== 'mouse') {
handleOpen(event);
}
🤖 Prompt for AI Agents
In @packages/core/src/Select/SelectTrigger.vue around lines 76 - 88, The click
handler in SelectTrigger.vue is incorrectly typed as PointerEvent; change the
event parameter type to MouseEvent in the @click handler and update any related
casts to HTMLElement accordingly (refer to the @click handler, pointerTypeRef
and handleOpen identifiers), and make the analogous change in SelectItem.vue
where an event is cast (replace PointerEvent with MouseEvent for that cast) so
the runtime types match the declared types.

Comment on lines 60 to +61
<SelectContent
class="min-w-[160px] bg-white overflow-hidden rounded shadow-[0px_10px_38px_-10px_rgba(22,_23,_24,_0.35),_0px_10px_20px_-15px_rgba(22,_23,_24,_0.2)] will-change-[opacity,transform] data-[side=top]:animate-slideDownAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade data-[side=left]:animate-slideRightAndFade"
class="min-w-[160px] bg-white overflow-hidden rounded shadow-[0px_10px_38px_-10px_rgba(22,_23,_24,_0.35),_0px_10px_20px_-15px_rgba(22,_23,_24,_0.2)] will-change-[opacity,transform] data-[side=top]:animate-slideDownAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade data-[side=left]:animate-slideRightAndFade data-[state=close]:animate-[slide-in_150ms_cubic-bezier(0.16,1,0.3,1)] data-[state=closed]:animate-scaleOut"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Check what data-state values are used in the Select component
rg -n "data-state" packages/core/src/Select/ -A 2 -B 2

Repository: unovue/reka-ui

Length of output: 2290


🏁 Script executed:

# Also check SelectContentImpl.vue specifically around line 321
cat -n packages/core/src/Select/SelectContentImpl.vue | sed -n '315,330p'

Repository: unovue/reka-ui

Length of output: 710


🏁 Script executed:

# Verify the exact context in SelectForm.story.vue
cat -n packages/core/src/Select/story/SelectForm.story.vue | sed -n '55,70p'

Repository: unovue/reka-ui

Length of output: 1122


Fix typo: data-[state=close] should be data-[state=closed]

The data-state attribute is set to either 'open' or 'closed' (see SelectContentImpl.vue line 321). The class selector data-[state=close]:animate-[slide-in_150ms_cubic-bezier(0.16,1,0.3,1)] will never match. This should be data-[state=closed] to match the valid state value.

🤖 Prompt for AI Agents
In @packages/core/src/Select/story/SelectForm.story.vue around lines 60 - 61,
The SelectContent element has a typo in its state selector: it uses
data-[state=close] which never matches the component's data-state (which is
'open' or 'closed'); update the class on the SelectContent JSX/Template (the
element named SelectContent in this diff) to replace data-[state=close]:... with
data-[state=closed]:... so the
animate-[slide-in_150ms_cubic-bezier(0.16,1,0.3,1)] rule applies when
SelectContent's state is closed.

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.

3 participants