@vuepress-plume/plugin-search 1.0.0-rc.213 → 1.0.0-rc.214

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,8 +4,10 @@ import { onKeyStroke } from '@vueuse/core'
4
4
  import {
5
5
  computed,
6
6
  defineAsyncComponent,
7
+ nextTick,
7
8
  ref,
8
9
  toRef,
10
+ useTemplateRef,
9
11
  } from 'vue'
10
12
  import { useLocale } from '../composables/index.js'
11
13
  import SearchButton from './SearchButton.vue'
@@ -19,19 +21,49 @@ const SearchBox = defineAsyncComponent(() => import('./SearchBox.vue'))
19
21
 
20
22
  const showSearch = ref(false)
21
23
 
24
+ const searchButtonRef = useTemplateRef<{ focus: () => void }>('searchButton')
25
+
22
26
  const locale = useLocale(toRef(() => props.locales))
23
27
 
24
28
  /**
25
- * Text shown when JavaScript is disabled.
29
+ * Hand focus back to the trigger button.
30
+ *
31
+ * 把焦点归还给触发按钮。
26
32
  *
27
- * 禁用 JavaScript 时展示的提示文本。
33
+ * The search box unmounts its own focus trap, so without this the focused input
34
+ * disappears and focus falls back to `<body>`, forcing keyboard users to tab
35
+ * through the whole page again.
28
36
  *
29
- * `<noscript>` 仅在脚本被禁用时渲染,且必须是纯文本:启用脚本时浏览器会把
30
- * 其中的内容当作原始文本,若包含元素或 HTML 实体会造成水合不匹配。
31
- * `<noscript>` only renders with scripting disabled, and must stay plain text:
32
- * with scripting enabled browsers parse its content as raw text, so elements or
33
- * HTML entities would cause a hydration mismatch.
37
+ * 搜索框卸载时会一并销毁自身的焦点陷阱:若不显式归还,被聚焦的输入框随组件
38
+ * 消失后焦点会落到 `<body>`,键盘用户只能从页面第一个元素重新 Tab 一遍。
34
39
  */
40
+ function restoreSearchFocus() {
41
+ // 等待卸载完成(焦点陷阱销毁)后再归还焦点,否则会被其收尾逻辑覆盖。
42
+ // Wait until the unmount finished (the focus trap is destroyed), otherwise the
43
+ // trap teardown would override the focus we just set.
44
+ nextTick(() => searchButtonRef.value?.focus())
45
+ }
46
+
47
+ function closeSearch(options?: { restoreFocus?: boolean }) {
48
+ showSearch.value = false
49
+
50
+ if (options?.restoreFocus === false) {
51
+ nextTick(() => {
52
+ const main = document.querySelector<HTMLElement>('main, [role="main"]')
53
+ if (!main) {
54
+ searchButtonRef.value?.focus()
55
+ return
56
+ }
57
+ if (!main.hasAttribute('tabindex'))
58
+ main.setAttribute('tabindex', '-1')
59
+ main.focus({ preventScroll: true })
60
+ })
61
+ return
62
+ }
63
+
64
+ restoreSearchFocus()
65
+ }
66
+
35
67
  const noscriptText = computed(
36
68
  () => locale.value.noscriptText || 'Search is unavailable without JavaScript.',
37
69
  )
