Skip to content

Conversation

@discreted66
Copy link
Collaborator

@discreted66 discreted66 commented Nov 27, 2025

PR

feat:select组件重构

PR Checklist

Please check if your PR fulfills the following requirements:

  • The commit message follows our Commit Message Guidelines
  • Tests for the changes have been added (for bug fixes / features)
  • Docs have been added / updated (for bug fixes / features)

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • Other... Please describe:

What is the current behavior?

Issue Number: N/A

What is the new behavior?

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

Summary by CodeRabbit

  • New Features

    • Grid select now triggers an automatic remote search when its dropdown is first opened.
    • Select now delegates rendering to an adaptive wrapper that chooses the appropriate select variant.
  • Bug Fixes

    • Input sizing guards to prevent invalid width/max-width calculations.
    • Loading icon display logic corrected.
    • Robust handling for tree select with lazy-loaded data to avoid empty-selection issues.
  • Style

    • Updated default dropdown icon.
    • Increased option label top padding and enabled horizontal checkbox layout.

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

@coderabbitai
Copy link

coderabbitai bot commented Nov 27, 2025

Walkthrough

This PR updates many Playwright test selectors to new base-select class names and page-level locators, changes a default dropdown icon fallback, enhances grid-select to auto-trigger remote search on first panel show, adds a select-wrapper renderless module, refactors the Select PC component into a dynamic wrapper, improves tree-select guards, and adds small style tweaks.

Changes

Cohort / File(s) Summary
Test selector updates
examples/sites/demos/pc/app/select/automatic-dropdown.spec.ts, examples/sites/demos/pc/app/select/copy-multi.spec.ts, examples/sites/demos/pc/app/select/events.spec.ts, examples/sites/demos/pc/app/select/input-box-type.spec.ts, examples/sites/demos/pc/app/select/nest-grid-remote.spec.ts, examples/sites/demos/pc/app/select/nest-grid.spec.ts, examples/sites/demos/pc/app/select/nest-radio-grid-much-data.spec.ts, examples/sites/demos/pc/app/select/nest-tree.spec.ts, examples/sites/demos/pc/app/select/popup-style-position.spec.ts, examples/sites/demos/pc/app/select/remote-method.spec.ts
Updated Playwright selectors to use tiny-base-select__* classes and page-level locators; replaced some nested locators with page.getByRole(...).getByText(...); adjusted a few timing/wait steps and element selections for closing/clearing.
Icon fallback
packages/renderless/src/base-select/index.ts
Changed default dropdown icon fallback from icon-delta-down to icon-down-ward.
Grid-select: visibility/auto-search & config
packages/renderless/src/grid-select/index.ts, packages/renderless/src/grid-select/vue.ts
Added firstAutoSearch state and changed handleVisibleChange signature to accept props; when panel becomes visible and autoSearch is enabled, call api.filter('') once; improved checkRowKeys extraction and removed initQuery from public API.
Select wrapper (renderless)
packages/renderless/src/select-wrapper/vue.ts
New renderless factory renderless and public API list (state, resolvedComponent, mergedProps, listeners, slotNames, hasScopedDefault, focus, blur); implements dynamic child resolution, merged props, slot detection, and a proxy state delegating to child ref.
Tree-select robustness
packages/renderless/src/tree-select/vue.ts, packages/renderless/src/tree-select/index.ts
Added resolveTreeData normalization and watchers guarding non-array/treeOp shapes; added lazy-load guards and safer handling when data/options are missing.
Select PC refactor → wrapper-based
packages/vue/src/select/src/pc.vue
Replaced large inline PC template with dynamic wrapper rendering resolved child components (TinyBaseSelect, TinyTreeSelect, TinyGridSelect); consolidated and expanded props surface; setup now accepts injected components via extendOptions.
Base-select input guards & loading icon
packages/vue/src/base-select/src/pc.vue
Guard width/max-width calculations to use auto/none when state.inputWidth <= 0; adjusted loading icon rendering with v-else to avoid duplicate renders.
Mobile option icon class binding
packages/vue/src/select/src/mobile-first.vue
Refactored class binding into array form (static + conditional) for readability; behavior unchanged.
Option styling tweaks
packages/theme/src/option/index.less
Added padding-top: 3px for option label container and display: flex for checkbox wrap container.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Wrapper as SelectWrapper
    participant Renderless
    participant Child as ChildComponent

    User->>Wrapper: Mount/select props
    Wrapper->>Renderless: renderless.setup(props, context, extendOptions)
    Renderless->>Renderless: determine resolvedComponent (Base/Tree/Grid)
    Renderless-->>Wrapper: resolvedComponent, mergedProps, listeners
    Wrapper->>Child: render dynamic component with mergedProps & listeners
    User->>Child: interact (open/select)
    Child->>Child: update internal state / emit
    Child-->>Wrapper: emit via listeners
    Wrapper-->>User: propagate events
