Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions packages/vue/src/file-upload/src/mobile-first.vue
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,12 @@ export default defineComponent({
<span class="ml-2">{t('ui.fileUpload.uploadFile')}</span>
</div>
</tiny-button>
<icon-plus-circle custom-class="sm:hidden w-5 h-5 absolute top-0.5 right-5" />
<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"
/>
Comment on lines +351 to +356
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.

</div>
)
} else if (listType === 'drag-single') {
Expand All @@ -359,7 +364,13 @@ export default defineComponent({
listType === 'picture-single' && uploadFiles.length && (defaultClass += ' hidden')

defaultContent = (
<div class={defaultClass}>
<div
class={defaultClass}
role="button"
tabindex="0"
aria-label={t('ui.fileUpload.uploadFile')}
onKeydown={(event) => event.key === 'Enter' && handleTriggerClick()}
>
<div class="absolute w-full top-1/2 left-0 -translate-y-1/2 z-[1] text-center">
{defaultList[type || 'picture']}
</div>
Expand Down Expand Up @@ -667,7 +678,13 @@ export default defineComponent({
const attrs = a($attrs, ['^on[A-Z]'])

return (
<div {...attrs} data-tag="tiny-file-upload" class={isDragSingle ? 'relative inline-block' : ''}>
<div
{...attrs}
data-tag="tiny-file-upload"
role="group"
aria-label={title}
class={isDragSingle ? 'relative inline-block' : ''}
>
{getDefaultTitle({ listType, title, showTitle, displayOnly, mode })}
{noticePC}
{isText && !displayOnly ? (slots.trigger ? [createUploadComponent()] : createUploadComponent()) : null}
Expand Down
29 changes: 24 additions & 5 deletions packages/vue/src/file-upload/src/pc.vue
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ export default defineComponent({
const getTriggerContent = (t: any, disabled: boolean) => {
return (
<div class="trigger-btn">
<tiny-button disabled={disabled} onClick={handleTriggerClick}>
<tiny-button disabled={disabled} aria-label={t('ui.fileUpload.uploadFile')} onClick={handleTriggerClick}>
<TinyIconPlus />
<span>{t('ui.fileUpload.uploadFile')}</span>
</tiny-button>
Expand Down Expand Up @@ -225,17 +225,36 @@ export default defineComponent({

const getThumIcon = (file) => [
showDownload && (
<span class="thumb-icon" title={t('ui.fileUpload.downloadFile')} onClick={() => execDownload(file)}>
<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>
Comment on lines +228 to 236
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.

),
isEdm && !isFolder && showUpdate && (
<span class="thumb-icon" title={t('ui.fileUpload.updateFile')} onClick={() => updateFile(file)}>
<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>
Comment on lines +239 to 247
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.

),
showDel && (
<span class="thumb-icon" title={t('ui.fileUpload.deleteFile')} onClick={() => handleRemove(file)}>
<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>
)
Comment on lines +250 to 260
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.

Expand Down Expand Up @@ -508,7 +527,7 @@ export default defineComponent({
const attrs = a($attrs, ['^on[A-Z]'])

return (
<div class="tiny-file-upload" {...attrs}>
<div class="tiny-file-upload" role="group" aria-label={title} {...attrs}>
{isSaasType ? getDefaultTitle(title, this.showTitle) : ''}
{notice}
{isPictureCard ? uploadList : ''}
Expand Down
9 changes: 9 additions & 0 deletions packages/vue/src/numeric/src/mobile-first.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
"
v-if="controls"
role="button"
:aria-disabled="state.minDisabled || state.inputDisabled"
aria-label="decrease"
Comment on lines +27 to +28
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".

v-repeat-click="decrease"
@keydown.enter="decrease"
>
Expand Down Expand Up @@ -51,6 +53,8 @@
"
v-if="controls"
role="button"
:aria-disabled="state.maxDisabled || state.inputDisabled"
aria-label="increase"
Comment on lines +56 to +57
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 "increase". This value should be internationalized.

🌐 Proposed fix to use i18n
         :aria-disabled="state.maxDisabled || state.inputDisabled"
-        aria-label="increase"
+        :aria-label="t('ui.numeric.increase')"
         v-repeat-click="increase"
📝 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
:aria-disabled="state.maxDisabled || state.inputDisabled"
aria-label="increase"
:aria-disabled="state.maxDisabled || state.inputDisabled"
:aria-label="t('ui.numeric.increase')"
v-repeat-click="increase"
🤖 Prompt for AI Agents
In @packages/vue/src/numeric/src/mobile-first.vue around lines 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.

v-repeat-click="increase"
@keydown.enter="increase"
>
Expand All @@ -74,6 +78,11 @@
>
<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 角色设计。

:aria-valuemin="min"
:aria-valuemax="max"
:aria-valuenow="state.currentValue"
:aria-disabled="state.inputDisabled"
Comment on lines +81 to +85
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: Replace invalid ARIA role with "spinbutton".

role="numeric" is not a valid ARIA role. For numeric inputs with increment/decrement controls, use role="spinbutton". The aria-valuemin, aria-valuemax, and aria-valuenow attributes are specifically designed for the spinbutton role.

🔧 Proposed fix
-          role="numeric"
+          role="spinbutton"
           :aria-valuemin="min"
           :aria-valuemax="max"
           :aria-valuenow="state.currentValue"
           :aria-disabled="state.inputDisabled"
📝 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="numeric"
:aria-valuemin="min"
:aria-valuemax="max"
:aria-valuenow="state.currentValue"
:aria-disabled="state.inputDisabled"
role="spinbutton"
:aria-valuemin="min"
:aria-valuemax="max"
:aria-valuenow="state.currentValue"
:aria-disabled="state.inputDisabled"
🤖 Prompt for AI Agents
In @packages/vue/src/numeric/src/mobile-first.vue around lines 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.

:class="
m(
gcls('numeric_input_inner'),
Expand Down
18 changes: 18 additions & 0 deletions packages/vue/src/numeric/src/pc.vue
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@
<span
class="tiny-numeric__decrease"
role="button"
aria-label="decrease"
:aria-disabled="state.minDisabled"
Comment on lines +65 to +66
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 values.

The aria-label attributes use hardcoded English strings ("decrease", "increase"), but this component already supports internationalization via the t() function (see lines 41, 44). Screen reader users in non-English locales will hear English labels.

🌐 Proposed fix to use i18n
-            aria-label="decrease"
+            :aria-label="t('ui.numeric.decrease')"
             :aria-disabled="state.minDisabled"
-            aria-label="increase"
+            :aria-label="t('ui.numeric.increase')"
             :aria-disabled="state.maxDisabled"

Also applies to: 77-78

🤖 Prompt for AI Agents
In @packages/vue/src/numeric/src/pc.vue around lines 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.

v-if="state.controls && !unit"
v-repeat-click="decrease"
:class="{ 'is-disabled': state.minDisabled }"
Expand All @@ -72,6 +74,8 @@
<span
class="tiny-numeric__increase"
role="button"
aria-label="increase"
:aria-disabled="state.maxDisabled"
v-if="state.controls && !unit"
v-repeat-click="increase"
:class="{ 'is-disabled': state.maxDisabled }"
Expand Down Expand Up @@ -103,6 +107,11 @@
</span>
<input
:tabindex="tabindex"
role="numeric"
:aria-valuemin="min"
:aria-valuemax="max"
:aria-valuenow="state.currentValue"
:aria-disabled="state.inputDisabled"
Comment on lines +110 to +114
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: Replace invalid ARIA role with "spinbutton".

role="numeric" is not a valid ARIA role and will break screen reader functionality. For numeric inputs with increment/decrement controls, the correct role is "spinbutton". The aria-valuemin, aria-valuemax, and aria-valuenow attributes are designed specifically for the spinbutton role.

🔧 Proposed fix
-                role="numeric"
+                role="spinbutton"
                 :aria-valuemin="min"
                 :aria-valuemax="max"
                 :aria-valuenow="state.currentValue"
                 :aria-disabled="state.inputDisabled"
📝 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="numeric"
:aria-valuemin="min"
:aria-valuemax="max"
:aria-valuenow="state.currentValue"
:aria-disabled="state.inputDisabled"
role="spinbutton"
:aria-valuemin="min"
:aria-valuemax="max"
:aria-valuenow="state.currentValue"
:aria-disabled="state.inputDisabled"
🤖 Prompt for AI Agents
In @packages/vue/src/numeric/src/pc.vue around lines 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".

:class="[
'tiny-numeric__input-inner',
{ 'tiny-numeric__show-left': !state.controls && (showLeft || shape === 'filter') }
Expand Down Expand Up @@ -130,6 +139,8 @@
<span
class="tiny-numeric__decrease"
role="button"
aria-label="decrease"
:aria-disabled="state.minDisabled"
Comment on lines +142 to +143
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 values.

Same issue as in the filter variant: aria-label attributes use hardcoded English strings that won't be translated for non-English screen reader users.

🌐 Proposed fix to use i18n
-        aria-label="decrease"
+        :aria-label="t('ui.numeric.decrease')"
         :aria-disabled="state.minDisabled"
-        aria-label="increase"
+        :aria-label="t('ui.numeric.increase')"
         :aria-disabled="state.maxDisabled"

Also applies to: 154-155

v-if="controls && !unit"
v-repeat-click="decrease"
:class="{ 'is-disabled': state.minDisabled }"
Expand All @@ -140,6 +151,8 @@
<span
class="tiny-numeric__increase"
role="button"
aria-label="increase"
:aria-disabled="state.maxDisabled"
v-if="controls && !unit"
v-repeat-click="increase"
:class="{ 'is-disabled': state.maxDisabled }"
Expand Down Expand Up @@ -171,6 +184,11 @@
</span>
<input
:tabindex="tabindex"
role="numeric"
:aria-valuemin="min"
:aria-valuemax="max"
:aria-valuenow="state.currentValue"
:aria-disabled="state.inputDisabled"
Comment on lines +187 to +191
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: Replace invalid ARIA role with "spinbutton".

Same critical issue as in the filter variant: role="numeric" is not valid. Use role="spinbutton" instead.

🔧 Proposed fix
-            role="numeric"
+            role="spinbutton"
             :aria-valuemin="min"
             :aria-valuemax="max"
             :aria-valuenow="state.currentValue"
             :aria-disabled="state.inputDisabled"
📝 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="numeric"
:aria-valuemin="min"
:aria-valuemax="max"
:aria-valuenow="state.currentValue"
:aria-disabled="state.inputDisabled"
role="spinbutton"
:aria-valuemin="min"
:aria-valuemax="max"
:aria-valuenow="state.currentValue"
:aria-disabled="state.inputDisabled"
🤖 Prompt for AI Agents
In @packages/vue/src/numeric/src/pc.vue around lines 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.

:class="['tiny-numeric__input-inner', { 'tiny-numeric__show-left': !controls && showLeft }]"
ref="input"
:value="state.displayValue"
Expand Down
32 changes: 30 additions & 2 deletions packages/vue/src/search/src/mobile-first.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<div
:class="m(gcls('search-default'))"
data-tag="tiny-search"
role="search"
:aria-label="placeholder || 'search'"
@mouseenter="state.hovering = true"
@mouseleave="state.hovering = false"
>
Expand Down Expand Up @@ -30,6 +32,10 @@
gcls({ 'pc-search-present-unbig': size === 'small' && !big })
)
"
role="button"
tabindex="0"
aria-haspopup="listbox"
:aria-expanded="state.show"
Comment on lines +35 to +38
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 to the selector trigger.

The trigger div has role="button" and tabindex="0" but lacks a keyboard event handler. Users can focus it but cannot activate it via keyboard.

♻️ Proposed fix
        <div
          v-show="!state.collapse && state.types.length"
          data-tag="tiny-search__present"
          :class="..."
          role="button"
          tabindex="0"
          aria-haspopup="listbox"
          :aria-expanded="state.show"
+         @keydown.enter="showSelector"
+         @keydown.space.prevent="showSelector"
          @click="showSelector"
        >
📝 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="button"
tabindex="0"
aria-haspopup="listbox"
:aria-expanded="state.show"
<div
v-show="!state.collapse && state.types.length"
data-tag="tiny-search__present"
:class="..."
role="button"
tabindex="0"
aria-haspopup="listbox"
:aria-expanded="state.show"
@keydown.enter="showSelector"
@keydown.space.prevent="showSelector"
@click="showSelector"
>
🤖 Prompt for AI Agents
In @packages/vue/src/search/src/mobile-first.vue around lines 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).

@click="showSelector"
>
<slot name="text" :slot-scope="state.searchValue">
Expand Down Expand Up @@ -70,6 +76,10 @@
:placeholder="placeholder"
type="text"
data-tag="tiny-search__input"
role="searchbox"
:aria-label="placeholder || 'search input'"
:aria-expanded="state.show"
aria-haspopup="listbox"
@keyup.enter="searchEnterKey"
@input="handleInput"
@change="handleChange"
Expand All @@ -91,7 +101,13 @@
"
v-if="state.showClear && !state.collapse"
>
<a :class="m(gcls('pc-search-input-btn-transtion-a'))" @click="clear">
<a
:class="m(gcls('pc-search-input-btn-transtion-a'))"
role="button"
tabindex="0"
aria-label="clear search"
@click="clear"
>
Comment on lines +104 to +110
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 button-like anchors.

The clear (lines 104-110) and search (lines 130-136) anchors have role="button" and tabindex="0" but lack keyboard event handlers. Consider using native <button> elements instead, or add keyboard handlers.

♻️ Proposed fix for clear button
          <a
            :class="m(gcls('pc-search-input-btn-transtion-a'))"
            role="button"
            tabindex="0"
            aria-label="clear search"
+           @keydown.enter="clear"
+           @keydown.space.prevent="clear"
            @click="clear"
          >

Apply similar changes to the search button (lines 130-136).

Also applies to: 130-136

<icon-close
@mousedown.prevent
data-tag="tiny-svg-size"
Expand All @@ -111,7 +127,13 @@
)
"
>
<a :class="m(gcls('pc-search-input-btn-a'))" @click="searchClick">
<a
:class="m(gcls('pc-search-input-btn-a'))"
role="button"
tabindex="0"
aria-label="submit search"
@click="searchClick"
>
<icon-search
:class="
m(
Expand All @@ -130,13 +152,19 @@
ref="selector"
data-tag="tiny-search__selector"
:class="m(gcls('search-selector'))"
role="listbox"
>
<div data-tag="tiny-search__selector-body" :class="m(gcls('search-selector-body'))">
<ul data-tag="tiny-search__poplist">
<li
v-for="(item, index) in state.types"
:key="index"
data-tag="tiny-search__poplist-item"
role="option"
:aria-label="item.text || item.label"
:aria-selected="item === state.searchValue"
:id="`tiny-search__option-${index}`"
tabindex="-1"
Comment on lines +155 to +167
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's understand the full structure of the mobile-first.vue file
wc -l packages/vue/src/search/src/mobile-first.vue

Repository: opentiny/tiny-vue

Length of output: 109


🏁 Script executed:

#!/bin/bash
# Read the file to understand the context around lines 155-167
sed -n '1,50p' packages/vue/src/search/src/mobile-first.vue

Repository: opentiny/tiny-vue

Length of output: 1811


🏁 Script executed:

#!/bin/bash
# Read around the lines in question (155-167)
sed -n '140,180p' packages/vue/src/search/src/mobile-first.vue

Repository: opentiny/tiny-vue

Length of output: 1624


🏁 Script executed:

#!/bin/bash
# Search for keyboard event handlers in the search component
rg -n 'onKeydown|onKeyup|keypress|@keydown|@keyup|ArrowDown|ArrowUp|handleKey' packages/vue/src/search/src/ --type=ts --type=vue

Repository: opentiny/tiny-vue

Length of output: 88


🏁 Script executed:

#!/bin/bash
# Search for click handlers and interaction handlers in the listbox section
rg -n '@click|click|handleClick' packages/vue/src/search/src/mobile-first.vue -A 2 -B 2

Repository: opentiny/tiny-vue

Length of output: 1191


🏁 Script executed:

#!/bin/bash
# Search for keyboard patterns without file type restriction
rg -n 'keydown|keyup|keypress|ArrowDown|ArrowUp|Escape|handleKey' packages/vue/src/search/src/mobile-first.vue

Repository: opentiny/tiny-vue

Length of output: 101


🏁 Script executed:

#!/bin/bash
# Read the script section of the file to see if keyboard handlers are defined there
tail -n +180 packages/vue/src/search/src/mobile-first.vue | head -n 85

Repository: opentiny/tiny-vue

Length of output: 2574


🏁 Script executed:

#!/bin/bash
# Let's look at the entire script logic section
sed -n '220,265p' packages/vue/src/search/src/mobile-first.vue

Repository: opentiny/tiny-vue

Length of output: 1291


🏁 Script executed:

#!/bin/bash
# Search for the showSelector and changeKey function implementations
rg -n 'showSelector|changeKey' packages/vue/src/search/src/mobile-first.vue -A 5

Repository: opentiny/tiny-vue

Length of output: 543


🏁 Script executed:

#!/bin/bash
# Check parent components or renderless logic for search component
find packages/vue/src/search -name '*.ts' -o -name '*.js' -o -name '*.vue' | head -20

Repository: opentiny/tiny-vue

Length of output: 247


🏁 Script executed:

#!/bin/bash
# Find the renderless search implementation
find . -path '*/vue-renderless/search*' -name '*.ts' -o -name '*.js' | grep -v node_modules | head -20

Repository: opentiny/tiny-vue

Length of output: 827


🏁 Script executed:

#!/bin/bash
# Try to find renderless search in node_modules since it's an external dependency
find packages -name '*renderless*' -type d

Repository: opentiny/tiny-vue

Length of output: 80


🏁 Script executed:

#!/bin/bash
# Check if renderless is in package.json as dependency
cat packages/vue/package.json | grep -A 5 -B 5 renderless

Repository: opentiny/tiny-vue

Length of output: 278


🏁 Script executed:

#!/bin/bash
# Search for keyboard navigation patterns specifically for listbox options in the entire search directory
rg -n 'listbox|option.*key|keyboard.*listbox' packages/vue/src/search/ --type=ts --type=vue

Repository: opentiny/tiny-vue

Length of output: 88


🏁 Script executed:

#!/bin/bash
# More specific search for keyboard event bindings on options or listbox elements
rg -n '@key|keydown|keyup|key.*handler|ArrowDown|ArrowUp' packages/vue/src/search/src/

Repository: opentiny/tiny-vue

Length of output: 389


🏁 Script executed:

#!/bin/bash
# List files in renderless search directory
find packages/renderless -name '*search*' -type f

Repository: opentiny/tiny-vue

Length of output: 101


🏁 Script executed:

#!/bin/bash
# Read the search renderless implementation
ls -la packages/renderless/src/search/ 2>/dev/null || ls -la packages/renderless/search/ 2>/dev/null || find packages/renderless -path '*search*' -type f

Repository: opentiny/tiny-vue

Length of output: 294


🏁 Script executed:

#!/bin/bash
# Find search.vue.ts or similar in renderless
find packages/renderless -name '*search*.ts' -o -name '*search*.js'

Repository: opentiny/tiny-vue

Length of output: 101


🏁 Script executed:

#!/bin/bash
# Read the renderless Vue search implementation
cat -n packages/renderless/src/search/vue.ts

Repository: opentiny/tiny-vue

Length of output: 4008


🏁 Script executed:

#!/bin/bash
# Also read the index.ts for context
cat -n packages/renderless/src/search/index.ts

Repository: opentiny/tiny-vue

Length of output: 5812


🏁 Script executed:

#!/bin/bash
# Search for keyboard-related logic in renderless search
rg -n 'keydown|keyup|keypress|Arrow|Escape' packages/renderless/src/search/

Repository: opentiny/tiny-vue

Length of output: 43


Implement keyboard navigation for the ARIA listbox pattern.

The listbox has proper ARIA attributes (role="listbox", role="option", aria-label, aria-selected) but lacks keyboard support. The WAI-ARIA listbox pattern requires:

  • Arrow keys to navigate between options
  • Enter/Space to select an option
  • Escape to close the listbox
  • Focus management when opening/closing

Currently, options have @click handlers only. No keyboard handlers exist in the template or renderless implementation.

Consider implementing the complete keyboard navigation as described in the WAI-ARIA Authoring Practices Guide.

🤖 Prompt for AI Agents
In @packages/vue/src/search/src/mobile-first.vue around lines 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.

:class="
m(
gcls('search-selector-poplist-item'),
Expand Down
34 changes: 30 additions & 4 deletions packages/vue/src/search/src/pc.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
state.searchSize ? 'tiny-search--' + state.searchSize : '',
{ 'is-disabled': disabled }
]"
role="search"
:aria-label="placeholder || 'search'"
@mouseenter="state.hovering = true"
@mouseleave="state.hovering = false"
>
Expand All @@ -26,7 +28,15 @@
<slot name="prefix"></slot>
</div>
<transition name="tiny-transition-search-line-fade" mode="out-in">
<div v-show="!state.collapse && state.types.length" class="tiny-search__present" @click="showSelector">
<div
v-show="!state.collapse && state.types.length"
class="tiny-search__present"
role="button"
tabindex="0"
aria-haspopup="listbox"
:aria-expanded="state.show"
@click="showSelector"
>
Comment on lines +31 to +39
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: Button role requires keyboard activation.

The dropdown trigger has role="button" and tabindex="0" but only responds to clicks. Per ARIA authoring practices, buttons must be activatable with both Enter and Space keys. Keyboard-only users cannot currently open the type selector dropdown.

⌨️ Proposed fix to add keyboard support
       <div
         v-show="!state.collapse && state.types.length"
         class="tiny-search__present"
         role="button"
         tabindex="0"
         aria-haspopup="listbox"
         :aria-expanded="state.show"
         @click="showSelector"
+        @keydown.enter="showSelector"
+        @keydown.space.prevent="showSelector"
       >
📝 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
<div
v-show="!state.collapse && state.types.length"
class="tiny-search__present"
role="button"
tabindex="0"
aria-haspopup="listbox"
:aria-expanded="state.show"
@click="showSelector"
>
<div
v-show="!state.collapse && state.types.length"
class="tiny-search__present"
role="button"
tabindex="0"
aria-haspopup="listbox"
:aria-expanded="state.show"
@click="showSelector"
@keydown.enter="showSelector"
@keydown.space.prevent="showSelector"
>

<slot name="text" :slot-scope="state.searchValue">
<span class="tiny-search__text">{{ state.searchValue.text }}</span>
</slot>
Expand All @@ -37,6 +47,10 @@
</transition>
<input
ref="input"
role="searchbox"
:aria-label="placeholder || 'search'"
:aria-expanded="state.show"
:aria-controls="state.show ? 'tiny-search__selector' : undefined"
v-bind="a($attrs, ['type', 'class', 'style', '^on[A-Z]', 'id', 'clearable'])"
v-model="state.currentValue"
:disabled="disabled"
Expand All @@ -51,6 +65,7 @@
:placeholder="placeholder"
type="text"
class="tiny-search__input"
aria-haspopup="listbox"
@keyup.enter="searchEnterKey($event)"
@change="handleChange"
@input="handleInput"
Expand All @@ -61,26 +76,37 @@
/>
<transition name="tiny-transition-icon-scale-in">
<div class="tiny-search__input-btn tiny-icon-close" v-if="state.showClear && !state.collapse && !disabled">
<a @click="clear($event)">
<a role="button" tabindex="0" aria-label="clear search" @click="clear($event)" @keydown.enter="clear($event)">
<icon-close @mousedown.prevent class="tiny-svg-size" />
</a>
</div>
</transition>
<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.

<icon-search :style="{ fill: state.collapse && transparent ? '#fff' : '' }" class="tiny-svg-size" />
</a>
</div>
</div>
<transition name="tiny-transition-zoom-in-top" mode="out-in">
<div v-show="state.show && state.types.length" ref="selector" class="tiny-search__selector">
<div
v-show="state.show && state.types.length"
ref="selector"
id="tiny-search__selector"
class="tiny-search__selector"
role="listbox"
>
<div class="tiny-search__selector-body">
<ul class="tiny-search__poplist">
<li
v-for="(item, index) in state.types"
:key="index"
class="tiny-search__poplist-item"
role="option"
:aria-label="item.text || item.label"
:aria-selected="item === state.searchValue"
:id="`tiny-search__option-${index}`"
tabindex="-1"
@click="changeKey(item)"
>
<slot name="poplist" :slot-scope="item">
Expand Down
5 changes: 5 additions & 0 deletions packages/vue/src/switch/src/mobile-first.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,14 @@
)
"
data-tag="tiny-switch"
role="switch"
:aria-checked="state.currentValue === trueValue"
:aria-disabled="state.disabled || types === 'loading'"
:aria-label="types === 'word' ? undefined : state.currentValue === trueValue ? 'on' : 'off'"
Comment on lines +15 to +18
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.

:tabindex="tabindex"
@click="toggle"
@keydown.space="toggle"
@keydown.enter="toggle"
>
<span
data-tag="tiny-switch-handle"
Expand Down
4 changes: 4 additions & 0 deletions packages/vue/src/switch/src/pc.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
<template>
<span
v-if="!state.isDisplayOnly"
role="switch"
:aria-checked="state.currentValue === trueValue"
:aria-disabled="disabled"
:aria-label="state.showText ? (state.currentValue === trueValue ? 'on' : 'off') : undefined"
Comment on lines +15 to +18
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.

:class="[state.wrapClasses, state.showText ? 'tiny-switch__text' : '']"
:tabindex="tabindex"
:style="state.switchStyle"
Expand Down
Loading
Loading