Skip to content

Conversation

@discreted66
Copy link
Collaborator

@discreted66 discreted66 commented Jan 8, 2026

PR

feat:添加无障碍信息

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

Accessibility Enhancements

  • Keyboard navigation and screen reader support improved across file upload, numeric input, search, switch, and upload list components
  • All interactive buttons and controls now include descriptive labels and keyboard navigation
  • Form inputs and buttons enhanced with keyboard activation capabilities
  • Improved accessibility support for users relying on assistive technology across the entire interface

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

@github-actions github-actions bot added the enhancement New feature or request (功能增强) label Jan 8, 2026
@coderabbitai
Copy link

coderabbitai bot commented Jan 8, 2026

Walkthrough

This pull request adds comprehensive accessibility enhancements across multiple Vue components (file-upload, numeric, search, switch, and upload-list) by introducing ARIA attributes, semantic roles, keyboard navigation support, and proper screen-reader labeling. No business logic or control flow is modified.

Changes

Cohort / File(s) Summary
File Upload Accessibility
packages/vue/src/file-upload/src/mobile-first.vue, packages/vue/src/file-upload/src/pc.vue
Added ARIA roles (button, group), aria-labels, tabindex, and Enter-key handlers to upload triggers and action elements; root container enhanced with role="group" for better screen reader context
Numeric Component Accessibility
packages/vue/src/numeric/src/mobile-first.vue, packages/vue/src/numeric/src/pc.vue
Enhanced increase/decrease buttons with aria-disabled and aria-labels; added role="numeric" and ARIA attributes (aria-valuemin, aria-valuemax, aria-valuenow, aria-disabled) to numeric input elements
Search Component Accessibility
packages/vue/src/search/src/mobile-first.vue, packages/vue/src/search/src/pc.vue
Added role="search" to root container; enhanced trigger, input, buttons, and selector elements with appropriate ARIA roles (button, searchbox, listbox, option), aria-labels, aria-expanded, aria-haspopup, and keyboard support for Enter key activation
Switch Component Accessibility
packages/vue/src/switch/src/mobile-first.vue, packages/vue/src/switch/src/pc.vue
Added role="switch", aria-checked bound to current value, aria-disabled reflecting state, conditional aria-labels, and Enter-key toggle support
Upload List Accessibility
packages/vue/src/upload-list/src/mobile-first.vue, packages/vue/src/upload-list/src/pc.vue
Enhanced delete and cancel controls with role="button", tabindex="0", and descriptive aria-labels including file names; improved keyboard navigation without altering click behavior

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Suggested labels

enhancement

Suggested reviewers

  • zzcr

Poem

🐰 Hops with glee through accessible lands,
Where keyboard warriors take their stands,
ARIA labels now crystal clear,
Screen readers dance, all can hear!
Buttons that focus, switches that sing,
A web made right—what joy we bring!

🚥 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 change: adding accessibility information (ARIA attributes and keyboard support) across multiple Vue components (file-upload, numeric, search, switch).
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.


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

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

Caution

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

⚠️ Outside diff range comments (1)
packages/vue/src/search/src/pc.vue (1)

101-115: Critical: Listbox keyboard navigation is missing.

The options correctly implement role="option" with aria-label, aria-selected, and unique IDs, but the component lacks the keyboard navigation required by the ARIA listbox pattern. The tabindex="-1" indicates an expectation of programmatic focus management, but no implementation exists.

Missing keyboard interactions:

  • Arrow Up/Down to navigate between options
  • Enter to select the focused option
  • Escape to close the dropdown
  • Focus management (active descendant or roving tabindex)

Keyboard-only users cannot interact with the dropdown options at all. Implement the full ARIA combobox with listbox pattern as documented in the ARIA Authoring Practices Guide.

🤖 Fix all issues with AI agents
In @packages/vue/src/file-upload/src/mobile-first.vue:
- Around line 351-356: The IconPlusCircle is not keyboard-accessible because
ARIA attributes (role, tabindex, aria-label) aren’t forwarded and it has no
click/keyboard handlers; fix by wrapping the icon usage in a native interactive
element (replace the bare IconPlusCircle instance with a <button> that contains
IconPlusCircle, apply the aria-label and classes to the button, and handle
click/keyboard via the button) or alternatively update the IconPlusCircle
component to forward all context.attrs (not just fill, width, height,
custom-class) to the rendered SVG and add click and keydown handlers in its
setup; ensure the mobile-only display (sm:hidden) remains but the interactive
semantics are on the button or forwarded attributes so keyboard and screen
reader users can activate it.

In @packages/vue/src/file-upload/src/pc.vue:
- Around line 239-247: The clickable update icon (span.thumb-icon with
TinyIconFileCloudupload) is keyboard-focusable but missing a key handler; add an
onKeydown handler that listens for Enter and Space and calls updateFile(file)
(preventDefault on Space to avoid page scroll) so keyboard users can activate
the button the same way as onClick.
- Around line 250-260: The span's onKeydown references an undefined helper
handleEnter which will throw at runtime; define or import a handleEnter keyboard
helper and use it in this component (or pull from the shared utility if one
exists) so key events trigger handleRemove(file) correctly. Implement
handleEnter to accept (event, callback), check for Enter/Space key (and call
event.preventDefault() if needed), and invoke the callback; then ensure the span
still uses onClick={() => handleRemove(file)} and onKeydown={(event) =>
handleEnter(event, () => handleRemove(file))} to provide keyboard accessibility.
- Around line 228-236: The span acting as a button (rendering TinyIconDownload)
is keyboard-focusable but missing a key handler; add an onKeydown handler on
that span to call execDownload(file) when Enter (key === 'Enter') or Space (key
=== ' ' or key === 'Spacebar') is pressed, and call event.preventDefault() for
Space to avoid page scroll; ensure the handler signature matches the component
(e.g., onKeydown={(e) => { if (e.key === 'Enter' || e.key === ' ' || e.key ===
'Spacebar') { e.preventDefault(); execDownload(file); } }}), keeping
role="button" and tabindex="0" and reusing execDownload and TinyIconDownload
identifiers.