Loading
sequenceDiagram
    participant User
    participant GridPanel as GridSelectPanel
    participant HandleVis as handleVisibleChange
    participant API as RemoteAPI
    participant State

    User->>GridPanel: open panel (visible=true)
    GridPanel->>HandleVis: visible change
    HandleVis->>State: check firstAutoSearch && props.remoteConfig?.autoSearch
    alt first auto-search needed
        HandleVis->>API: api.filter('')  %% trigger remote search
        API-->>State: results
        State->>State: set firstAutoSearch = false
    end
    HandleVis-->>GridPanel: finished
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~35 minutes

  • Areas needing extra attention:
    • packages/vue/src/select/src/pc.vue — dynamic resolution, prop forwarding, slot forwarding, and component injection correctness
    • packages/renderless/src/select-wrapper/vue.ts — proxy state behavior, child ref access, and focus/blur plumbing
    • packages/renderless/src/grid-select/* — handleVisibleChange signature change, firstAutoSearch lifecycle and remote call semantics
    • Playwright test files — verify updated selectors match rendered DOM/classes

Possibly related PRs

Suggested reviewers

  • zzcr
  • kagol

Poem

🐰 Selects reborn with classes new,
I hopped and refactored just for you.
Grid now searches when you peek,
Trees safer if their data's meek,
A wrapper hums — components queue. 🥕

Pre-merge checks and finishing touches

✅ 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 change: a reconstruction of select-related components (select, tree-select, grid-select, base-select).
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

@github-actions github-actions bot added the enhancement New feature or request (功能增强) label Nov 27, 2025
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: 2

🧹 Nitpick comments (4)
packages/theme/src/option/index.less (1)

95-95: Consider extracting hardcoded padding value to a CSS variable.

The padding-top: 3px is hardcoded directly in the stylesheet. For consistency with the component's theming approach (which uses CSS variables extensively throughout), consider defining this value as a reusable variable.

Current approach uses variables for other properties (e.g., var(--tv-Option-padding), var(--tv-Option-font-size)), so this pixel value stands out.

Example: Define --tv-Option-label-padding-top: 3px; in the component's CSS variable definitions (likely in vars.less) and use it here:

  .@{option-prefix-cls}-label {
    display: inline-block;
    width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    vertical-align: top;
-   padding-top: 3px;
+   padding-top: var(--tv-Option-label-padding-top);
  }
examples/sites/demos/pc/app/select/copy-multi.spec.ts (1)

39-44: Consider the fragility of page-level text-based locators.

The locator strategy has shifted from a local, specific selector (wrap.locator('.tiny-select').nth(1)) to a page-level text filter (page.locator('div').filter({ hasText: /^北京上海$/ }).first()). While this may work with the current DOM structure, text-based filters are generally more fragile and could match unintended elements if the page structure changes.

Consider whether a more specific selector (e.g., combining a class with the text filter, or using a data-testid attribute) would be more robust.

Alternative approach:

-  const select = page
-    .locator('div')
-    .filter({ hasText: /^北京上海$/ })
-    .first()
+  const select = wrap.locator('.tiny-select').nth(1)

Or add a test-specific attribute to the component for more reliable targeting.

examples/sites/demos/pc/app/select/automatic-dropdown.spec.ts (1)

26-26: Note the locator strategy inconsistency.

Line 26 now uses page.getByRole('list').getByText('上海') for clicking the option, while Line 30 still uses page.getByRole('listitem').filter({ hasText: '上海' }) for verification. This inconsistency suggests the two locators may target elements differently.

Consider aligning both to use the same strategy for consistency, or verify that the different approaches are intentional and both correctly target the desired elements.

-  await page.getByRole('list').getByText('上海').click()
+  await page.getByRole('listitem').filter({ hasText: '上海' }).click()

Or update Line 30 to match the new approach if the list-level locator is preferred.

packages/renderless/src/grid-select/vue.ts (1)

32-32: Consider adding explicit typing for the api object.

The any type removes type safety benefits. If the api structure is stable, consider defining an interface for it.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 94f7b91 and 8b63954.

📒 Files selected for processing (19)
  • examples/sites/demos/pc/app/select/automatic-dropdown.spec.ts (1 hunks)
  • examples/sites/demos/pc/app/select/copy-multi.spec.ts (1 hunks)
  • examples/sites/demos/pc/app/select/events.spec.ts (2 hunks)
  • examples/sites/demos/pc/app/select/input-box-type.spec.ts (1 hunks)
  • examples/sites/demos/pc/app/select/nest-grid-remote.spec.ts (5 hunks)
  • examples/sites/demos/pc/app/select/nest-grid.spec.ts (3 hunks)
  • examples/sites/demos/pc/app/select/nest-radio-grid-much-data.spec.ts (1 hunks)
  • examples/sites/demos/pc/app/select/nest-tree.spec.ts (2 hunks)
  • examples/sites/demos/pc/app/select/popup-style-position.spec.ts (1 hunks)
  • examples/sites/demos/pc/app/select/remote-method.spec.ts (1 hunks)
  • packages/renderless/src/base-select/index.ts (1 hunks)
  • packages/renderless/src/grid-select/index.ts (1 hunks)
  • packages/renderless/src/grid-select/vue.ts (3 hunks)
  • packages/renderless/src/select-wrapper/vue.ts (1 hunks)
  • packages/renderless/src/tree-select/vue.ts (2 hunks)
  • packages/theme/src/option/index.less (2 hunks)
  • packages/vue/src/base-select/src/pc.vue (2 hunks)
  • packages/vue/src/select/src/mobile-first.vue (1 hunks)
  • packages/vue/src/select/src/pc.vue (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
packages/renderless/src/base-select/index.ts (1)
examples/sites/src/tools/useTheme.js (1)
  • designConfig (53-62)
packages/renderless/src/grid-select/vue.ts (1)
packages/renderless/src/grid-select/index.ts (1)
  • handleVisibleChange (70-84)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: PR E2E Test (pnpm test:e2e3)
🔇 Additional comments (20)
packages/theme/src/option/index.less (1)

108-110: Verify flex display rule is comprehensive.

A new dedicated &__checkbox-wrap { display: flex; } rule is added here. This ensures the checkbox wrapper uses flex layout.

However, confirm whether additional flex properties should be included for alignment, such as align-items: center; or justify-content: center;, depending on the intended layout of checkbox content within the wrapper.

Please verify:

  1. Does the checkbox-wrap require only display: flex;, or should it also include alignment properties?
  2. Are there other select components (tree-select, grid-select) with similar checkbox-wrap styling that should remain consistent with this change?
examples/sites/demos/pc/app/select/nest-grid.spec.ts (1)

11-11: LGTM! Selector updates align with base-select refactoring.

The caret selector updates from the legacy naming to .tiny-base-select__caret are consistent across all three test cases and align with the broader component reconstruction mentioned in the PR objectives.

Also applies to: 34-34, 74-74

examples/sites/demos/pc/app/select/popup-style-position.spec.ts (1)

9-9: LGTM! Tags group selector updated correctly.

The selector update to .tiny-base-select__tags-group aligns with the new base class naming convention.

examples/sites/demos/pc/app/select/nest-radio-grid-much-data.spec.ts (1)

11-11: LGTM! Caret selector simplified and updated.

The selector update to .tiny-base-select__caret both simplifies the locator path and aligns with the new base class naming.

examples/sites/demos/pc/app/select/remote-method.spec.ts (1)

31-31: LGTM! Input selector updated for multi-select case.

The selector update to .tiny-base-select__input aligns with the new base class naming for the multi-select remote search scenario.

examples/sites/demos/pc/app/select/nest-tree.spec.ts (1)

11-11: LGTM! Tree select caret selectors updated.

The caret selector updates to .tiny-base-select__caret are consistent with the base-select refactoring and simplify the locator paths.

Also applies to: 35-35

packages/vue/src/select/src/mobile-first.vue (1)

454-458: LGTM! Improved class binding structure.

The refactor from string concatenation to array form is cleaner and more maintainable. The functionality remains the same—static classes in the first element, conditional classes in the second.

examples/sites/demos/pc/app/select/input-box-type.spec.ts (1)

63-63: LGTM! Selector updates align with component refactoring.

The test selectors have been correctly updated to use the new .tiny-base-select__* class naming convention, consistent with the component architecture changes across this PR.

Also applies to: 71-71

packages/vue/src/base-select/src/pc.vue (2)

253-254: LGTM! Defensive checks prevent invalid width calculations.

The added guards (state.inputWidth > 0) prevent potential division-by-zero or negative width issues when the input width hasn't been measured yet, falling back to sensible defaults ('auto' and 'none').


541-541: Minor template cleanup.

The v-else directive clarifies the conditional rendering logic for the loading icon.

packages/renderless/src/tree-select/vue.ts (1)

18-56: LGTM! Robust normalization for flexible tree data input.

The new resolveTreeData helper elegantly handles multiple input formats (array, object with data property, or empty), and the updated watcher correctly tracks reactive changes for both formats. The defensive Array.isArray check ensures type safety.

examples/sites/demos/pc/app/select/events.spec.ts (1)

29-30: LGTM! Test selectors updated for refactored component structure.

The selector changes from .tiny-select__caret.tiny-select__close to .tiny-input__suffix-inner correctly target the clear button in the new component architecture. The increased wait time (500ms → 1000ms) on line 30 likely accommodates longer rendering cycles in the wrapper-based architecture.

Also applies to: 68-68

packages/renderless/src/base-select/index.ts (1)

2010-2010: Icon name updated to standardized convention.

The fallback dropdown icon has been updated from 'icon-delta-down' to 'icon-down-ward', aligning with the icon naming standardization visible across the component refactor. The icon is properly imported and registered in the component.

examples/sites/demos/pc/app/select/nest-grid-remote.spec.ts (2)

12-12: LGTM! Test selectors correctly updated for base-select architecture.

All selector changes properly target the new .tiny-base-select__* class hierarchy, consistent with the component refactoring.

Also applies to: 48-48, 75-75, 97-97, 112-114


83-85: I'm unable to access the repository in the sandbox environment due to cloning restrictions. To verify the review comment's concerns, I need your help. Could you please provide:

  1. The full test file content (examples/sites/demos/pc/app/select/nest-grid-remote.spec.ts), or at minimum:

    • The complete test case containing lines 83-85
    • Any setup/beforeEach blocks
    • Related test cases
  2. Or search results from running this in your repository:

# Check for similar select click patterns in remote tests
rg -n -C8 "select.*click\(\)|\.click\(\).*await.*page\.waitFor" examples/sites/demos/pc/app/select/ --type=ts

# List all select test files to identify related tests
fd ".*select.*\.spec\.ts$" examples/sites/demos/pc/app/select/
  1. Or clarification on:
    • Was this click behavior added in this PR or was it pre-existing?
    • Are there other multi-select remote tests that follow the same pattern?
    • Has the multi-select component API changed recently?

With this information, I can verify whether this is intended behavior or a regression and provide a proper rewritten review comment.

packages/vue/src/select/src/pc.vue (1)

1-316: I'm unable to clone the repository to verify the architectural concerns directly. However, I can provide an assessment based on the code snippet provided and mark what requires manual verification.

Major architectural refactor: Requires thorough testing before merge.

This PR substantially rewrites the select component to use a compositional wrapper pattern. The code provided shows legitimate architectural changes, but key concerns require verification that I cannot perform without repository access.

Verified changes from code snippet:

  • Template reduced to dynamic component delegation with slot forwarding (lines 2-10)
  • Renderless wrapper pattern with runtime component selection
  • Props surface expanded from spread to explicit declarations
  • inheritAttrs: false enables proper attribute forwarding
  • Three child components dynamically composed

Concerns requiring verification:

  1. Backward compatibility: Cannot verify without checking existing component usage patterns and whether $refs access points have changed
  2. Performance impact: Cannot measure without access to performance profiling tools or benchmarks
  3. Error handling: No visible error boundaries in the template shown; requires validation that child component failures are gracefully handled
  4. Migration path: Cannot verify impact on existing code accessing select internals without searching the codebase
  5. Test coverage: Cannot confirm existing tests pass or new tests are added

Critical actions needed:

  • Run full test suite, especially integration tests for select component
  • Search codebase for direct $refs access to select internals and evaluate migration impact
  • Check if child component implementations (TinyBaseSelect, TinyTreeSelect, TinyGridSelect) properly handle the renderless wrapper pattern
  • Verify the renderless implementation correctly delegates to appropriate child based on props
packages/renderless/src/grid-select/vue.ts (2)

56-57: LGTM!

The additions of isMounted and firstAutoSearch to the state properly support the new automatic remote search functionality.


72-72: I encountered an issue cloning the repository. Let me request additional context to verify the concern about the initQuery removal.

To properly verify this concern, I need to see:

  1. The public API array in packages/renderless/src/grid-select/index.ts (lines 16-29) - to confirm initQuery was indeed removed
  2. The implementation to see if initQuery is still used internally
  3. Any documentation or exports that indicate whether initQuery was part of the public API contract

Could you provide:

  • The content of the public API array section from index.ts
  • Whether initQuery appears anywhere else in the grid-select module
  • Any git history or commit message explaining why initQuery was removed from the public API

Alternatively, if you can access the repository, please run:

# Search for initQuery usage in the entire codebase
grep -r "initQuery" packages/ --include="*.ts" --include="*.vue" --include="*.js"

# Check the public API exports
cat packages/renderless/src/grid-select/index.ts | head -50

This will help me determine if this removal is truly a breaking change or if initQuery was never part of the intended public API.

packages/renderless/src/grid-select/index.ts (2)

86-98: LGTM!

The enhanced checkRowKeys derivation logic provides robust handling for both regular arrays and Vue computed refs, preventing potential runtime errors from unexpected data structures.


70-84: I'm unable to clone the repository to perform automated verification. Let me provide a rewritten review comment based on code analysis and request manual verification where needed.

Verify auto-search implementation handles edge cases properly.

The new automatic remote search feature on panel open has several considerations:

  1. Unconventional condition check: The condition state.firstAutoSearch !== false is non-standard. Consider using state.firstAutoSearch === true or renaming to a boolean like hasPerformedFirstAutoSearch for clarity.

  2. Empty string query handling: Verify that all remoteMethod implementations correctly handle empty string queries (api.filter('')). This should be documented if intentional—empty strings may trigger different filtering logic than expected.

  3. Network request timing: This introduces a new network request when the panel first opens. Confirm this behavior is desired and doesn't conflict with existing data-loading patterns or cause duplicate requests in concurrent scenarios.

  4. State mutation: Ensure state.firstAutoSearch = false is the intended way to track this state. If this is shared across multiple instances, verify isolation and reset logic on component cleanup/remount.

Comment on lines +1 to +143
export const api = [
'state',
'resolvedComponent',
'mergedProps',
'listeners',
'slotNames',
'hasScopedDefault',
'focus',
'blur'
]

export const renderless = (props, { reactive, computed, useAttrs }, { constants, vm, components }) => {
const api = {}

const resolvedComponent = computed(() => computedResolvedComponent({ props, constants, vm, components }))

const mergedProps = computed(() => {
const runtimeAttrs = typeof useAttrs === 'function' ? useAttrs() : null
const attrs = runtimeAttrs || vm.$attrs || {}
const className = attrs.class
const classArray = Array.isArray(className)
? ['tiny-select', ...className]
: className
? ['tiny-select', className]
: ['tiny-select']

const { class: _omitClass, ...restAttrs } = attrs

return {
...props,
...restAttrs,
class: classArray
}
})

const slotNames = computed(() => Object.keys(vm.$slots || {}).filter((name) => name && name !== 'default'))

const hasScopedDefault = computed(() => {
const scoped = vm.$scopedSlots?.default
if (scoped && scoped.length) {
return true
}
const slot = vm.$slots?.default
return typeof slot === 'function' && slot.length > 0
})

const listeners = computed(() => {
if (vm.$listeners) {
return vm.$listeners
}
return {}
})

const getChildComponent = () => vm.$refs?.childComponent

// 暴露子组件的 state,让用户可以通过 ref.state 访问子组件的状态(如 cachedOptions)
// 使用 Proxy 代理子组件的 state,实现动态访问
const state = new Proxy(
{},
{
get(target, prop) {
const child = getChildComponent()
return child?.state?.[prop]
},
set(target, prop, value) {
const child = getChildComponent()
if (child?.state) {
child.state[prop] = value
return true
}
return false
},
has(target, prop) {
const child = getChildComponent()
return prop in (child?.state || {})
},
ownKeys(target) {
const child = getChildComponent()
return Object.keys(child?.state || {})
},
getOwnPropertyDescriptor(target, prop) {
const child = getChildComponent()
const childState = child?.state || {}
if (prop in childState) {
return {
enumerable: true,
configurable: true,
value: childState[prop]
}
}
return undefined
}
}
)

const focus = () => {
const child = getChildComponent()
child && typeof child.focus === 'function' && child.focus()
}

const blur = () => {
const child = getChildComponent()
child && typeof child.blur === 'function' && child.blur()
}

const hasData = (value: any) => {
if (!value) {
return false
}
if (Array.isArray(value)) {
return value.length > 0
}
if (typeof value === 'object') {
return Object.keys(value).length > 0
}
return true
}

const computedResolvedComponent = ({ props, constants, vm, components }) => {
// 优先使用传入的 components,否则从 vm.$options.components 获取
const comps = components || vm.$options?.components || {}

if (props.renderType === constants.TYPE.Tree || hasData(props.treeOp)) {
return comps.TinyTreeSelect || 'TinyTreeSelect'
}
if (props.renderType === constants.TYPE.Grid || hasData(props.gridOp)) {
return comps.TinyGridSelect || 'TinyGridSelect'
}
return comps.TinyBaseSelect || 'TinyBaseSelect'
}
Object.assign(api, {
state,
resolvedComponent,
mergedProps,
listeners,
slotNames,
hasScopedDefault,
focus,
blur
})

return api
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Script executed:

#!/bin/bash
# Check gridOp/treeOp usage to see if 0 or false are valid values
rg -n "gridOp\s*=|treeOp\s*=" --type=vue --type=ts -A 2 -B 2 packages/vue/src/select/ packages/renderless/src/select-wrapper/ 2>/dev/null | head -50

Repository: opentiny/tiny-vue


<shell_result>
Repository: opentiny/tiny-vue
Script execution failed with unrecoverable error: [CAPABILITY:RUN_SHELL_SCRIPT] Failed to clone repository opentiny/tiny-vue: Clone operation failed
</shell_result>


Script executed:

#!/bin/bash
# Also search more broadly for gridOp and treeOp assignments
rg -n "gridOp|treeOp" --type=vue --type=ts -C 2 packages/ | grep -E "(gridOp|treeOp)\s*[:=]" | head -30

Repository: opentiny/tiny-vue


<shell_result>
Repository: opentiny/tiny-vue
Script execution failed with unrecoverable error: [CAPABILITY:RUN_SHELL_SCRIPT] Failed to clone repository opentiny/tiny-vue: Clone operation failed
</shell_result>


Script executed:

#!/bin/bash
# Check if there are tests for the select-wrapper
fd -e "spec\|test" packages/renderless/src/select-wrapper/ -o packages/vue/src/select/ 2>/dev/null | head -20

Repository: opentiny/tiny-vue


<shell_result>
Repository: opentiny/tiny-vue
Script execution failed with unrecoverable error: [CAPABILITY:RUN_SHELL_SCRIPT] Failed to clone repository opentiny/tiny-vue: Clone operation failed
</shell_result>


Script executed:

#!/bin/bash
# Look at the actual child components to understand if state property is guaranteed
rg -n "state\s*=" packages/renderless/src/ --type=ts -A 1 -B 1 | grep -E "(TinyTreeSelect|TinyGridSelect|TinyBaseSelect)" | head -20

Repository: opentiny/tiny-vue


<shell_result>
Repository: opentiny/tiny-vue
Script execution failed with unrecoverable error: [CAPABILITY:RUN_SHELL_SCRIPT] Failed to clone repository opentiny/tiny-vue: Clone operation failed
</shell_result>


Script executed:

#!/bin/bash
# Check Vue class handling - whether duplicate classes are an issue
rg -n "class.*tiny-select" packages/ --type=vue --type=ts -C 1 | head -30

Repository: opentiny/tiny-vue


<shell_result>
Repository: opentiny/tiny-vue
Script execution failed with unrecoverable error: [CAPABILITY:RUN_SHELL_SCRIPT] Failed to clone repository opentiny/tiny-vue: Clone operation failed
</shell_result>


I'm unable to access the repository to run verification scripts due to a clone failure. However, I can provide a rewritten review based on static code analysis of the provided snippet.


Address three edge cases in renderless logic to improve robustness.

This wrapper elegantly delegates to dynamically selected child components, but has three correctness gaps:

Critical:

  • Lines 106-117 (hasData): Treats 0 and false as falsy returns, incorrectly triggering fallback behavior if gridOp/treeOp are legitimately 0 or false. Refactor to explicitly check for undefined/null:
    const hasData = (value: any) => {
      return value != null && (Array.isArray(value) ? value.length > 0 : typeof value === 'object' ? Object.keys(value).length > 0 : true)
    }

Important:

  • Lines 58-94 (State Proxy): Silently fails when child component lacks state property; no warning in development. All proxy traps return undefined or false instead of indicating a misconfiguration. Add a guard and warning on first access.

  • Lines 22-25 (Class merging): Always prepends 'tiny-select' even if present in input className, creating duplicates. Use Set or check before adding:

    const classArray = Array.from(new Set(['tiny-select', ...(Array.isArray(className) ? className : className ? [className] : [])]))
🤖 Prompt for AI Agents
In packages/renderless/src/select-wrapper/vue.ts around lines 22-25, 58-94 and
106-117: fix three robustness issues — (1) Class merging: avoid duplicate
"tiny-select" by normalizing incoming className to an array and only prepending
"tiny-select" when it's not already present (or use a Set) so duplicates are not
introduced. (2) State Proxy: add a guard that detects when the child component
has no state and, on first proxy access, emit a development-mode console.warn
(or use vm.$warn) describing the misconfiguration; ensure proxy traps return
safe defaults after the warning rather than silently failing. (3) hasData:
change the falsy check to explicitly treat only null/undefined as absent (i.e.,
return false if value is null/undefined), keep existing checks for
arrays/objects for emptiness, and allow legitimate values like 0 or false to be
treated as present.

Comment on lines +266 to +269
InputBoxType: {
type: String,
default: 'input',
validator: (value: string) => ['input', 'underline'].includes(value)
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

Typo: Prop name inconsistency.

Line 266 defines InputBoxType (capital I) but the rest of the codebase likely uses inputBoxType (lowercase). This could cause props not to bind correctly.

Apply this fix:

-    InputBoxType: {
+    inputBoxType: {
       type: String,
       default: 'input',
       validator: (value: string) => ['input', 'underline'].includes(value)
     },
📝 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
InputBoxType: {
type: String,
default: 'input',
validator: (value: string) => ['input', 'underline'].includes(value)
inputBoxType: {
type: String,
default: 'input',
validator: (value: string) => ['input', 'underline'].includes(value)
},
🤖 Prompt for AI Agents
In packages/vue/src/select/src/pc.vue around lines 266 to 269, the prop is
declared as InputBoxType (capital I) but the codebase expects inputBoxType
(camelCase); rename the prop declaration to inputBoxType, keep the type, default
and validator unchanged, and update any local references/usages in the
template/script to use inputBoxType so the prop binds correctly.

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: 0

♻️ Duplicate comments (2)
packages/renderless/src/select-wrapper/vue.ts (1)

17-33: Tighten class merging, state proxy guard, and hasData semantics (non-blocking).

These are minor robustness tweaks on otherwise solid wrapper logic:

  • Class merging: classArray always prepends 'tiny-select' and then appends any runtime class. If callers already include 'tiny-select', you’ll get duplicates. Not harmful, but you can easily de‑dup:

    const classArray = Array.from(
      new Set([
        'tiny-select',
        ...(Array.isArray(className) ? className : className ? [className] : [])
      ])
    )
  • State Proxy guard: When the child lacks a state field, all traps silently return undefined/false. Emitting a one-time dev warning in that case (e.g., the first time get runs with no child.state) would make misconfiguration easier to spot without changing behavior in production.

  • hasData and falsy values: hasData currently treats all falsy primitives as “no data”, so a future use of 0/false as a meaningful config for gridOp/treeOp would be ignored. If you want to be future-proof while still treating null/undefined as empty, you could adjust it to:

    const hasData = (value: any) => {
      if (value == null) return false
      if (Array.isArray(value)) return value.length > 0
      if (typeof value === 'object') return Object.keys(value).length > 0
      return true
    }

All three are nice-to-have improvements and not blockers.

To double-check whether gridOp/treeOp might legitimately be 0/false anywhere, you can run a quick search in your repo:

#!/bin/bash
rg -n "gridOp|treeOp" packages --type=ts --type=vue -C2 || true

Also applies to: 58-94, 106-130

packages/vue/src/select/src/pc.vue (1)

262-270: Prop name casing for InputBoxType is likely wrong and may break binding.

Here the prop is declared as InputBoxType, but templates typically use input-box-type, which Vue normalizes to inputBoxType. That may not match this capitalized prop name, and it’s also inconsistent with the rest of the props.

To avoid subtle binding issues, I’d rename the prop to inputBoxType:

-    InputBoxType: {
+    inputBoxType: {
       type: String,
       default: 'input',
       validator: (value: string) => ['input', 'underline'].includes(value)
     },

Please also adjust any internal references (including in base-select/tree-select/grid-select, demos, and docs) to use inputBoxType consistently.

You can quickly confirm which casing is used elsewhere with:

#!/bin/bash
rg -n "inputBoxType|input-box-type|InputBoxType" packages -C2 || true
🧹 Nitpick comments (6)
packages/vue/src/select/src/mobile-first.vue (1)

454-457: LGTM! Array syntax is correct.

The change from spread arguments to array syntax for the m() helper is functionally equivalent and valid. The conditional class logic correctly applies brand colors when state.selectCls !== 'check'.

Minor observation: The file uses mixed syntax for m() calls—some with arrays (here and line 490), others with spread arguments (line 308). Consider standardizing the pattern in future refactors for consistency.

examples/sites/demos/pc/app/select/nest-grid-remote.spec.ts (4)

44-51: Consider narrowing the caret locator with .first() for stability

select.locator('.tiny-base-select__caret') may match multiple elements if more carets or similar icons are ever introduced under this select. Using .first() (as in other tests) would keep the intent clear and maintain previous behavior of asserting a single primary caret.


74-96: Multi-select: selectors look good; consider updating caret class and avoiding fixed timeouts

  • The switch to select.locator('.tiny-base-select__input').first() plus an initial select.click() makes sense with the new base-select structure and should reduce flakiness around input visibility.
  • This block still uses the old .tiny-select__caret class at Line 77. If the caret class has been fully migrated to .tiny-base-select__caret elsewhere, this is likely an oversight and could break when the old class is removed.
  • The added waitForTimeout(200) (and later 1000ms waits) works but is brittle. When convenient, consider replacing with expectations such as await expect(dropdown).toBeVisible() or await expect(input).toBeVisible() to make the test less timing-sensitive.

96-103: Scope tag locator to the current select to avoid cross-select interference

const tags = page.locator('.tiny-base-select .tiny-tag') is global to the page and could accidentally pick up tags from other selects if more are added to the demo. Scoping to the current select would make the assertion more robust, e.g.:

const tags = select.locator('.tiny-base-select .tiny-tag')
// or if .tiny-base-select wraps this select:
// const tags = select.locator('.tiny-tag')

110-138: Fourth test selectors align with new base-select API; optional .first() on input

The use of .tiny-base-select__input and .tiny-base-select__caret.first() is consistent with the refactor and the test flow looks sound. If this variant can ever render multiple base-select inputs, you might optionally mirror the .first() usage on input for clarity, but it’s not strictly necessary.

examples/sites/demos/pc/app/select/automatic-dropdown.spec.ts (1)

24-30: Scope the option click to the dropdown for more robust tests.

Using page.getByRole('list').getByText('上海') is page-wide and could target the wrong list if the page gains other lists containing the same text. Consider scoping through the dropdown locator instead:

await dropdown.getByRole('list').getByText('上海').click()

This keeps the test coupled only to the select’s dropdown DOM.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8b63954 and 29bfe1e.

📒 Files selected for processing (20)
  • examples/sites/demos/pc/app/select/automatic-dropdown.spec.ts (1 hunks)
  • examples/sites/demos/pc/app/select/copy-multi.spec.ts (1 hunks)
  • examples/sites/demos/pc/app/select/events.spec.ts (2 hunks)
  • examples/sites/demos/pc/app/select/input-box-type.spec.ts (1 hunks)
  • examples/sites/demos/pc/app/select/nest-grid-remote.spec.ts (5 hunks)
  • examples/sites/demos/pc/app/select/nest-grid.spec.ts (3 hunks)
  • examples/sites/demos/pc/app/select/nest-radio-grid-much-data.spec.ts (1 hunks)
  • examples/sites/demos/pc/app/select/nest-tree.spec.ts (2 hunks)
  • examples/sites/demos/pc/app/select/popup-style-position.spec.ts (1 hunks)
  • examples/sites/demos/pc/app/select/remote-method.spec.ts (1 hunks)
  • packages/renderless/src/base-select/index.ts (1 hunks)
  • packages/renderless/src/grid-select/index.ts (1 hunks)
  • packages/renderless/src/grid-select/vue.ts (3 hunks)
  • packages/renderless/src/select-wrapper/vue.ts (1 hunks)
  • packages/renderless/src/tree-select/index.ts (3 hunks)
  • packages/renderless/src/tree-select/vue.ts (2 hunks)
  • packages/theme/src/option/index.less (2 hunks)
  • packages/vue/src/base-select/src/pc.vue (2 hunks)
  • packages/vue/src/select/src/mobile-first.vue (1 hunks)
  • packages/vue/src/select/src/pc.vue (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (11)
  • examples/sites/demos/pc/app/select/nest-tree.spec.ts
  • examples/sites/demos/pc/app/select/events.spec.ts
  • examples/sites/demos/pc/app/select/remote-method.spec.ts
  • packages/renderless/src/tree-select/vue.ts
  • examples/sites/demos/pc/app/select/popup-style-position.spec.ts
  • examples/sites/demos/pc/app/select/copy-multi.spec.ts
  • packages/theme/src/option/index.less
  • packages/vue/src/base-select/src/pc.vue
  • packages/renderless/src/grid-select/vue.ts
  • examples/sites/demos/pc/app/select/nest-radio-grid-much-data.spec.ts
  • packages/renderless/src/base-select/index.ts
🧰 Additional context used
🧬 Code graph analysis (3)
packages/renderless/src/select-wrapper/vue.ts (4)
packages/renderless/src/grid-select/vue.ts (2)
  • api (16-29)
  • renderless (31-139)
packages/renderless/src/tree-select/vue.ts (2)
  • api (13-13)
  • renderless (15-77)
packages/vue-common/src/index.ts (1)
  • props (55-73)
packages/renderless/src/base-select/index.ts (2)
  • focus (442-448)
  • blur (450-455)
packages/renderless/src/grid-select/index.ts (1)
packages/renderless/src/grid-select/vue.ts (1)
  • api (16-29)
packages/renderless/src/tree-select/index.ts (2)
packages/renderless/src/grid-select/vue.ts (1)
  • api (16-29)
packages/renderless/src/tree-select/vue.ts (1)
  • api (13-13)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: PR E2E Test (pnpm test:e2e3)
🔇 Additional comments (7)
examples/sites/demos/pc/app/select/nest-grid-remote.spec.ts (1)

8-15: Confirm caret DOM location for the basic single-select case

Here suffixSvg is now resolved from dropdown.locator('.tiny-base-select__caret'), whereas other tests resolve the caret from the .tiny-select root. If .tiny-base-select__caret actually lives in the base-select wrapper rather than inside the dropdown panel, this assertion may become a no-op (element never found / always “hidden”). Please double-check the DOM for this variant and align the root (select vs dropdown) with where the caret truly sits.

packages/renderless/src/tree-select/index.ts (1)

159-204: Lazy-load guards in mounted/watchValue look good.

The added checks around state.modelValue and option && option.length > 0, plus the early returns when initialNodes/data are empty, make the multiple and single branches safer in lazy-load/remote scenarios and prevent updating baseSelect with empty selections. If you ever allow 0/false as valid model values, you may eventually want to relax the !state.modelValue check to state.modelValue == null, but as-is this is consistent with prior behavior.

Also applies to: 233-239

packages/renderless/src/grid-select/index.ts (2)

70-84: Auto remote-search on first open is wired safely.

Extending handleVisibleChange to take props and gating api.filter('') behind props.remote && props.remoteConfig?.autoSearch && state.firstAutoSearch !== false (plus state.isMounted) keeps the first-auto-search behavior predictable and ensures it only fires once per instance.


86-98: checkRowKeys normalization in buildSelectConfig improves robustness.

Normalizing state.gridCheckedData into checkRowKeys (array, or raw.value array, else []) makes the select–grid wiring tolerant of both raw arrays and ref-like shapes, and avoids leaking undefined/object wrappers into the table config.

examples/sites/demos/pc/app/select/nest-grid.spec.ts (1)

11-12: Caret selectors updated consistently to the new base-select class.

Switching the caret locator to .tiny-base-select__caret across single/multiple/searchable cases keeps these specs aligned with the new DOM structure while still scoping via the .tiny-select wrapper.

Also applies to: 34-35, 74-75

examples/sites/demos/pc/app/select/input-box-type.spec.ts (1)

58-64: Assertions now match base-select caret and multiple-mode classes.

Using .tiny-base-select__caret for the icon color assertion and .tiny-base-select__multiple for the multi-select class keeps this spec in sync with the refactored select DOM.

Also applies to: 70-72

packages/vue/src/select/src/pc.vue (1)

2-10: Dynamic wrapper composition for select variants looks clean and compatible.

Delegating to <component ref="childComponent" :is="resolvedComponent" v-bind="mergedProps" v-on="listeners"> with slot/slotProps forwarding and extendOptions: { components } keeps TinyBaseSelect, TinyTreeSelect, and TinyGridSelect behind a single public <tiny-select> surface while preserving listeners and slots. Combined with the renderless wrapper, this should make variant switching and future extension much easier to maintain.

Also applies to: 28-36, 306-314

@discreted66 discreted66 closed this Dec 2, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request (功能增强)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant