fix: add aria-disabled attribute for disabled options in Select component#1212
fix: add aria-disabled attribute for disabled options in Select component#1212afc163 merged 2 commits intoreact-component:masterfrom
Conversation
|
@meet-student is attempting to deploy a commit to the React Component Team on Vercel. A member of the Team first needs to authorize it. |
Summary of ChangesHello @meet-student, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly enhances the accessibility of the Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Changelog
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
总体说明在OptionList组件中添加了aria-disabled属性以增强无障碍支持,对禁用的选项项进行了正确的ARIA标记,并新增了相应的可访问性测试用例。 变更一览表
代码审查工作量估计🎯 2 (简单) | ⏱️ ~10 分钟 建议审查者
庆祝诗
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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 |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #1212 +/- ##
=======================================
Coverage 99.43% 99.43%
=======================================
Files 31 31
Lines 1236 1236
Branches 446 446
=======================================
Hits 1229 1229
Misses 7 7 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Code Review
This pull request correctly adds the aria-disabled attribute to disabled options in the Select component, improving accessibility. However, a high-severity Cross-Site Scripting (XSS) vulnerability was identified in the OptionList component, allowing injection of arbitrary HTML attributes like dangerouslySetInnerHTML via the options prop, which could lead to malicious script execution. While this PR does not cause or mitigate this issue, a remediation recommendation is provided in the review comments. Additionally, my review includes suggestions to improve code consistency and the robustness of the new test.
| key={index} | ||
| {...getItemAriaProps(item, index)} | ||
| aria-selected={isAriaSelected(value)} | ||
| aria-disabled={disabled} |
There was a problem hiding this comment.
For consistency with how aria-disabled is handled elsewhere in this component (e.g., on line 405 with mergedDisabled), it would be better to ensure a boolean is always passed here. When disabled is undefined, the attribute is currently omitted, whereas in other cases it becomes aria-disabled="false". Using !!disabled would make the behavior consistent across the component for enabled options.
aria-disabled={!!disabled}
| expect(input).toHaveAttribute('aria-activedescendant', 'select_list_0'); | ||
| }); | ||
|
|
||
| // https://github.com/ant-design/ant-design/issues/xxxxx |
| const optionA = Array.from(optionItems).find((el) => el.textContent === 'a'); | ||
| const optionB = Array.from(optionItems).find((el) => el.textContent === 'b'); | ||
| expect(optionA).not.toHaveAttribute('aria-disabled', 'true'); | ||
| expect(optionB).toHaveAttribute('aria-disabled', 'true'); |
There was a problem hiding this comment.
These assertions are good, but could be more robust. Consider these improvements:
- Assert that
optionAandoptionBare found before checking their attributes to avoid potential test failures if the elements aren't found (e.g.,expect(optionA).not.toBeNull()). - Make the assertion for
optionAmore specific. Instead of checking that it's nottrue, you could assert that the attribute is not present at all:expect(optionA).not.toHaveAttribute('aria-disabled');. - Add assertions for
optionCas well to ensure all non-disabled options in your test data are handled correctly.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/OptionList.tsx (1)
298-309:⚠️ Potential issue | 🟠 Major
renderItem使用原始disabled而非mergedDisabled,导致 overMaxCount 场景下无障碍状态不一致
virtual={true}(默认)时,屏幕阅读器通过隐藏 ARIA div 中的renderItem输出感知选项状态。当前aria-disabled={disabled}仅反映数据层面的disabled字段,而可视列表(第 405 行)使用mergedDisabled = disabled || (!selected && overMaxCount)。当用户在多选模式下达到
maxCount上限时,未被选中的选项会因overMaxCount而被视觉禁用(mergedDisabled = true),但隐藏 ARIA div 中的对应节点aria-disabled仍为undefined/false,导致辅助技术无法正确感知选项的禁用状态,屏幕阅读器用户将获得与视觉/交互行为相矛盾的信息。🛡️ 建议修复:在 renderItem 中计算 mergedDisabled
const renderItem = (index: number) => { const item = memoFlattenOptions[index]; if (!item) { return null; } const itemData = item.data || {}; const { value, disabled } = itemData; const { group } = item; const attrs = pickAttrs(itemData, true); const mergedLabel = getLabel(item); + const selected = isSelected(value); + const mergedDisabled = disabled || (!selected && overMaxCount); return item ? ( <div aria-label={typeof mergedLabel === 'string' && !group ? mergedLabel : null} {...attrs} key={index} {...getItemAriaProps(item, index)} aria-selected={isAriaSelected(value)} - aria-disabled={disabled} + aria-disabled={mergedDisabled} > {value} </div> ) : null; };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/OptionList.tsx` around lines 298 - 309, The hidden ARIA output in renderItem uses the raw disabled value instead of the visual mergedDisabled, causing mismatch when overMaxCount disables unselected items; update renderItem to compute mergedDisabled = disabled || (!selected && overMaxCount) (same logic used in the visual list) and use mergedDisabled for aria-disabled (replace aria-disabled={disabled} with aria-disabled={mergedDisabled}) so screen readers reflect the actual disabled state for the option; ensure you reference itemData.disabled, item.selected (or selected variable) and overMaxCount when computing mergedDisabled and keep isAriaSelected(value) unchanged.
🧹 Nitpick comments (1)
tests/Accessibility.test.tsx (1)
326-343: 缺少virtual={false}路径的覆盖当前测试仅覆盖
virtual={true}(默认值)下隐藏 ARIA div 的renderItem路径(对应src/OptionList.tsx第 309 行的变更)。src/OptionList.tsx第 405 行新增的aria-disabled={mergedDisabled}作用于virtual={false}时的可见列表项(此时列表项具有role="option"),但此路径缺少对应的测试用例。♻️ 建议补充 virtual=false 路径的测试
+ it('aria-disabled should be set on disabled options (virtual=false)', () => { + const { container } = render( + <Select + id="non-virtual-select" + open + virtual={false} + options={[ + { label: 'Option A', value: 'a' }, + { label: 'Option B', value: 'b', disabled: true }, + { label: 'Option C', value: 'c' }, + ]} + />, + ); + + const dropdown = document.querySelector('#non-virtual-select_list'); + const optionItems = dropdown!.querySelectorAll('[role="option"]'); + const optionA = Array.from(optionItems).find( + (el) => el.querySelector('.rc-select-item-option-content')?.textContent === 'Option A', + ); + const optionB = Array.from(optionItems).find( + (el) => el.querySelector('.rc-select-item-option-content')?.textContent === 'Option B', + ); + expect(optionA).toBeDefined(); + expect(optionB).toBeDefined(); + expect(optionA).not.toHaveAttribute('aria-disabled', 'true'); + expect(optionB).toHaveAttribute('aria-disabled', 'true'); + });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/Accessibility.test.tsx` around lines 326 - 343, Add a test that covers the virtual={false} rendering path so the visible option elements receive the new aria-disabled attribute: render <Select open virtual={false} options=[{label:'Option A',value:'a'},{label:'Option B',value:'b',disabled:true},{label:'Option C',value:'c'}] />, query for elements with role="option" (same approach as the existing test), locate the option whose text matches 'Option B' and assert it has aria-disabled="true" while 'Option A' does not; this ensures the OptionList.tsx mergedDisabled behavior for the non-virtual branch is tested.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@tests/Accessibility.test.tsx`:
- Line 325: Replace the placeholder comment "//
https://github.com/ant-design/ant-design/issues/xxxxx" in Accessibility.test.tsx
with the actual GitHub issue URL referenced by other tests (or remove the
comment entirely); locate the placeholder string in the file and either update
it to the correct issue link used by the related test case or delete the line so
no placeholder remains.
- Around line 338-342: The test is querying options from the test container but
the Select renders its popup via a Portal with an auto-generated id, so
container.querySelectorAll('[role="option"]') returns undefined; update the test
to pass a stable id prop to the Select (e.g., id="virtual-select"), then query
the portal using document.querySelector(`#${'virtual-select'}_list`) (or
document.querySelector('#virtual-select_list')) to get optionItems, and then
find optionA/optionB from that NodeList before asserting aria-disabled on
optionA and optionB.
---
Outside diff comments:
In `@src/OptionList.tsx`:
- Around line 298-309: The hidden ARIA output in renderItem uses the raw
disabled value instead of the visual mergedDisabled, causing mismatch when
overMaxCount disables unselected items; update renderItem to compute
mergedDisabled = disabled || (!selected && overMaxCount) (same logic used in the
visual list) and use mergedDisabled for aria-disabled (replace
aria-disabled={disabled} with aria-disabled={mergedDisabled}) so screen readers
reflect the actual disabled state for the option; ensure you reference
itemData.disabled, item.selected (or selected variable) and overMaxCount when
computing mergedDisabled and keep isAriaSelected(value) unchanged.
---
Nitpick comments:
In `@tests/Accessibility.test.tsx`:
- Around line 326-343: Add a test that covers the virtual={false} rendering path
so the visible option elements receive the new aria-disabled attribute: render
<Select open virtual={false} options=[{label:'Option
A',value:'a'},{label:'Option B',value:'b',disabled:true},{label:'Option
C',value:'c'}] />, query for elements with role="option" (same approach as the
existing test), locate the option whose text matches 'Option B' and assert it
has aria-disabled="true" while 'Option A' does not; this ensures the
OptionList.tsx mergedDisabled behavior for the non-virtual branch is tested.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (3)
tests/__snapshots__/OptionList.test.tsx.snapis excluded by!**/*.snaptests/__snapshots__/Select.test.tsx.snapis excluded by!**/*.snaptests/__snapshots__/Tags.test.tsx.snapis excluded by!**/*.snap
📒 Files selected for processing (2)
src/OptionList.tsxtests/Accessibility.test.tsx
| expect(input).toHaveAttribute('aria-activedescendant', 'select_list_0'); | ||
| }); | ||
|
|
||
| // https://github.com/ant-design/ant-design/issues/xxxxx |
There was a problem hiding this comment.
占位符 Issue URL 应替换为真实链接或删除
同文件中其他测试均引用了真实的 GitHub Issue(如第 24、46、127、162 行),此处 xxxxx 仍为占位符,应在合并前替换为对应 issue 链接或将该注释删除。
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/Accessibility.test.tsx` at line 325, Replace the placeholder comment
"// https://github.com/ant-design/ant-design/issues/xxxxx" in
Accessibility.test.tsx with the actual GitHub issue URL referenced by other
tests (or remove the comment entirely); locate the placeholder string in the
file and either update it to the correct issue link used by the related test
case or delete the line so no placeholder remains.
| const optionItems = container.querySelectorAll('[role="option"]'); | ||
| const optionA = Array.from(optionItems).find((el) => el.textContent === 'a'); | ||
| const optionB = Array.from(optionItems).find((el) => el.textContent === 'b'); | ||
| expect(optionA).not.toHaveAttribute('aria-disabled', 'true'); | ||
| expect(optionB).toHaveAttribute('aria-disabled', 'true'); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's look at the Accessibility.test.tsx file around the mentioned lines
# to understand the context and check lines 183, 226 to verify the query pattern
fd Accessibility.test.tsxRepository: react-component/select
Length of output: 94
🏁 Script executed:
# Search for the test file and read the relevant sections
rg -n "querySelectorAll|querySelector.*_list" tests/Accessibility.test.tsx -B2 -A2Repository: react-component/select
Length of output: 1335
🏁 Script executed:
# Check the Select component implementation for portal/container configuration
fd -e tsx -e ts -e jsx -e js | grep -i select | head -20Repository: react-component/select
Length of output: 623
🏁 Script executed:
# Let's look at the full test case around line 338 to understand context
sed -n '320,345p' tests/Accessibility.test.tsxRepository: react-component/select
Length of output: 1086
🏁 Script executed:
# Check the Select component implementation to understand portal/getContainer configuration
head -100 src/Select.tsxRepository: react-component/select
Length of output: 3293
🏁 Script executed:
# Search for getContainer or portal-related configuration in SelectTrigger
rg -n "getContainer\|getPopupContainer\|portal" src/SelectTrigger.tsx -B2 -A2Repository: react-component/select
Length of output: 48
🏁 Script executed:
# Search more broadly for getContainer, getPopupContainer, or portal in all source files
rg "getContainer\|getPopupContainer\|portal" src/ -nRepository: react-component/select
Length of output: 48
🏁 Script executed:
# Let's look at SelectTrigger.tsx to understand how it renders
head -150 src/SelectTrigger.tsxRepository: react-component/select
Length of output: 3862
🏁 Script executed:
# Check if rc-trigger is used and what its default behavior is
rg -n "rc-trigger" src/ -A3Repository: react-component/select
Length of output: 48
🏁 Script executed:
# Continue reading SelectTrigger.tsx to see Trigger component usage
sed -n '150,250p' src/SelectTrigger.tsxRepository: react-component/select
Length of output: 1454
🏁 Script executed:
# Look at the render call in SelectTrigger to see if getPopupContainer is passed to Trigger
sed -n '200,300p' src/SelectTrigger.tsxRepository: react-component/select
Length of output: 48
🏁 Script executed:
# Check the test for hints about how Select renders elements
grep -n "render(" tests/Accessibility.test.tsx | head -5Repository: react-component/select
Length of output: 299
🏁 Script executed:
# Check BaseSelect.tsx for default getPopupContainer configuration
rg -n "getPopupContainer" src/BaseSelect.tsx -B3 -A3Repository: react-component/select
Length of output: 124
🏁 Script executed:
# Look for where the id prop gets generated/used
rg -n "id.*_list\|selectId\|auto.*id" src/ -t tsx -t ts | head -20Repository: react-component/select
Length of output: 93
🏁 Script executed:
# Check the OptionList component to see how it gets its id
fd -e tsx -e ts | xargs grep -l "role=\"listbox\"" | head -5Repository: react-component/select
Length of output: 84
🏁 Script executed:
# Find BaseSelect file
fd BaseSelectRepository: react-component/select
Length of output: 107
🏁 Script executed:
# Check OptionList for how listbox id is generated
head -80 src/OptionList.tsxRepository: react-component/select
Length of output: 2467
🏁 Script executed:
# Search for getPopupContainer in the entire src directory
rg "getPopupContainer" src/ -B2 -A2Repository: react-component/select
Length of output: 1680
🏁 Script executed:
# Look at the full test function to see all render parameters
sed -n '326,345p' tests/Accessibility.test.tsxRepository: react-component/select
Length of output: 802
🏁 Script executed:
# Check OptionList for the id prop and how it renders the listbox
rg -n "role=\"listbox\"" src/ -B5 -A5Repository: react-component/select
Length of output: 616
🏁 Script executed:
# Check if rc-trigger has default getPopupContainer behavior
rg -n "Trigger" src/SelectTrigger.tsx | head -10Repository: react-component/select
Length of output: 596
🏁 Script executed:
# Let's run the test to see if it actually passes or fails
grep -n "aria-disabled should be set on disabled options" tests/Accessibility.test.tsxRepository: react-component/select
Length of output: 135
🏁 Script executed:
# Check how id is generated in BaseSelect
rg -n "useId\|const id" src/BaseSelect/ -A2Repository: react-component/select
Length of output: 48
🏁 Script executed:
# Look for where id comes from in OptionList
sed -n '35,65p' src/OptionList.tsxRepository: react-component/select
Length of output: 664
🏁 Script executed:
# Check the test setup to see if there's any id configuration
sed -n '1,30p' tests/Accessibility.test.tsxRepository: react-component/select
Length of output: 1057
🏁 Script executed:
# Search for rc-trigger default behavior with getPopupContainer in package.json
cat package.json | grep -A5 -B5 "rc-component/trigger"Repository: react-component/select
Length of output: 337
🏁 Script executed:
# Check how useBaseProps gets the id value
rg -n "useBaseProps\|selectId\|useId" src/BaseSelect/ -B2 -A5Repository: react-component/select
Length of output: 48
🏁 Script executed:
# Let's look at the full OptionList render to see where listbox is rendered
sed -n '260,290p' src/OptionList.tsxRepository: react-component/select
Length of output: 824
🏁 Script executed:
# Check if SelectTrigger passes getPopupContainer from BaseSelect
sed -n '400,500p' src/BaseSelect/index.tsxRepository: react-component/select
Length of output: 3373
🏁 Script executed:
# Find useBaseProps implementation
fd useBasePropsRepository: react-component/select
Length of output: 91
🏁 Script executed:
# Search for id generation in BaseSelect/index.tsx
rg -n "id.*=" src/BaseSelect/index.tsx | head -20Repository: react-component/select
Length of output: 270
🏁 Script executed:
# Check if there's any default getPopupContainer configuration
rg -n "getPopupContainer\s*=" src/ -B2 -A2Repository: react-component/select
Length of output: 761
🏁 Script executed:
# Let's look at where Trigger is rendered with getPopupContainer in BaseSelect
sed -n '550,650p' src/BaseSelect/index.tsxRepository: react-component/select
Length of output: 3143
🏁 Script executed:
# Look at useBaseProps to see how id is generated
cat src/hooks/useBaseProps.tsRepository: react-component/select
Length of output: 625
🏁 Script executed:
# Check if there's a default value for getPopupContainer in BaseSelect props
sed -n '1,150p' src/BaseSelect/index.tsx | grep -A20 "interface.*Props"Repository: react-component/select
Length of output: 1678
🏁 Script executed:
# Let's check the test render function to see if RTL setup affects portal rendering
grep -n "render\|setupTest\|beforeEach" tests/Accessibility.test.tsx | head -20Repository: react-component/select
Length of output: 568
🏁 Script executed:
# Search for where id is defined/initialized in BaseSelect/index.tsx
rg -n "const.*id\s*=" src/BaseSelect/index.tsx -A2Repository: react-component/select
Length of output: 48
🏁 Script executed:
# Let's look at the start of BaseSelect/index.tsx to see the full component signature
head -400 src/BaseSelect/index.tsx | tail -200Repository: react-component/select
Length of output: 5500
🏁 Script executed:
# Let's check if there's a default id or useId hook in the component
rg -n "useId\|getId\|generateId" src/ -B2 -A2Repository: react-component/select
Length of output: 48
🏁 Script executed:
# Check if id is required or has a default value in the BaseSelectPrivateProps
sed -n '1,200p' src/BaseSelect/index.tsx | grep -A5 "BaseSelectPrivateProps"Repository: react-component/select
Length of output: 500
🏁 Script executed:
# Let's check the Select.tsx component which wraps BaseSelect
head -100 src/Select.tsxRepository: react-component/select
Length of output: 3293
🏁 Script executed:
# Check how Select component uses useId
sed -n '40,150p' src/Select.tsxRepository: react-component/select
Length of output: 3670
🏁 Script executed:
# Let's also check if rc-trigger has default getPopupContainer behavior
rg -n "export.*Trigger" src/SelectTrigger.tsx -A20Repository: react-component/select
Length of output: 869
🏁 Script executed:
# Check if the test might actually be querying the document instead of just container
grep -n "document\|portal\|Trigger" tests/Accessibility.test.tsx | head -30Repository: react-component/select
Length of output: 494
🏁 Script executed:
# Let's see how Select.tsx generates id and passes it to BaseSelect
sed -n '100,250p' src/Select.tsxRepository: react-component/select
Length of output: 4584
🏁 Script executed:
# Check if there are any other tests in Accessibility.test.tsx that also use container.querySelectorAll
rg -n "container\.querySelectorAll\|container\.querySelector" tests/Accessibility.test.tsxRepository: react-component/select
Length of output: 48
🏁 Script executed:
# Let's verify if the portal is actually rendered by checking all references to `#virtual-select_list`
rg -n "virtual-select_list\|_list" tests/Accessibility.test.tsx -B3 -A3Repository: react-component/select
Length of output: 48
🏁 Script executed:
# Continue reading Select.tsx to see where useId is called
sed -n '180,350p' src/Select.tsxRepository: react-component/select
Length of output: 5326
🏁 Script executed:
# Let's check if the test is actually working or failing by looking for test runner configs
cat package.json | grep -A10 "jest\|test"Repository: react-component/select
Length of output: 990
🏁 Script executed:
# More specifically, let's see if there are any Trigger import or usage patterns in tests
rg -n "Trigger\|getPopupElement\|document\.querySelector" tests/Accessibility.test.tsxRepository: react-component/select
Length of output: 48
需要使用 document.querySelector 访问 Portal 中的下拉列表元素
同文件其他测试(第 183、226 行)均通过 document.querySelector('#virtual-select_list') 访问下拉内容。这表明 Select 组件的下拉列表通过 Portal 渲染到 document.body,而非 container 内部。
当前测试未传递 id 属性,Select 会通过 useId 自动生成一个随机 id(如 rc-select_12345),因此 container.querySelectorAll('[role="option"]') 无法访问到 Portal 中的选项元素,optionA 和 optionB 都会是 undefined。后续的 toHaveAttribute 断言可能返回不可靠的结果。
应参照其他测试的做法,显式传递 id 并通过 document.querySelector 查询:
🛡️ 建议的修复方案
+ const id = 'test-select';
const { container } = render(
<Select
+ id={id}
open
options={[
{ label: 'Option A', value: 'a' },
{ label: 'Option B', value: 'b', disabled: true },
{ label: 'Option C', value: 'c' },
]}
/>,
);
- const optionItems = container.querySelectorAll('[role="option"]');
+ const dropdown = document.querySelector(`#${id}_list`);
+ const optionItems = dropdown!.querySelectorAll('[role="option"]');
const optionA = Array.from(optionItems).find((el) => el.textContent === 'a');
const optionB = Array.from(optionItems).find((el) => el.textContent === 'b');
+ expect(optionA).toBeDefined();
+ expect(optionB).toBeDefined();
expect(optionA).not.toHaveAttribute('aria-disabled', 'true');
expect(optionB).toHaveAttribute('aria-disabled', 'true');🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/Accessibility.test.tsx` around lines 338 - 342, The test is querying
options from the test container but the Select renders its popup via a Portal
with an auto-generated id, so container.querySelectorAll('[role="option"]')
returns undefined; update the test to pass a stable id prop to the Select (e.g.,
id="virtual-select"), then query the portal using
document.querySelector(`#${'virtual-select'}_list`) (or
document.querySelector('#virtual-select_list')) to get optionItems, and then
find optionA/optionB from that NodeList before asserting aria-disabled on
optionA and optionB.
ref ant-design/ant-design#57036
Summary by CodeRabbit
发版说明
辅助功能改进
测试