In @packages/vue/src/numeric/src/mobile-first.vue:
- Around line 81-85: Replace the invalid ARIA role "numeric" with the correct
"spinbutton" on the element that sets role="numeric" (in mobile-first.vue around
the numeric input element), keeping the existing aria-valuemin, aria-valuemax,
aria-valuenow and aria-disabled bindings (state.currentValue, min, max,
state.inputDisabled) so they map correctly to the spinbutton role semantics.
- Around line 56-57: The hardcoded aria-label="increase" should be replaced with
a localized string; update the template in mobile-first.vue to bind aria-label
to a translation key (e.g. :aria-label="t('numeric.increase')" or
:aria-label="$t('numeric.increase')") and ensure the component uses the i18n
composable or this.$t (import/use useI18n and expose t if using setup). Add the
corresponding key (numeric.increase) to the locale messages so the label is
internationalized.
- Around line 27-28: Replace the hardcoded aria-label="decrease" with a
localized string: use your component's i18n method (e.g., the useI18n hook's t
function or $t) and bind the attribute, e.g. :aria-label="t('decrease')" (or
:aria-label="$t('decrease')" if using the global $t). Ensure you import/use
useI18n in the component setup (or access $t in options API) and add the
"decrease" key to the locale messages so the screen-reader label is
internationalized; update the attribute on the element that currently has
aria-label="decrease".

In @packages/vue/src/numeric/src/pc.vue:
- Around line 65-66: Replace the hardcoded aria-label strings with the
component's i18n helper by calling t(...) instead of literal text: change
aria-label="decrease" to aria-label="t('decrease')" and aria-label="increase" to
aria-label="t('increase')" in the pc.vue template (the aria-label attributes
shown near the buttons that use :aria-disabled="state.minDisabled" and
:aria-disabled="state.maxDisabled"); ensure you use the same t() function
already used in this component (see usages around lines 41 and 44) so screen
readers receive localized labels.
- Around line 110-114: The template uses an invalid ARIA role "numeric"; replace
role="numeric" with role="spinbutton" in the pc.vue template where the element
sets :aria-valuemin="min", :aria-valuemax="max",
:aria-valuenow="state.currentValue" and :aria-disabled="state.inputDisabled" so
the attributes match the correct ARIA widget; keep the existing aria attributes
and bindings (min, max, state.currentValue, state.inputDisabled) but only change
the role string to "spinbutton".
- Around line 187-191: The template uses an invalid ARIA role "numeric" on the
element that binds :aria-valuemin="min", :aria-valuemax="max",
:aria-valuenow="state.currentValue" and :aria-disabled="state.inputDisabled";
change that role to the correct "spinbutton" value (i.e., replace role="numeric"
with role="spinbutton") so the element and its associated state
(state.currentValue, state.inputDisabled) use a valid ARIA role.

In @packages/vue/src/search/src/mobile-first.vue:
- Around line 35-38: The trigger element with role="button" and tabindex="0" is
focusable but not keyboard-activatable; add a keyboard handler on that same
trigger (e.g., @keydown="onTriggerKeydown") and implement onTriggerKeydown to
listen for Enter and Space and invoke the same handler used by the trigger's
click (the existing click handler function for opening/toggling the selector) so
it toggles state.show via keyboard activation. Ensure the handler prevents
default for Space and only reacts to "Enter" or " " (Space).
- Around line 155-167: The listbox lacks keyboard handlers and focus management
required by the WAI-ARIA listbox pattern; update the component to handle
ArrowUp/ArrowDown to move a focused index through state.types, Enter/Space to
select the current option, and Escape to close the listbox while keeping
aria-selected in sync and moving DOM focus to/from elements with ids like
`tiny-search__option-${index}`; implement or reuse methods in the component
(e.g., a focusedIndex reactive state, methods like
onOptionActivate/closeListbox/selectType) and attach keydown handlers on the
listbox container and per-option to call those methods, ensure @click still
selects, and when opening/closing set focus to the listbox or previously focused
control to satisfy focus management.

In @packages/vue/src/search/src/pc.vue:
- Line 86: The anchor acting as a button (the <a ...
@click="searchClick($event)">) lacks keyboard activation; add keyboard handlers
so pressing Enter or Space invokes the same action as click. Update the element
to handle keydown (or Vue shorthand @keydown.enter and @keydown.space) and call
searchClick($event) while preventing default/stop propagation for Space to avoid
page scroll; keep the existing role="button" and tabindex="0" and ensure the
searchClick method still receives the event.

In @packages/vue/src/switch/src/mobile-first.vue:
- Around line 15-18: The aria-label currently uses hardcoded 'on'/'off' strings;
update the binding on the element (the :aria-label in the template where
role="switch" and state.currentValue is compared to trueValue) to use the
component i18n function t() so it returns t('yes') when state.currentValue ===
trueValue and t('no') otherwise for non-'word' types, leaving undefined when
types === 'word' so visible text still provides context.