@@ -79,11 +111,11 @@ function isEditingContent(event: KeyboardEvent): boolean {
79
111
  v-if="showSearch"
80
112
  :locales="locales"
81
113
  :options="options"
82
- @close="showSearch = false"
114
+ @close="closeSearch"
83
115
  />
84
116
 
85
117
  <div id="local-search">
86
- <SearchButton :locales="locales" @click="showSearch = true" />
118
+ <SearchButton ref="searchButton" :locales="locales" :expanded="showSearch" @click="showSearch = true" />
87
119
  </div>
88
120
 
89
121
  <noscript>{{ noscriptText }}</noscript>
@@ -96,8 +128,6 @@ function isEditingContent(event: KeyboardEvent): boolean {
96
128
  align-items: center;
97
129
  }
98
130
 
99
- /* 仅在禁用 JavaScript 时可见的降级提示。
100
- Fallback hint that is only visible when JavaScript is disabled. */
101
131
  .search-wrapper noscript {
102
132
  font-size: 13px;
103
133
  line-height: 1.4;
@@ -22,8 +22,10 @@ import {
22
22
  ref,
23
23
  shallowRef,
24
24
  toRef,
25
+ useId,
25
26
  watch,
26
27
  } from 'vue'
28
+ import { isNavigationFailure, NavigationFailureType } from 'vue-router'
27
29
  import { usePageLang, useRouteLocale, useRouter, withBase } from 'vuepress/client'
28
30
  import { createTokenizer } from '../../shared/index.js'
29
31
  import { loadSearchIndexJSON, useLocale } from '../composables/index.js'
@@ -37,7 +39,11 @@ const { locales, options } = defineProps<{
37
39
  }>()
38
40
 
39
41
  const emit = defineEmits<{
40
- (e: 'close'): void
42
+ // `restoreFocus: false` 表示关闭是因为页面已切换到搜索结果,焦点不应归还给触发按钮。
43
+ //
44
+ // `restoreFocus: false` means the box is closing because the page really moved to
45
+ // the result, so focus must not go back to the trigger button.
46
+ (e: 'close', options?: { restoreFocus?: boolean }): void
41
47
  }>()
42
48
 
43
49
  const routeLocale = useRouteLocale()
@@ -47,12 +53,21 @@ const lang = usePageLang()
47
53
  const el = shallowRef<HTMLElement>()
48
54
  const resultsEl = shallowRef<HTMLElement>()
49
55
 
56
+ const uid = useId()
57
+ const labelId = `${uid}-label`
58
+ const inputId = `${uid}-input`
59
+ const listId = `${uid}-list`
60
+
61
+ function optionId(index: number) {
62
+ return `${uid}-option-${index}`
63
+ }
64
+
50
65
  interface Result {
51
66
  title: string
52
67
  titles: string[]
53
68
  }
54
69
 
55
- const { activate } = useFocusTrap(el, { immediate: true })
70
+ const { activate } = useFocusTrap(el, { immediate: true, returnFocusOnDeactivate: false })
56
71
 
57
72
  const isSearchIndexLoading = ref(false)
58
73
  const isSearching = ref(false)
@@ -129,8 +144,8 @@ const searchIndex = computedAsync(async (onCancel) => {
129
144
  prefix: true,
130
145
  boost: { title: 4, text: 2, titles: 1 },
131
146
  tokenize,
147
+ ...options.miniSearch?.searchOptions,
132
148
  },
133
- ...options.miniSearch?.searchOptions,
134
149
  ...options.miniSearch?.options,
135
150
  }),
136
151
  )
@@ -160,6 +175,16 @@ watch(filterText, () => {
160
175
  enableNoResults.value = false
161
176
  })
162
177
 
