Repository: ant-design/ant-design-mobile
Pull request #5465 (2022-07-13 02:02:47)

enhance(SearchBar): use aria labels to imporve a11y and add some tests


Patch:
diff --git a/src/components/input/input.tsx b/src/components/input/input.tsx
index 07bbfa570..9816cf161 100644
--- a/src/components/input/input.tsx
+++ b/src/components/input/input.tsx
@@ -7,6 +7,7 @@ import classNames from 'classnames'
 import { useIsomorphicLayoutEffect } from 'ahooks'
 import { bound } from '../../utils/bound'
 import { isIOS } from '../../utils/validate'
+import { useConfig } from '../config-provider'
 
 const classPrefix = `adm-input`
 
@@ -85,6 +86,7 @@ export const Input = forwardRef<InputRef, InputProps>((p, ref) => {
   const [hasFocus, setHasFocus] = useState(false)
   const compositionStartRef = useRef(false)
   const nativeInputRef = useRef<HTMLInputElement>(null)
+  const { locale } = useConfig()
 
   useImperativeHandle(ref, () => ({
     clear: () => {
@@ -192,6 +194,7 @@ export const Input = forwardRef<InputRef, InputProps>((p, ref) => {
         aria-valuenow={props['aria-valuenow']}
         aria-valuemax={props['aria-valuemax']}
         aria-valuemin={props['aria-valuemin']}
+        aria-label={props['aria-label']}
       />
       {shouldShowClear && (
         <div
@@ -209,6 +212,7 @@ export const Input = forwardRef<InputRef, InputProps>((p, ref) => {
               nativeInputRef.current?.blur()
             }
           }}
+          aria-label={locale.Input.clear}
         >
           <CloseCircleFill />
         </div>
diff --git a/src/components/search-bar/search-bar.tsx b/src/components/search-bar/search-bar.tsx
index c10921322..93ade31e8 100644
--- a/src/components/search-bar/search-bar.tsx
+++ b/src/components/search-bar/search-bar.tsx
@@ -144,6 +144,7 @@ export const SearchBar = forwardRef<SearchBarRef, SearchBarProps>((p, ref) => {
             inputRef.current?.blur()
             props.onSearch?.(value)
           }}
+          aria-label={locale.SearchBar.name}
         />
       </div>
       {renderCancelButton()}
diff --git a/src/locales/base.ts b/src/locales/base.ts
index cb7d51468..643f8b5a6 100644
--- a/src/locales/base.ts
+++ b/src/locales/base.ts
@@ -95,6 +95,9 @@ export const base = {
     failedToLoad: 'Failed to load',
     retry: 'Retry',
   },
+  Input: {
+    clear: 'clear',
+  },
   Mask: {
     name: 'Mask',
   },
@@ -106,6 +109,9 @@ export const base = {
     canRelease: 'Release to refresh immediately',
     complete: 'Refresh successful',
   },
+  SearchBar: {
+    name: 'Search Bar',
+  },
   Slider: {
     name: 'Slider',
   },
diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts
index b53d70832..8efda22cf 100644
--- a/src/locales/zh-CN.ts
+++ b/src/locales/zh-CN.ts
@@ -98,6 +98,9 @@ const zhCN = mergeLocale(base, {
     failedToLoad: '加载失败',
     retry: '重新加载',
   },
+  Input: {
+    clear: '清除',
+  },
   Mask: {
     name: '遮罩层',
   },
@@ -109,6 +112,9 @@ const zhCN = mergeLocale(base, {
     canRelease: '释放立即刷新',
     complete: '刷新成功',
   },
+  SearchBar: {
+    name: '搜索框',
+  },
   Slider: {
     name: '滑动输入条',
   },


Test patch:
diff --git a/src/components/config-provider/tests/__snapshots__/config-provider.test.tsx.snap b/src/components/config-provider/tests/__snapshots__/config-provider.test.tsx.snap
index 30b955a3d..68540be64 100644
--- a/src/components/config-provider/tests/__snapshots__/config-provider.test.tsx.snap
+++ b/src/components/config-provider/tests/__snapshots__/config-provider.test.tsx.snap
@@ -1060,9 +1060,11 @@ exports[`ConfigProvider should display the text as en 1`] = `
         </svg>
       </div>
       <div
+        aria-label="Search Bar"
         class="adm-input adm-search-bar-input"
       >
         <input
+          aria-label="Search Bar"
           class="adm-input-element"
           enterkeyhint="search"
           type="search"
@@ -2276,9 +2278,11 @@ exports[`ConfigProvider should display the text as es 1`] = `
         </svg>
       </div>
       <div
+        aria-label="Search Bar"
         class="adm-input adm-search-bar-input"
       >
         <input
+          aria-label="Search Bar"
           class="adm-input-element"
           enterkeyhint="search"
           type="search"
@@ -3492,9 +3496,11 @@ exports[`ConfigProvider should display the text as fa-IR 1`] = `
         </svg>
       </div>
       <div
+        aria-label="Search Bar"
         class="adm-input adm-search-bar-input"
       >
         <input
+          aria-label="Search Bar"
           class="adm-input-element"
           enterkeyhint="search"
           type="search"
@@ -4708,9 +4714,11 @@ exports[`ConfigProvider should display the text as fr-FR 1`] = `
         </svg>
       </div>
       <div
+        aria-label="Search Bar"
         class="adm-input adm-search-bar-input"
       >
         <input
+          aria-label="Search Bar"
           class="adm-input-element"
           enterkeyhint="search"
           type="search"
@@ -5924,9 +5932,11 @@ exports[`ConfigProvider should display the text as id-ID 1`] = `
         </svg>
       </div>
       <div
+        aria-label="Search Bar"
         class="adm-input adm-search-bar-input"
       >
         <input
+          aria-label="Search Bar"
           class="adm-input-element"
           enterkeyhint="search"
           type="search"
@@ -7140,9 +7150,11 @@ exports[`ConfigProvider should display the text as kk-KZ 1`] = `
         </svg>
       </div>
       <div
+        aria-label="Search Bar"
         class="adm-input adm-search-bar-input"
       >
         <input
+          aria-label="Search Bar"
           class="adm-input-element"
           enterkeyhint="search"
           type="search"
@@ -8356,9 +8368,11 @@ exports[`ConfigProvider should display the text as ko-KR 1`] = `
         </svg>
       </div>
       <div
+        aria-label="Search Bar"
         class="adm-input adm-search-bar-input"
       >
         <input
+          aria-label="Search Bar"
           class="adm-input-element"
           enterkeyhint="search"
           type="search"
@@ -9572,9 +9586,11 @@ exports[`ConfigProvider should display the text as zh-CH 1`] = `
         </svg>
       </div>
       <div
+        aria-label="搜索框"
         class="adm-input adm-search-bar-input"
       >
         <input
+          aria-label="搜索框"
           class="adm-input-element"
           enterkeyhint="search"
           type="search"
@@ -10788,9 +10804,11 @@ exports[`ConfigProvider should display the text as zh-HK 1`] = `
         </svg>
       </div>
       <div
+        aria-label="Search Bar"
         class="adm-input adm-search-bar-input"
       >
         <input
+          aria-label="Search Bar"
           class="adm-input-element"
           enterkeyhint="search"
           type="search"
@@ -12004,9 +12022,11 @@ exports[`ConfigProvider should display the text as zh-TW 1`] = `
         </svg>
       </div>
       <div
+        aria-label="Search Bar"
         class="adm-input adm-search-bar-input"
       >
         <input
+          aria-label="Search Bar"
           class="adm-input-element"
           enterkeyhint="search"
           type="search"
diff --git a/src/components/search-bar/tests/search-bar.test.tsx b/src/components/search-bar/tests/search-bar.test.tsx
new file mode 100644
index 000000000..f68cf4e11
--- /dev/null
+++ b/src/components/search-bar/tests/search-bar.test.tsx
@@ -0,0 +1,95 @@
+import React, { createRef } from 'react'
+import {
+  render,
+  testA11y,
+  fireEvent,
+  screen,
+  waitForElementToBeRemoved,
+  userEvent,
+  act,
+} from 'testing'
+import SearchBar, { SearchBarRef } from '..'
+
+const classPrefix = `adm-search-bar`
+
+describe('adm-search-bar', () => {
+  test('a11y', async () => {
+    await testA11y(<SearchBar />)
+  })
+
+  test('basic usage', () => {
+    const placeholder = 'search bar placeholder'
+    render(<SearchBar placeholder={placeholder} />)
+    const input = screen.getByPlaceholderText(placeholder)
+    expect(input).toBeInTheDocument()
+
+    fireEvent.focus(input)
+    expect(document.querySelector(`.${classPrefix}`)).toHaveClass(
+      `${classPrefix}-active`
+    )
+  })
+
+  test('show cancel button when `showCancelButton` is true and focused', () => {
+    const onCancel = jest.fn()
+    render(<SearchBar showCancelButton onCancel={onCancel} />)
+    const input = screen.getByRole('searchbox')
+    fireEvent.focus(input)
+    const cancel = screen.getByText('取消')
+    expect(cancel).toBeInTheDocument()
+    fireEvent.click(cancel)
+    waitForElementToBeRemoved(cancel)
+    expect(onCancel).toBeCalled()
+  })
+
+  test('show cancel button when `showCancelButton` is function', () => {
+    render(<SearchBar showCancelButton={() => true} />)
+    const cancel = screen.getByText('取消')
+    expect(cancel).toBeInTheDocument()
+    fireEvent.click(cancel)
+    expect(cancel).toBeInTheDocument()
+  })
+
+  test('not clear on cancel', async () => {
+    render(<SearchBar showCancelButton />)
+    const input = screen.getByRole('searchbox')
+    fireEvent.focus(input)
+    await userEvent.type(input, '12')
+    fireEvent.click(screen.getByText('取消'))
+    expect(input).toHaveValue('')
+  })
+
+  test('onSearch should be called', async () => {
+    const onSearch = jest.fn()
+    render(<SearchBar onSearch={onSearch} />)
+    const input = screen.getByRole('searchbox')
+    await userEvent.type(input, '12{enter}')
+    expect(onSearch).toBeCalledWith('12')
+  })
+
+  test('ref', async () => {
+    const ref = createRef<SearchBarRef>()
+    const onFocus = jest.fn()
+    const onBlur = jest.fn()
+    render(<SearchBar ref={ref} onFocus={onFocus} onBlur={onBlur} />)
+    const input = screen.getByRole('searchbox')
+    expect(ref.current?.nativeElement).toBeDefined()
+
+    act(() => {
+      ref.current?.focus()
+    })
+    expect(input).toHaveFocus()
+    expect(onFocus).toBeCalled()
+
+    await userEvent.type(input, '12')
+    act(() => {
+      ref.current?.clear()
+    })
+    expect(input).toHaveValue('')
+
+    act(() => {
+      ref.current?.blur()
+    })
+    expect(input).not.toHaveFocus()
+    expect(onBlur).toBeCalled()
+  })
+})