In @packages/vue/src/switch/src/pc.vue:
- Around line 15-18: Replace the conditional, hard-coded aria-label with an
unconditional, i18n-backed label: remove the ternary that checks state.showText
and instead set aria-label based on state.currentValue === trueValue using the
component i18n function t() (e.g., t('on')/t('off') or t('yes')/t('no')
depending on available locale keys) so screen readers always receive a localized
label and casing is controlled by the locale strings; update the attribute
expression where aria-label is defined (referencing state, currentValue,
trueValue and t()) accordingly.

In @packages/vue/src/upload-list/src/mobile-first.vue:
- Around line 98-106: Several span-based delete controls have role="button" and
tabindex="0" but lack keyboard activation; update each of these elements (the
ones that call remove({ file })) to also handle keyboard activation by adding
@keydown.enter and @keydown.space handlers that invoke the same remove({ file })
action, and ensure the handlers stop propagation and preventDefault for the
space key so activation behavior matches the click. Apply the same change to
every delete control that uses the remove({ file }) invocation (all spans with
role="button" and tabindex="0") so they become keyboard-activatable.

In @packages/vue/src/upload-list/src/pc.vue:
- Around line 89-97: The delete/cancel span elements with role="button" and
tabindex="0" are missing keyboard handlers; add keydown handlers to make Enter
and Space activate them. For the spans that call remove({ file }) on click
(e.g., the delete control referencing remove and t('ui.uploadList.delete')), add
@keydown.enter="remove({ file })" and @keydown.space.prevent="remove({ file })".
For the cancel control that emits 'remove' (uses $emit('remove', file)), add
@keydown.enter="$emit('remove', file)" and
@keydown.space.prevent="$emit('remove', file)". Apply the same changes to all
affected controls (the spans at the reviewed locations) so keyboard users can
activate them.
🧹 Nitpick comments (1)
packages/vue/src/file-upload/src/pc.vue (1)

176-176: Consider removing the redundant aria-label.

The button already contains visible text (<span>{t('ui.fileUpload.uploadFile')}</span>), so the aria-label will override it for screen readers. It's generally better to let the visible text be announced naturally unless additional context is needed.

♻️ Proposed fix
-          <tiny-button disabled={disabled} aria-label={t('ui.fileUpload.uploadFile')} onClick={handleTriggerClick}>
+          <tiny-button disabled={disabled} onClick={handleTriggerClick}>
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 138c31e and 2e25f90.

📒 Files selected for processing (10)
  • packages/vue/src/file-upload/src/mobile-first.vue
  • packages/vue/src/file-upload/src/pc.vue
  • packages/vue/src/numeric/src/mobile-first.vue
  • packages/vue/src/numeric/src/pc.vue
  • packages/vue/src/search/src/mobile-first.vue
  • packages/vue/src/search/src/pc.vue
  • packages/vue/src/switch/src/mobile-first.vue
  • packages/vue/src/switch/src/pc.vue
  • packages/vue/src/upload-list/src/mobile-first.vue
  • packages/vue/src/upload-list/src/pc.vue
⏰ 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 (6)
packages/vue/src/switch/src/mobile-first.vue (1)

22-22: Good addition for keyboard accessibility!

Adding Enter key support aligns with standard keyboard navigation patterns for switches and brings the mobile-first variant to parity with the PC variant. This complements the existing Space key handler (line 21) and improves the experience for keyboard-only users.

packages/vue/src/file-upload/src/mobile-first.vue (1)

367-378: LGTM! Proper keyboard accessibility implementation.

The wrapper div correctly implements keyboard accessibility with role="button", tabindex="0", and an onKeydown handler that activates on Enter key press.

packages/vue/src/search/src/pc.vue (4)

21-22: LGTM! Good accessibility landmark.

The role="search" provides a semantic landmark for screen reader users, and the aria-label with a sensible fallback ensures the search region is properly identified.


48-76: Excellent searchbox ARIA implementation.

The input properly implements the searchbox role with complete ARIA attributes: aria-label, aria-expanded, aria-controls (conditionally set), and aria-haspopup. The relationship to the listbox is well-defined.


79-79: Perfect button implementation with keyboard support.

The clear button correctly implements role="button" with both click and keyboard activation (@keydown.enter), plus a descriptive aria-label. This is exactly how interactive buttons should be implemented.


92-98: Good listbox container structure.

The selector properly uses role="listbox" with a unique ID that's correctly referenced by the input's aria-controls attribute, establishing the proper ARIA relationship.

Comment on lines +351 to +356
<icon-plus-circle
role="button"
tabindex="0"
aria-label={t('ui.fileUpload.uploadFile')}
custom-class="sm:hidden w-5 h-5 absolute top-0.5 right-5"
/>
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:

# First, locate the IconPlusCircle component definition
find . -type f \( -name "*.vue" -o -name "*.ts" -o -name "*.tsx" -o -name "*.js" \) -path "*/icon*" | head -20

Repository: opentiny/tiny-vue

Length of output: 1251


🏁 Script executed:

# Search for IconPlusCircle in the codebase
rg -t vue -t ts 'IconPlusCircle' --max-count=20

Repository: opentiny/tiny-vue

Length of output: 88


🏁 Script executed:

# Check the file that contains the review comment
sed -n '340,365p' packages/vue/src/file-upload/src/mobile-first.vue

Repository: opentiny/tiny-vue

Length of output: 1040


🏁 Script executed:

# Search for IconPlusCircle component definition
rg -t ts -t js 'IconPlusCircle|icon-plus-circle' --max-count=30

Repository: opentiny/tiny-vue

Length of output: 4250


🏁 Script executed:

# Look for icon component imports in the file
head -100 packages/vue/src/file-upload/src/mobile-first.vue

Repository: opentiny/tiny-vue

Length of output: 2215


🏁 Script executed:

# Search for how icon components are implemented in this codebase
fd 'icon-plus' -t f | head -20

Repository: opentiny/tiny-vue

Length of output: 43


🏁 Script executed:

# Find the IconPlusCircle implementation
cat packages/vue-icon/src/plus-circle.ts

Repository: opentiny/tiny-vue

Length of output: 277


🏁 Script executed:

# Look at the svg utility function to understand how icons handle props
rg -A10 -B5 'export.*svg.*=.*function|const svg' packages/vue-icon/src/ | head -50

Repository: opentiny/tiny-vue

Length of output: 43


🏁 Script executed:

# Check the icon component implementation
cat packages/vue-icon/src/index.ts 2>/dev/null || find packages/vue-icon -name "index.ts" -exec cat {} \;

Repository: opentiny/tiny-vue

Length of output: 50373


🏁 Script executed:

# Look for the svg function definition
fd 'index.ts' packages/vue-icon | xargs cat | head -80

Repository: opentiny/tiny-vue

Length of output: 3688


🏁 Script executed:

# Find the svg function implementation in vue-common
fd -t f -name "*.ts" "packages/vue-common" | head -20

Repository: opentiny/tiny-vue

Length of output: 291


🏁 Script executed:

# Search for svg function definition
rg -n "export.*svg" packages/vue-common/src/ -A5 | head -40

Repository: opentiny/tiny-vue

Length of output: 513


🏁 Script executed:

# Look for how svg handles props/attributes
rg -n "function svg|const svg" packages/vue-common/src/ -B2 -A15 | head -60

Repository: opentiny/tiny-vue

Length of output: 2791


🏁 Script executed:

# Get the full svg function implementation
sed -n '316,400p' packages/vue-common/src/index.ts

Repository: opentiny/tiny-vue

Length of output: 3008


🏁 Script executed:

# Look at how the component renders and handles attributes
sed -n '320,380p' packages/vue-common/src/index.ts

Repository: opentiny/tiny-vue

Length of output: 2112


🏁 Script executed:

# Find renderComponent function to see how the component is rendered
rg -n "renderComponent" packages/vue-common/src/index.ts -A10 | head -50

Repository: opentiny/tiny-vue

Length of output: 1180


🏁 Script executed:

# Check if the icon component has inheritAttrs set
rg -n "inheritAttrs" packages/vue-common/src/index.ts

Repository: opentiny/tiny-vue

Length of output: 137


🏁 Script executed:

# Look for the complete setup return to see how the component is rendered
sed -n '316,420p' packages/vue-common/src/index.ts

Repository: opentiny/tiny-vue

Length of output: 3460


🏁 Script executed:

# Check renderComponent implementation to see if it passes through all attrs
sed -n '1,50p' packages/vue-common/src/adapter/vue3/index.ts | head -40

Repository: opentiny/tiny-vue

Length of output: 1327


🏁 Script executed:

# Look for renderComponent in adapter
rg -n "renderComponent" packages/vue-common/src/adapter/vue3/index.ts -A15 | head -60

Repository: opentiny/tiny-vue

Length of output: 456


🏁 Script executed:

# Get more context around the icon usage in mobile-first.vue
sed -n '340,365p' packages/vue/src/file-upload/src/mobile-first.vue

Repository: opentiny/tiny-vue

Length of output: 1040


🏁 Script executed:

# Check if there's a parent button or interactive element wrapping the icon
sed -n '330,370p' packages/vue/src/file-upload/src/mobile-first.vue

Repository: opentiny/tiny-vue

Length of output: 1548


The ARIA attributes on IconPlusCircle are not applied to the underlying DOM element.

The IconPlusCircle component receives role="button", tabindex="0", and aria-label attributes, but they are not forwarded to the rendered SVG. The component's setup function only extracts fill, width, height, and custom-class from context attributes—ARIA attributes are excluded. Additionally, the icon lacks click and keyboard handlers.

To fix this:

  1. Wrap the icon in a proper <button> element, or
  2. Extract and forward ARIA attributes in the icon component's setup, or
  3. Add click/keyboard event handlers directly to the icon.

On mobile, the icon is shown alone (sm:hidden) without a button wrapper, making it inaccessible to keyboard users.

🤖 Prompt for AI Agents
In @packages/vue/src/file-upload/src/mobile-first.vue around lines 351 - 356,
The IconPlusCircle is not keyboard-accessible because ARIA attributes (role,
tabindex, aria-label) aren’t forwarded and it has no click/keyboard handlers;
fix by wrapping the icon usage in a native interactive element (replace the bare
IconPlusCircle instance with a <button> that contains IconPlusCircle, apply the
aria-label and classes to the button, and handle click/keyboard via the button)
or alternatively update the IconPlusCircle component to forward all
context.attrs (not just fill, width, height, custom-class) to the rendered SVG
and add click and keydown handlers in its setup; ensure the mobile-only display
(sm:hidden) remains but the interactive semantics are on the button or forwarded
attributes so keyboard and screen reader users can activate it.

Comment on lines +228 to 236
<span
class="thumb-icon"
role="button"
tabindex="0"
aria-label={t('ui.fileUpload.downloadFile')}
title={t('ui.fileUpload.downloadFile')}
onClick={() => execDownload(file)}>
<TinyIconDownload class="download-icon" />
</span>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add keyboard event handler for accessibility.

