-
Notifications
You must be signed in to change notification settings - Fork 336
feat(file-upload, input, numeric, search, switch): Add accessibility information #3974
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Conversation
WalkthroughThis 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
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ 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. Comment |
There was a problem hiding this 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"witharia-label,aria-selected, and unique IDs, but the component lacks the keyboard navigation required by the ARIA listbox pattern. Thetabindex="-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 redundantaria-label.The button already contains visible text (
<span>{t('ui.fileUpload.uploadFile')}</span>), so thearia-labelwill 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
📒 Files selected for processing (10)
packages/vue/src/file-upload/src/mobile-first.vuepackages/vue/src/file-upload/src/pc.vuepackages/vue/src/numeric/src/mobile-first.vuepackages/vue/src/numeric/src/pc.vuepackages/vue/src/search/src/mobile-first.vuepackages/vue/src/search/src/pc.vuepackages/vue/src/switch/src/mobile-first.vuepackages/vue/src/switch/src/pc.vuepackages/vue/src/upload-list/src/mobile-first.vuepackages/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 anonKeydownhandler 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 thearia-labelwith 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), andaria-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 descriptivearia-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'saria-controlsattribute, establishing the proper ARIA relationship.
| <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" | ||
| /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 -20Repository: opentiny/tiny-vue
Length of output: 1251
🏁 Script executed:
# Search for IconPlusCircle in the codebase
rg -t vue -t ts 'IconPlusCircle' --max-count=20Repository: 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.vueRepository: 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=30Repository: 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.vueRepository: 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 -20Repository: opentiny/tiny-vue
Length of output: 43
🏁 Script executed:
# Find the IconPlusCircle implementation
cat packages/vue-icon/src/plus-circle.tsRepository: 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 -50Repository: 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 -80Repository: 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 -20Repository: opentiny/tiny-vue
Length of output: 291
🏁 Script executed:
# Search for svg function definition
rg -n "export.*svg" packages/vue-common/src/ -A5 | head -40Repository: 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 -60Repository: opentiny/tiny-vue
Length of output: 2791
🏁 Script executed:
# Get the full svg function implementation
sed -n '316,400p' packages/vue-common/src/index.tsRepository: 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.tsRepository: 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 -50Repository: 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.tsRepository: 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.tsRepository: 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 -40Repository: 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 -60Repository: 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.vueRepository: 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.vueRepository: 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:
- Wrap the icon in a proper
<button>element, or - Extract and forward ARIA attributes in the icon component's setup, or
- 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.
| <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> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| <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> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| <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> | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| :aria-disabled="state.minDisabled || state.inputDisabled" | ||
| aria-label="decrease" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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)"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| <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.
| role="switch" | ||
| :aria-checked="state.currentValue === trueValue" | ||
| :aria-disabled="state.disabled || types === 'loading'" | ||
| :aria-label="types === 'word' ? undefined : state.currentValue === trueValue ? 'on' : 'off'" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 -20Repository: 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.vueRepository: 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 -80Repository: opentiny/tiny-vue
Length of output: 3472
🏁 Script executed:
cat -n packages/vue/src/switch/src/pc.vue | head -80Repository: 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 -20Repository: 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 -20Repository: 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 -30Repository: opentiny/tiny-vue
Length of output: 88
🏁 Script executed:
# Find locale-related files
fd -e json -e ts -e js | grep -i locale | head -20Repository: 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 -30Repository: 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 -100Repository: 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 -50Repository: 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 -50Repository: 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.
| 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.
| role="switch" | ||
| :aria-checked="state.currentValue === trueValue" | ||
| :aria-disabled="disabled" | ||
| :aria-label="state.showText ? (state.currentValue === trueValue ? 'on' : 'off') : undefined" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 -60Repository: 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 2Repository: 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 -20Repository: 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 switchRepository: 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 fRepository: 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 jsonRepository: 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 -20Repository: 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 -30Repository: 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 jsonRepository: 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 2Repository: opentiny/tiny-vue
Length of output: 19428
🏁 Script executed:
# Check the full English locale structure
head -100 packages/vue-locale/src/lang/en.tsRepository: 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 1Repository: 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 -50Repository: 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.tsRepository: 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:
-
Non-internationalized labels: The aria-label uses hardcoded English strings
'on'and'off', but the component already supports i18n with thet()function (see lines 45-46 usingt('yes')andt('no')). Non-English speakers using screen readers will hear English labels. -
Conditional aria-label: The aria-label is only present when
state.showTextis true. Screen readers need labels regardless of whether text is visually displayed. Provide the aria-label unconditionally for better accessibility. -
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.
| 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.
| <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 | ||
| > |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| <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 | ||
| > |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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" |
There was a problem hiding this comment.
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 角色设计。
PR
feat:添加无障碍信息
PR Checklist
Please check if your PR fulfills the following requirements:
PR Type
What kind of change does this PR introduce?
What is the current behavior?
Issue Number: N/A
What is the new behavior?
Does this PR introduce a breaking change?
Other information
Summary by CodeRabbit
Accessibility Enhancements
✏️ Tip: You can customize this high-level summary in your review settings.