178
+ const searchStatusText = computed(() => {
179
+ if (searchIndexError.value)
180
+ return searchIndexError.value
181
+ if (!filterText.value || !enableNoResults.value)
182
+ return ''
183
+ if (!results.value.length)
184
+ return `${locale.value.noResultsText || 'No results for'} "${filterText.value}"`
185
+ return `${results.value.length} results`
186
+ })
187
+
163
188
  const mark = computedAsync(async () => {
164
189
  if (!resultsEl.value)
165
190
  return
@@ -197,10 +222,10 @@ watchDebounced(
197
222
  if (canceled)
198
223
  return
199
224
 
200
- await new Promise((r) => {
225
+ await new Promise((done) => {
201
226
  mark.value?.unmark({
202
227
  done: () => {
203
- mark.value?.markRegExp(formMarkRegex(terms), { done: r })
228
+ mark.value?.markRegExp(formMarkRegex(terms), { done })
204
229
  },
205
230
  })
206
231
  })
@@ -236,6 +261,10 @@ function onSearchBarClick(event: PointerEvent) {
236
261
  const selectedIndex = ref(-1)
237
262
  const disableMouseOver = ref(false)
238
263
 
264
+ const activeDescendantId = computed(() =>
265
+ results.value[selectedIndex.value] ? optionId(selectedIndex.value) : undefined,
266
+ )
267
+
239
268
  watch(results, (r) => {
240
269
  selectedIndex.value = r.length ? 0 : -1
241
270
  scrollToSelectedResult()
@@ -264,6 +293,12 @@ function isMacCtrlShortcut(event: KeyboardEvent) {
264
293
 
265
294
  function selectPreviousResult(event: KeyboardEvent) {
266
295
  event.preventDefault()
296
+
297
+ if (!results.value.length) {
298
+ selectedIndex.value = -1
299
+ return
300
+ }
301
+
267
302
  selectedIndex.value--
268
303
  if (selectedIndex.value < 0)
269
304
  selectedIndex.value = results.value.length - 1
@@ -274,6 +309,12 @@ function selectPreviousResult(event: KeyboardEvent) {
274
309
 
275
310
  function selectNextResult(event: KeyboardEvent) {
276
311
  event.preventDefault()
312
+
313
+ if (!results.value.length) {
314
+ selectedIndex.value = -1
315
+ return
316
+ }
317
+
277
318
  selectedIndex.value++
278
319
  if (selectedIndex.value >= results.value.length)
279
320
  selectedIndex.value = 0
@@ -310,17 +351,33 @@ const router = useRouter()
310
351
  let closedByPopstate = false
311
352
 
312
353
  /**
313
- * Whether the search box is being closed because a result is being opened.
354
+ * Whether the URL leaves the current page because a result is being opened, so
355
+ * unmount must not roll back the temporary history entry pushed on mount.
314
356
  *
315
- * 搜索框是否因跳转到搜索结果而关闭。
316
- *
317
- * In that case the URL must stay on the target page, so history must not be
318
- * rolled back on unmount.
319
- *
320
- * 此时 URL 必须停留在目标页面,卸载时不能回退历史。
357
+ * 为 true 表示 URL 会离开当前页(搜索结果导航在途、已成功或被更新的导航接替),
358
+ * 卸载时不能回退挂载时压入的临时历史条目。
321
359
  */
322
360
  let navigatingToResult = false
323
361
 
362
+ /**
363
+ * Open a search result: navigate to the target page, then close the box.
364
+ *
365
+ * 打开搜索结果:跳转到目标页面后再关闭搜索框。
366
+ */
367
+ async function openResult(id: string) {
368
+ navigatingToResult = true
369
+ // 用 replace 顶替挂载时 pushState 的临时历史条目,避免从结果页返回时多一次 Back。
370
+ const navigation = router.replace(id)
371
+
372
+ const navigated = await navigation.then(
373
+ failure => failure === undefined || isNavigationFailure(failure, NavigationFailureType.cancelled),
374
+ () => false,
375
+ )
376
+
377
+ navigatingToResult = navigated
378
+ emit('close', { restoreFocus: !navigated })
379
+ }
380
+
324
381
  onKeyStroke('Enter', (e) => {
325
382
  if (e.isComposing)
326
383
  return
@@ -334,14 +391,8 @@ onKeyStroke('Enter', (e) => {
334
391
  return
335
392
  }
336
393
 
337
- if (selectedPackage) {
338
- navigatingToResult = true
339
- // 用 replace 顶替挂载时 pushState 的临时历史条目,避免从结果页返回时多一次 Back。
340
- // Replace the temporary history entry pushed on mount, so returning from the
341
- // result page does not require an extra Back press.
342
- router.replace(selectedPackage.id)
343
- emit('close')
344
- }
394
+ if (selectedPackage)
395
+ openResult(selectedPackage.id)
345
396
  })
346
397
 
347
398
  onKeyStroke('Escape', () => {
@@ -364,6 +415,8 @@ useEventListener('popstate', (event) => {
364
415
  /** Lock body */
365
416
  const isLocked = useScrollLock(typeof document !== 'undefined' ? document.body : null)
366
417
 
418
+ let appEl: Element | null = null
419
+
367
420
  onMounted(() => {
368
421
  nextTick(() => {
369
422
  isLocked.value = true
@@ -371,15 +424,19 @@ onMounted(() => {
371
424
  })
372
425
  })
373
426
 
427
+ onMounted(() => {
428
+ appEl = document.querySelector('#app')
429
+ appEl?.setAttribute('inert', '')
430
+ })
431
+
432
+ onBeforeUnmount(() => {
433
+ appEl?.removeAttribute('inert')
434
+ })
435
+
374
436
  onBeforeUnmount(() => {
375
437
  isLocked.value = false
376
438
  // 配对挂载时的 pushState,避免反复开关搜索框累积历史条目。
377
439
  // 若该条目已被浏览器后退键(popstate)消费,或正在跳转到搜索结果,则不能回退。
378
- //
379
- // Pair the pushState made on mount so repeated open/close does not pile up
380
- // history entries. Skip the rollback when the entry was already consumed by
381
- // the browser back button (popstate), or when opening a result (the URL must
382
- // stay on the target page).
383
440
  if (!closedByPopstate && !navigatingToResult)
384
441
  window.history.back()
385
442
  })
@@ -404,12 +461,11 @@ function formMarkRegex(terms: Set<string>) {
404
461
  }
405
462
 
406
463
  function selectedClick(e: MouseEvent, p: SearchResult & Result) {
464
+ if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey)
465
+ return
466
+
407
467
  e.preventDefault()
408
- navigatingToResult = true
409
- // 同 Enter 键路径:replace 消费掉挂载时压入的临时历史条目。
410
- // Same as the Enter path: replace consumes the temp entry pushed on mount.
411
- router.replace(p.id)
412
- emit('close')
468
+ openResult(p.id)
413
469
  }
414
470
  </script>
415
471
 
@@ -417,16 +473,23 @@ function selectedClick(e: MouseEvent, p: SearchResult & Result) {
417
473
  <Teleport to="body">
418
474
  <div
419
475
  ref="el"
420
- role="button"
421
- :aria-owns="results?.length ? 'localsearch-list' : undefined"
422
- aria-expanded="true"
423
- aria-haspopup="listbox"
424
- aria-labelledby="localsearch-label"
476
+ role="dialog"
477
+ aria-modal="true"
478
+ :aria-label="buttonText"
425
479
  class="VPLocalSearchBox"
426
480
  >
481
+ <!-- eslint-disable-next-line vue-a11y/no-static-element-interactions, vue-a11y/click-events-have-key-events -->
427
482
  <div class="backdrop" @click="$emit('close')" />
428
483
 
429
484
  <div class="shell">
485
+ <div
486
+ class="sr-only"
487
+ role="status"
488
+ aria-live="polite"
489
+ aria-atomic="true"
490
+ >
491
+ {{ searchStatusText }}
492
+ </div>
430
493
  <form
431
494
  class="search-bar"
432
495
  @pointerup="onSearchBarClick($event)"
@@ -434,15 +497,18 @@ function selectedClick(e: MouseEvent, p: SearchResult & Result) {
434
497
  >
435
498
  <!-- eslint-disable vue-a11y/label-has-for -->
436
499
  <label
437
- id="localsearch-label"
500
+ :id="labelId"
438
501
  :title="buttonText"
439
- for="localsearch-input"
502
+ :for="inputId"
440
503
  >
441
504
  <SearchIcon class="search-icon" />
505
+ <span class="sr-only">{{ buttonText }}</span>
442
506
  </label>
443
507
  <div class="search-actions before">
444
508
  <button
509
+ type="button"
445
510
  class="back-button"
511
+ :aria-label="locale.backButtonTitle"
446
512
  :title="locale.backButtonTitle"
447
513
  @click="$emit('close')"
448
514
  >
@@ -450,20 +516,23 @@ function selectedClick(e: MouseEvent, p: SearchResult & Result) {
450
516
  </button>
451
517
  </div>
452
518
  <input
453
- id="localsearch-input"
519
+ :id="inputId"
454
520
  ref="searchInput"
455
521
  v-model="filterText"
456
522
  :placeholder="buttonText"
457
- aria-labelledby="localsearch-label"
523
+ role="combobox"
524
+ aria-autocomplete="list"
525
+ :aria-expanded="results.length > 0 ? 'true' : 'false'"
526
+ :aria-controls="listId"
527
+ :aria-labelledby="labelId"
528
+ :aria-activedescendant="activeDescendantId"
458
529
  class="search-input"
459
530
  >
460
531
  <div class="search-actions">
461
532
  <span
462
533
  class="search-loading"
463
534
  :class="{ active: showSearchSpinner }"
464
- :role="showSearchSpinner ? 'status' : undefined"
465
- aria-live="polite"
466
- :aria-label="showSearchSpinner ? 'Loading search results' : undefined"
535
+ aria-hidden="true"
467
536
  />
468
537
  <button
469
538
  class="clear-button"
@@ -478,40 +547,42 @@ function selectedClick(e: MouseEvent, p: SearchResult & Result) {
478
547
  </form>
479
548
 
480
549
  <ul
481
- :id="results?.length ? 'localsearch-list' : undefined"
550
+ :id="listId"
482
551
  ref="resultsEl"
483
- :role="results?.length ? 'listbox' : undefined"
484
- :aria-labelledby="results?.length ? 'localsearch-label' : undefined"
552
+ role="listbox"
553
+ :aria-labelledby="labelId"
485
554
  class="results"
555
+ tabindex="-1"
486
556
  @mousemove="disableMouseOver = false"
487
557
  >
488
558
  <li
489
559
  v-for="(p, index) in results"
560
+ :id="optionId(index)"
490
561
  :key="p.id"
491
562
  role="option"
492
563
  :aria-selected="selectedIndex === index ? 'true' : 'false'"
564
+ tabindex="-1"
565
+ @mouseenter="!disableMouseOver && (selectedIndex = index)"
566
+ @focusin="selectedIndex = index"
493
567
  >
494
568
  <a
495
- :href="withBase(p.id)"
496
569
  class="result"
497
570
  :class="{
498
571
  selected: selectedIndex === index,
499
572
  }"
500
- :aria-label="[...p.titles, p.title].join(' > ')"
501
- @mouseenter="!disableMouseOver && (selectedIndex = index)"
502
- @focusin="selectedIndex = index"
573
+ :href="withBase(p.id)"
503
574
  @click="selectedClick($event, p)"
504
575
  >
505
576
  <div>
506
577
  <div class="titles">
507
- <span class="title-icon">#</span>
578
+ <span class="title-icon" aria-hidden="true">#</span>
508
579
  <span
509
580
  v-for="(t, i) in p.titles"
510
581
  :key="i"
511
582
  class="title"
512
583
  >
513
584
  <span class="text" v-html="t" />
514
- <svg width="18" height="18" viewBox="0 0 24 24">
585
+ <svg width="18" height="18" viewBox="0 0 24 24" aria-hidden="true">
515
586
  <path
516
587
  fill="none"
517
588
  stroke="currentColor"
@@ -529,24 +600,24 @@ function selectedClick(e: MouseEvent, p: SearchResult & Result) {
529
600
  </div>
530
601
  </a>
531
602
  </li>
532
- <li
533
- v-if="searchIndexError"
534
- class="no-results error"
535
- role="alert"
536
- >
537
- {{ searchIndexError }}
538
- </li>
539
- <li
540
- v-else-if="filterText && !results.length && enableNoResults"
541
- class="no-results"
542
- >
543
- {{ locale.noResultsText }} "<strong>{{ filterText }}</strong>"
544
- </li>
545
603
  </ul>
546
604
 
605
+ <div
606
+ v-if="searchIndexError"
607
+ class="no-results error"
608
+ >
609
+ {{ searchIndexError }}
610
+ </div>
611
+ <div
612
+ v-else-if="filterText && !results.length && enableNoResults"
613
+ class="no-results"
614
+ >
615
+ {{ locale.noResultsText }} "<strong>{{ filterText }}</strong>"
616
+ </div>
617
+
547
618
  <div class="search-keyboard-shortcuts">
548
619
  <span>
549
- <kbd :aria-label="locale.footer?.navigateUpKeyAriaLabel ?? ''">
620
+ <kbd :aria-label="locale.footer?.navigateUpKeyAriaLabel || 'up arrow'">
550
621
  <svg width="14" height="14" viewBox="0 0 24 24">
551
622
  <path
552
623
  fill="none"
@@ -558,7 +629,7 @@ function selectedClick(e: MouseEvent, p: SearchResult & Result) {
558
629
  />
559
630
  </svg>
560
631
  </kbd>
561
- <kbd :aria-label="locale.footer?.navigateDownKeyAriaLabel ?? ''">
632
+ <kbd :aria-label="locale.footer?.navigateDownKeyAriaLabel || 'down arrow'">
562
633
  <svg width="14" height="14" viewBox="0 0 24 24">
563
634
  <path
564
635
  fill="none"
@@ -573,7 +644,7 @@ function selectedClick(e: MouseEvent, p: SearchResult & Result) {
573
644
  {{ locale.footer?.navigateText ?? '' }}
574
645
  </span>
575
646
  <span>
576
- <kbd :aria-label="locale.footer?.selectKeyAriaLabel ?? ''">
647
+ <kbd :aria-label="locale.footer?.selectKeyAriaLabel || 'enter'">
577
648
  <svg width="14" height="14" viewBox="0 0 24 24">
578
649
  <g
579
650
  fill="none"
@@ -590,7 +661,7 @@ function selectedClick(e: MouseEvent, p: SearchResult & Result) {
590
661
  {{ locale.footer?.selectText ?? '' }}
591
662
  </span>
592
663
  <span>
593
- <kbd :aria-label="locale.footer?.closeKeyAriaLabel ?? ''">esc</kbd>
664
+ <kbd :aria-label="locale.footer?.closeKeyAriaLabel || 'esc'">esc</kbd>
594
665
  {{ locale.footer?.closeText ?? '' }}
595
666
  </span>
596
667
  </div>
@@ -632,6 +703,7 @@ svg {
632
703
 
633
704
  .shell {
634
705
  position: relative;
706
+ box-sizing: border-box;
635
707
  display: flex;
636
708
  flex-direction: column;
637
709
  gap: 16px;
@@ -647,7 +719,7 @@ svg {
647
719
  @media (max-width: 767px) {
648
720
  .shell {
649
721
  width: 100vw;
650
- height: 100vh;
722
+ height: 100dvh;
651
723
  max-height: none;
652
724
  margin: 0;
653
725
  border-radius: 0;
@@ -659,7 +731,7 @@ svg {
659
731
  align-items: center;
660
732
  padding: 0 12px;
661
733
  cursor: text;
662
- border: 1px solid var(--vp-c-divider);
734
+ border: 1px solid var(--vp-c-text-2);
663
735
  border-radius: 4px;
664
736
  }
665
737
 
@@ -744,7 +816,7 @@ svg {
744
816
  height: 18px;
745
817
  margin: 8px;
746
818
  visibility: hidden;
747
- border: 2px solid var(--vp-c-divider);
819
+ border: 2px solid var(--vp-c-text-2);
748
820
  border-top-color: var(--vp-c-brand-1);
749
821
  border-radius: 50%;
750
822
  }
@@ -828,6 +900,10 @@ svg {
828
900
  list-style: none;
829
901
  }
830
902
 
903
+ .results:empty {
904
+ display: none;
905
+ }
906
+
831
907
  .result {
832
908
  display: flex;
833
909
  gap: 8px;
@@ -835,10 +911,14 @@ svg {
835
911
  line-height: 1rem;
836
912
  border: solid 2px var(--vp-mini-search-result-border);
837
913
  border-radius: 4px;
838
- outline: none;
839
914
  transition: none;
840
915
  }
841
916
 
917
+ .result:focus-visible {
918
+ outline: 2px solid var(--vp-c-brand-1);
919
+ outline-offset: 2px;
920
+ }
921
+
842
922
  .result > div {
843
923
  width: 100%;
844
924
  margin: 12px;
@@ -872,8 +952,7 @@ svg {
872
952
 
873
953
  .title-icon {
874
954
  font-weight: 500;
875
- color: var(--vp-c-brand-1);
876
- opacity: 0.5;
955
+ color: var(--vp-c-text-2);
877
956
  }
878
957
 
879
958
  .title :deep(svg) {
@@ -1,19 +1,46 @@
1
1
  <script lang="ts" setup>
2
2
  import type { SearchBoxLocales } from '../../shared/index.js'
3
- import { toRef } from 'vue'
3
+ import { toRef, useTemplateRef } from 'vue'
4
4
  import { useLocale } from '../composables/index.js'
5
5
 
6
- const { locales } = defineProps<{
6
+ const { locales, expanded } = defineProps<{
7
7
  locales: SearchBoxLocales
8
+ expanded?: boolean
8
9
  }>()
9
10
 
10
11
  const locale = useLocale(toRef(() => locales))
12
+
13
+ const buttonEl = useTemplateRef<HTMLButtonElement>('button')
14
+
15
+ /**
16
+ * Move focus back to the trigger button.
17
+ *
18
+ * 将焦点移回触发按钮。
19
+ *
20
+ * Exposed so the search box can hand focus back to this trigger after closing,
21
+ * which keeps keyboard users from being thrown back to the top of the page.
22
+ *
23
+ * 暴露该方法,使搜索框在关闭后能把焦点归还给该触发按钮,
24
+ * 避免键盘用户的焦点被弹回页面顶部。
25
+ */
26
+ function focus() {
27
+ buttonEl.value?.focus({ preventScroll: true })
28
+ }
29
+
30
+ defineExpose({ focus })
11
31
  </script>
12
32
 
13
33
  <template>
14
- <button type="button" class="mini-search mini-search-button" :aria-label="locale.placeholder">
34
+ <button
35
+ ref="button"
36
+ type="button"
37
+ class="mini-search mini-search-button"
38
+ :aria-label="locale.placeholder"
39
+ aria-haspopup="dialog"
40
+ :aria-expanded="expanded ? 'true' : 'false'"
41
+ >
15
42
  <span class="mini-search-button-container">
16
- <span class="mini-search-search-icon vpi-mini-search" aria-label="search icon" />
43
+ <span class="mini-search-search-icon vpi-mini-search" aria-hidden="true" />
17
44
  <span class="mini-search-button-placeholder">{{ locale.placeholder }}</span>
18
45
  </span>
19
46
  <span class="mini-search-button-keys">
@@ -40,13 +67,13 @@ const locale = useLocale(toRef(() => locales))
40
67
  background: transparent;
41
68
  }
42
69
 
43
- .mini-search-button:focus {
44
- outline: 1px dotted;
45
- outline: 5px auto -webkit-focus-ring-color;
70
+ .mini-search-button:focus-visible {
71
+ outline: var(--vp-focus-ring-width) solid var(--vp-focus-ring-color);
72
+ outline-offset: var(--vp-focus-ring-offset);
46
73
  }
47
74
 
48
75
  .mini-search-button:focus:not(:focus-visible) {
49
- outline: none !important;
76
+ outline: none;
50
77
  }
51
78
 
52
79
  @media (min-width: 768px) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vuepress-plume/plugin-search",
3
3
  "type": "module",
4
- "version": "1.0.0-rc.213",
4
+ "version": "1.0.0-rc.214",
5
5
  "description": "The Plugin for VuePress 2 - local search",
6
6
  "author": "pengzhanbo <volodymyr@foxmail.com>",
7
7
  "license": "MIT",
@@ -44,12 +44,13 @@
44
44
  "@vueuse/core": "^15.0.0",
45
45
  "@vueuse/integrations": "^15.0.0",
46
46
  "chokidar": "5.0.0",
47
- "focus-trap": "^8.2.2",
47
+ "focus-trap": "^8.2.3",
48
48
  "mark.js": "^8.11.1",
49
49
  "minisearch": "^7.2.0",
50
50
  "p-map": "^7.0.8",
51
51
  "sanitize-html": "^2.18.0",
52
- "vue": "^3.5.43"
52
+ "vue": "^3.5.43",
53
+ "vue-router": "^5.4.0"
53
54
  },
54
55
  "publishConfig": {
55
56
  "access": "public",