The download icon has role="button" and tabindex="0", making it keyboard-focusable, but lacks an onKeydown handler to activate it via keyboard (e.g., Enter or Space keys). This creates an inconsistent and incomplete accessibility experience.

♻️ Proposed fix
        <span
          class="thumb-icon"
          role="button"
          tabindex="0"
          aria-label={t('ui.fileUpload.downloadFile')}
          title={t('ui.fileUpload.downloadFile')}
+         onKeydown={(event) => handleEnter(event, () => execDownload(file))}
          onClick={() => execDownload(file)}>
          <TinyIconDownload class="download-icon" />
        </span>

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @packages/vue/src/file-upload/src/pc.vue around lines 228 - 236, The span
acting as a button (rendering TinyIconDownload) is keyboard-focusable but
missing a key handler; add an onKeydown handler on that span to call
execDownload(file) when Enter (key === 'Enter') or Space (key === ' ' or key ===
'Spacebar') is pressed, and call event.preventDefault() for Space to avoid page
scroll; ensure the handler signature matches the component (e.g., onKeydown={(e)
=> { if (e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar') {
e.preventDefault(); execDownload(file); } }}), keeping role="button" and
tabindex="0" and reusing execDownload and TinyIconDownload identifiers.

Comment on lines +239 to 247
<span
class="thumb-icon"
role="button"
tabindex="0"
aria-label={t('ui.fileUpload.updateFile')}
title={t('ui.fileUpload.updateFile')}
onClick={() => updateFile(file)}>
<TinyIconFileCloudupload class="refres-icon" />
</span>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add keyboard event handler for accessibility.

The update icon has role="button" and tabindex="0", making it keyboard-focusable, but lacks an onKeydown handler to activate it via keyboard. This creates an incomplete accessibility experience.

♻️ Proposed fix
        <span
          class="thumb-icon"
          role="button"
          tabindex="0"
          aria-label={t('ui.fileUpload.updateFile')}
          title={t('ui.fileUpload.updateFile')}
+         onKeydown={(event) => handleEnter(event, () => updateFile(file))}
          onClick={() => updateFile(file)}>
          <TinyIconFileCloudupload class="refres-icon" />
        </span>

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @packages/vue/src/file-upload/src/pc.vue around lines 239 - 247, The
clickable update icon (span.thumb-icon with TinyIconFileCloudupload) is
keyboard-focusable but missing a key handler; add an onKeydown handler that
listens for Enter and Space and calls updateFile(file) (preventDefault on Space
to avoid page scroll) so keyboard users can activate the button the same way as
onClick.

Comment on lines +250 to 260
<span
class="thumb-icon"
role="button"
tabindex="0"
aria-label={t('ui.fileUpload.deleteFile')}
title={t('ui.fileUpload.deleteFile')}
onClick={() => handleRemove(file)}
onKeydown={(event) => handleEnter(event, () => handleRemove(file))}>
<TinyIconClose class="close-icon" />
</span>
)
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

Undefined function handleEnter will cause runtime error.

Line 257 references the handleEnter function, but it is not defined anywhere in this file. This will cause a runtime error when users attempt to delete a file via keyboard.

🐛 Proposed fix

Add the handleEnter helper function to handle keyboard events:

    const getThumIcon = (file) => [
+     // Helper to handle Enter key press
+     const handleEnter = (event, callback) => {
+       if (event.key === 'Enter' || event.key === ' ') {
+         event.preventDefault()
+         callback()
+       }
+     }
+
      showDownload && (

Alternatively, if this pattern is used elsewhere in the codebase, consider extracting it to a shared utility module.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @packages/vue/src/file-upload/src/pc.vue around lines 250 - 260, The span's
onKeydown references an undefined helper handleEnter which will throw at
runtime; define or import a handleEnter keyboard helper and use it in this
component (or pull from the shared utility if one exists) so key events trigger
handleRemove(file) correctly. Implement handleEnter to accept (event, callback),
check for Enter/Space key (and call event.preventDefault() if needed), and
invoke the callback; then ensure the span still uses onClick={() =>
handleRemove(file)} and onKeydown={(event) => handleEnter(event, () =>
handleRemove(file))} to provide keyboard accessibility.

Comment on lines +27 to +28
:aria-disabled="state.minDisabled || state.inputDisabled"
aria-label="decrease"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Internationalize the aria-label value.

The aria-label uses a hardcoded English string "decrease". This value should be internationalized to support non-English screen reader users, consistent with the accessibility improvements in this PR.

🌐 Proposed fix to use i18n
         :aria-disabled="state.minDisabled || state.inputDisabled"
-        aria-label="decrease"
+        :aria-label="t('ui.numeric.decrease')"
         v-repeat-click="decrease"

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @packages/vue/src/numeric/src/mobile-first.vue around lines 27 - 28, Replace
the hardcoded aria-label="decrease" with a localized string: use your
component's i18n method (e.g., the useI18n hook's t function or $t) and bind the
attribute, e.g. :aria-label="t('decrease')" (or :aria-label="$t('decrease')" if
using the global $t). Ensure you import/use useI18n in the component setup (or
access $t in options API) and add the "decrease" key to the locale messages so
the screen-reader label is internationalized; update the attribute on the
element that currently has aria-label="decrease".

<slot v-if="slots.suffix" name="suffix"></slot>
<div v-else-if="!slots.prefix && !slots.suffix" class="tiny-search__input-btn">
<a @click="searchClick($event)">
<a role="button" tabindex="0" aria-label="submit search" @click="searchClick($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 | 🔴 Critical

Critical: Submit button missing keyboard activation.

Similar to the dropdown trigger, this button has role="button" and tabindex="0" but only responds to clicks. Keyboard-only users cannot submit the search using this button.

⌨️ Proposed fix to add keyboard support
-        <a role="button" tabindex="0" aria-label="submit search" @click="searchClick($event)">
+        <a role="button" tabindex="0" aria-label="submit search" @click="searchClick($event)" @keydown.enter="searchClick($event)" @keydown.space.prevent="searchClick($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
<a role="button" tabindex="0" aria-label="submit search" @click="searchClick($event)">
<a role="button" tabindex="0" aria-label="submit search" @click="searchClick($event)" @keydown.enter="searchClick($event)" @keydown.space.prevent="searchClick($event)">
🤖 Prompt for AI Agents
In @packages/vue/src/search/src/pc.vue at line 86, The anchor acting as a button
(the <a ... @click="searchClick($event)">) lacks keyboard activation; add
keyboard handlers so pressing Enter or Space invokes the same action as click.
Update the element to handle keydown (or Vue shorthand @keydown.enter and
@keydown.space) and call searchClick($event) while preventing default/stop
propagation for Space to avoid page scroll; keep the existing role="button" and
tabindex="0" and ensure the searchClick method still receives the event.

Comment on lines +15 to +18
role="switch"
:aria-checked="state.currentValue === trueValue"
:aria-disabled="state.disabled || types === 'loading'"
:aria-label="types === 'word' ? undefined : state.currentValue === trueValue ? 'on' : 'off'"
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:

# List files in the switch directory to understand the structure
find packages/vue/src/switch/src -type f -name "*.vue" | head -20

Repository: opentiny/tiny-vue

Length of output: 140


🏁 Script executed:

# Read the mobile-first.vue file to see the actual code
wc -l packages/vue/src/switch/src/mobile-first.vue

Repository: opentiny/tiny-vue

Length of output: 109


🏁 Script executed:

# Read the relevant sections of mobile-first.vue
cat -n packages/vue/src/switch/src/mobile-first.vue | head -80

Repository: opentiny/tiny-vue

Length of output: 3472


🏁 Script executed:

cat -n packages/vue/src/switch/src/pc.vue | head -80

Repository: opentiny/tiny-vue

Length of output: 3328


🏁 Script executed:

# Search for 'on' and 'off' i18n keys in the switch or locale files
find . -type f \( -name "*.ts" -o -name "*.js" -o -name "*.json" \) -path "*/locale/*" | head -20

Repository: opentiny/tiny-vue

Length of output: 43


🏁 Script executed:

# Search for usage of t('on') or t('off') in the codebase
rg "t\(['\"]on['\"]" --type ts --type js | head -20

Repository: opentiny/tiny-vue

Length of output: 43


🏁 Script executed:

# Search for 'yes' and 'no' to understand the pattern
rg "t\(['\"]yes['\"]|t\(['\"]no['\"]" --type ts --type js --type vue | head -30

Repository: opentiny/tiny-vue

Length of output: 88


🏁 Script executed:

# Find locale-related files
fd -e json -e ts -e js | grep -i locale | head -20

Repository: opentiny/tiny-vue

Length of output: 821


🏁 Script executed:

# Search for where 'yes' and 'no' are used in the codebase
rg "yes|no" packages/vue/src/switch --type-add vue:*.vue -t vue | grep -E "(yes|no)" | head -30

Repository: opentiny/tiny-vue

Length of output: 926


🏁 Script executed:

# Check the renderless switch implementation to understand i18n
cat -n packages/renderless/src/switch/vue.ts | head -100

Repository: opentiny/tiny-vue

Length of output: 3509


🏁 Script executed:

# Check the English locale file
cat -n packages/vue-locale/src/lang/en.ts | grep -A 5 -B 5 "yes\|no\|on\|off" | head -50

Repository: opentiny/tiny-vue

Length of output: 1719


🏁 Script executed:

# Check the Chinese locale file
cat -n packages/vue-locale/src/lang/zh-CN.ts | grep -A 5 -B 5 "yes\|no\|on\|off" | head -50

Repository: opentiny/tiny-vue

Length of output: 1587


Internationalize aria-label to match visible text translations.

The aria-label on line 18 uses hardcoded English strings 'on' and 'off', but the component already supports i18n via t(). When types === 'word', the visible text displays t('yes') and t('no') (lines 61-62), yet the aria-label is undefined for that case—which is appropriate since the visible text provides context.

For other switch types (icon/default), the aria-label should be internationalized to be consistent with the rest of the component's translation strategy.

🌐 Suggested fix
-    :aria-label="types === 'word' ? undefined : state.currentValue === trueValue ? 'on' : 'off'"
+    :aria-label="types === 'word' ? undefined : state.currentValue === trueValue ? t('yes') : t('no')"

This uses the existing i18n keys that are already employed for visible text, ensuring accessibility labels are translated for all users.

📝 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
role="switch"
:aria-checked="state.currentValue === trueValue"
:aria-disabled="state.disabled || types === 'loading'"
:aria-label="types === 'word' ? undefined : state.currentValue === trueValue ? 'on' : 'off'"
role="switch"
:aria-checked="state.currentValue === trueValue"
:aria-disabled="state.disabled || types === 'loading'"
:aria-label="types === 'word' ? undefined : state.currentValue === trueValue ? t('yes') : t('no')"
🤖 Prompt for AI Agents
In @packages/vue/src/switch/src/mobile-first.vue around lines 15 - 18, The
aria-label currently uses hardcoded 'on'/'off' strings; update the binding on
the element (the :aria-label in the template where role="switch" and
state.currentValue is compared to trueValue) to use the component i18n function
t() so it returns t('yes') when state.currentValue === trueValue and t('no')
otherwise for non-'word' types, leaving undefined when types === 'word' so
visible text still provides context.

Comment on lines +15 to +18
role="switch"
:aria-checked="state.currentValue === trueValue"
:aria-disabled="disabled"
:aria-label="state.showText ? (state.currentValue === trueValue ? 'on' : 'off') : undefined"
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:

# First, let's check if the file exists and read the relevant sections
fd "pc.vue" -p "*switch*"

Repository: opentiny/tiny-vue

Length of output: 156


🏁 Script executed:

# Read the file to verify the code snippet and context
cat -n packages/vue/src/switch/src/pc.vue | head -60

Repository: opentiny/tiny-vue

Length of output: 2844


🏁 Script executed:

# Search for i18n function usage in the component
rg -t vue "t\(" packages/vue/src/switch/src/pc.vue -B 2 -A 2

Repository: opentiny/tiny-vue

Length of output: 88


🏁 Script executed:

# Search for i18n resources to check if 'on'/'off', 'yes'/'no' keys exist
fd -t f -e "json" -e "ts" -e "js" | xargs rg -l "\"yes\"|\"no\"|\"on\"|\"off\"" | head -20

Repository: opentiny/tiny-vue

Length of output: 43


🏁 Script executed:

# Check what i18n keys are actually available for switch component
fd "switch" -p "*lang*" -o "*i18n*" -o "*locale*"

Repository: opentiny/tiny-vue

Length of output: 192


🏁 Script executed:

# Look for i18n configuration and translation files
find . -type f -name "*switch*" -path "*/lang/*" -o -type f -name "*switch*" -path "*/locale/*"

Repository: opentiny/tiny-vue

Length of output: 43


🏁 Script executed:

# Search for where switch i18n strings are defined
rg -i "yes|no|on|off" --type json | grep -i switch

Repository: opentiny/tiny-vue

Length of output: 1210


🏁 Script executed:

# Check the import statement to understand where t() comes from
cat -n packages/vue/src/switch/src/pc.vue | grep -A 5 "import"

Repository: opentiny/tiny-vue

Length of output: 549


🏁 Script executed:

# Look for locale/lang files in the switch component directory
fd . packages/vue/src/switch/ -type f

Repository: opentiny/tiny-vue

Length of output: 231


🏁 Script executed:

# Check if there are i18n keys specifically for 'on'/'off'
rg "on.*off|off.*on" --type json

Repository: opentiny/tiny-vue

Length of output: 401


🏁 Script executed:

# Look for locale or translation files
find . -type f -name "*.json" -path "*locale*" -o -type f -name "*.ts" -path "*locale*"

Repository: opentiny/tiny-vue

Length of output: 963


🏁 Script executed:

# Check vue-common for the t() function implementation to understand i18n usage
find . -path "*vue-common*" -type f | head -20

Repository: opentiny/tiny-vue

Length of output: 919


🏁 Script executed:

# Search for where 'on'/'off' might be defined as i18n keys
rg "\"on\"|\"off\"" --type json | head -30

Repository: opentiny/tiny-vue

Length of output: 43


🏁 Script executed:

# Check if there's a switch-specific locale file
find . -type d -name "switch" | xargs -I {} find {} -name "*.json" -o -name "*.ts"

Repository: opentiny/tiny-vue

Length of output: 976


🏁 Script executed:

# Search for accessible.on or accessible.off patterns in the codebase
rg "accessible|aria" --type json

Repository: opentiny/tiny-vue

Length of output: 169


🏁 Script executed:

# Check the English locale file for switch-related keys
cat packages/vue-locale/src/lang/en.ts | grep -i "switch\|yes\|no\|on\|off" -A 2 -B 2

Repository: opentiny/tiny-vue

Length of output: 19428


🏁 Script executed:

# Check the full English locale structure
head -100 packages/vue-locale/src/lang/en.ts

Repository: opentiny/tiny-vue

Length of output: 2497


🏁 Script executed:

# Search for 'yes' and 'no' in locale files
rg "yes|no" packages/vue-locale/src/lang/ -A 1 -B 1

Repository: opentiny/tiny-vue

Length of output: 50373


🏁 Script executed:

# Check if 'on'/'off' keys exist in locale files
rg "on|off" packages/vue-locale/src/lang/ -A 1 -B 1 | head -50

Repository: opentiny/tiny-vue

Length of output: 2575


🏁 Script executed:

# Look at the renderless locale index to understand available keys
cat packages/renderless/src/locales/index.ts

Repository: opentiny/tiny-vue

Length of output: 3642


Internationalize aria-label values and remove conditional logic.

The accessibility attributes are a great addition, but there are two actionable concerns:

  1. Non-internationalized labels: The aria-label uses hardcoded English strings 'on' and 'off', but the component already supports i18n with the t() function (see lines 45-46 using t('yes') and t('no')). Non-English speakers using screen readers will hear English labels.

  2. Conditional aria-label: The aria-label is only present when state.showText is true. Screen readers need labels regardless of whether text is visually displayed. Provide the aria-label unconditionally for better accessibility.

  3. Case mismatch: The aria-label values are lowercase ('on'/'off') while the visual text slots show uppercase ('ON'/'OFF'), creating a slight disconnect for users who can perceive both.

🌐 Suggested fix for internationalization
-    :aria-label="state.showText ? (state.currentValue === trueValue ? 'on' : 'off') : undefined"
+    :aria-label="state.currentValue === trueValue ? t('yes') : t('no')"

This uses the existing i18n function and provides labels unconditionally. Consider whether 'yes'/'no' semantics are appropriate for your use case, or add dedicated 'on'/'off' keys to locale files for better semantic alignment.

📝 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
role="switch"
:aria-checked="state.currentValue === trueValue"
:aria-disabled="disabled"
:aria-label="state.showText ? (state.currentValue === trueValue ? 'on' : 'off') : undefined"
role="switch"
:aria-checked="state.currentValue === trueValue"
:aria-disabled="disabled"
:aria-label="state.currentValue === trueValue ? t('yes') : t('no')"
🤖 Prompt for AI Agents
In @packages/vue/src/switch/src/pc.vue around lines 15 - 18, Replace the
conditional, hard-coded aria-label with an unconditional, i18n-backed label:
remove the ternary that checks state.showText and instead set aria-label based
on state.currentValue === trueValue using the component i18n function t() (e.g.,
t('on')/t('off') or t('yes')/t('no') depending on available locale keys) so
screen readers always receive a localized label and casing is controlled by the
locale strings; update the attribute expression where aria-label is defined
(referencing state, currentValue, trueValue and t()) accordingly.

Comment on lines +98 to +106
<span
v-if="!displayOnly"
class="cursor-pointer"
role="button"
tabindex="0"
:aria-label="t('ui.uploadList.delete') + ' ' + file.name"
@click.stop="remove({ file })"
>{{ t('ui.uploadList.delete') }}</span
>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add keyboard event handlers to all delete controls.

Multiple delete controls throughout the file have role="button" and tabindex="0" but lack keyboard event handlers (e.g., @keydown.enter or @keydown.space). This makes them keyboard-focusable but not keyboard-activatable, creating an incomplete accessibility experience.

Affected lines: 98-106, 139-143, 239-246, 284-287, 368-371, 391-394, 482-486.

♻️ Example fix for lines 98-106
                  <span
                    v-if="!displayOnly"
                    class="cursor-pointer"
                    role="button"
                    tabindex="0"
                    :aria-label="t('ui.uploadList.delete') + ' ' + file.name"
+                   @keydown.enter="remove({ file })"
+                   @keydown.space.prevent="remove({ file })"
                    @click.stop="remove({ file })"
                    >{{ t('ui.uploadList.delete') }}</span
                  >

Apply similar changes to all other delete controls in this file.

Also applies to: 139-143, 239-246, 284-287, 368-371, 391-394, 482-486

🤖 Prompt for AI Agents
In @packages/vue/src/upload-list/src/mobile-first.vue around lines 98 - 106,
Several span-based delete controls have role="button" and tabindex="0" but lack
keyboard activation; update each of these elements (the ones that call remove({
file })) to also handle keyboard activation by adding @keydown.enter and
@keydown.space handlers that invoke the same remove({ file }) action, and ensure
the handlers stop propagation and preventDefault for the space key so activation
behavior matches the click. Apply the same change to every delete control that
uses the remove({ file }) invocation (all spans with role="button" and
tabindex="0") so they become keyboard-activatable.

Comment on lines +89 to +97
<span
v-if="!displayOnly"
class="operate-btn"
role="button"
tabindex="0"
:aria-label="t('ui.uploadList.delete') + ' ' + file.name"
@click.stop="remove({ file })"
>{{ t('ui.uploadList.delete') }}</span
>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add keyboard event handlers to delete and cancel controls.

Multiple delete and cancel controls have role="button" and tabindex="0" but lack keyboard event handlers. Keyboard users can focus these elements but cannot activate them, creating an incomplete accessibility experience.

Affected lines: 89-97 (delete), 218-222 (delete), 246-249 (cancel), 293-297 (delete).

♻️ Example fix for lines 89-97
                  <span
                    v-if="!displayOnly"
                    class="operate-btn"
                    role="button"
                    tabindex="0"
                    :aria-label="t('ui.uploadList.delete') + ' ' + file.name"
+                   @keydown.enter="remove({ file })"
+                   @keydown.space.prevent="remove({ file })"
                    @click.stop="remove({ file })"
                    >{{ t('ui.uploadList.delete') }}</span
                  >

Apply similar changes to all other affected controls. Note: For the cancel control (lines 246-249), use @keydown.enter="$emit('remove', file)" and @keydown.space.prevent="$emit('remove', file)".

Also applies to: 218-222, 246-249, 293-297

🤖 Prompt for AI Agents
In @packages/vue/src/upload-list/src/pc.vue around lines 89 - 97, The
delete/cancel span elements with role="button" and tabindex="0" are missing
keyboard handlers; add keydown handlers to make Enter and Space activate them.
For the spans that call remove({ file }) on click (e.g., the delete control
referencing remove and t('ui.uploadList.delete')), add @keydown.enter="remove({
file })" and @keydown.space.prevent="remove({ file })". For the cancel control
that emits 'remove' (uses $emit('remove', file)), add
@keydown.enter="$emit('remove', file)" and
@keydown.space.prevent="$emit('remove', file)". Apply the same changes to all
affected controls (the spans at the reviewed locations) so keyboard users can
activate them.

>
<input
:tabindex="tabindex"
role="numeric"
Copy link
Member

Choose a reason for hiding this comment

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

role=“numeric” 不是有效的 ARIA 角色。对于带有增减控制的数字输入,请使用 role=“spinbutton”。aria-valuemin、aria-valuemax 和 aria-valuenow 属性专为 spinbutton 角色设计。

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.

2 participants