Skip to content

Commit 7264cdb

Browse files
heiskrCopilotEbonsignori
authored
🦾 Fix search overlay focus restoration on close (#60299)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Evan Bonsignori <evanabonsignori@gmail.com>
1 parent a9cc23a commit 7264cdb

File tree

2 files changed

+62
-53
lines changed

2 files changed

+62
-53
lines changed

‎src/frame/components/page-header/Header.tsx‎

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,23 @@ export const Header = () => {
4343
const isEarlyAccessPage = currentProduct && currentProduct.id === 'early-access'
4444
const { width } = useInnerWindowWidth()
4545
const returnFocusRef = useRef(null)
46-
const searchButtonRef = useRef<HTMLButtonElement>(null)
46+
const searchButtonRefLarge = useRef<HTMLButtonElement>(null)
47+
const searchButtonRefSmall = useRef<HTMLButtonElement>(null)
4748
const { initializeCTA } = useCTAPopoverContext()
4849
const { isSearchOpen, setIsSearchOpen } = useSearchOverlayContext()
4950

51+
// The lg breakpoint (1012px) determines which search button is visible.
52+
// Pass the correct ref to SearchOverlayContainer so Primer's Overlay
53+
// restores focus to the visible trigger element on close.
54+
const isLargeViewport = width !== null && width >= 1012
55+
const searchButtonRef = isLargeViewport ? searchButtonRefLarge : searchButtonRefSmall
56+
5057
const SearchButtonLarge: JSX.Element = (
5158
<SearchBarButton
5259
isSearchOpen={isSearchOpen}
5360
setIsSearchOpen={setIsSearchOpen}
5461
params={params}
55-
searchButtonRef={searchButtonRef}
62+
searchButtonRef={searchButtonRefLarge}
5663
instanceId="large"
5764
/>
5865
)
@@ -62,7 +69,7 @@ export const Header = () => {
6269
isSearchOpen={isSearchOpen}
6370
setIsSearchOpen={setIsSearchOpen}
6471
params={params}
65-
searchButtonRef={searchButtonRef}
72+
searchButtonRef={searchButtonRefSmall}
6673
instanceId="small"
6774
/>
6875
)
@@ -90,17 +97,18 @@ export const Header = () => {
9097
return () => window.removeEventListener('keydown', close)
9198
}, [])
9299

93-
// For the UI in smaller browser widths, and focus the picker menu button when the search
94-
// input is closed.
100+
// For the UI in smaller browser widths, focus the picker menu button when
101+
// the sidebar is closed (not when the search overlay is closed — the
102+
// overlay's returnFocusRef handles focus restoration to the search button).
95103
useEffect(() => {
96-
if (!isSearchOpen && isMounted.current && menuButtonRef.current) {
104+
if (!isSidebarOpen && isMounted.current && menuButtonRef.current) {
97105
menuButtonRef.current.focus()
98106
}
99107

100108
if (!isMounted.current) {
101109
isMounted.current = true
102110
}
103-
}, [isSearchOpen])
111+
}, [isSidebarOpen])
104112

105113
// When the sidebar overlay is opened, prevent the main content from being
106114
// scrollable.

‎src/search/components/input/SearchBarButton.tsx‎

Lines changed: 47 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -52,55 +52,56 @@ export function SearchBarButton({
5252

5353
return (
5454
<>
55-
{/* We don't want to show the input when overlay is open */}
56-
{!isSearchOpen ? (
57-
<>
58-
{/* On mobile only the IconButton is shown */}
59-
<IconButton
60-
data-testid="mobile-search-button"
61-
data-instance={instanceId}
62-
ref={searchButtonRef}
63-
className={styles.searchIconButton}
64-
onClick={handleClick}
65-
tabIndex={0}
66-
aria-label={t('search.input.placeholder_no_icon')}
67-
icon={SearchIcon}
68-
/>
69-
{/* On large and up the SearchBarButton is shown */}
70-
<button
71-
data-testid="search"
72-
data-instance={instanceId}
73-
tabIndex={0}
74-
aria-label={t('search.input.placeholder_no_icon')}
75-
className={styles.searchInputButton}
76-
onKeyDown={handleKeyDown}
77-
onClick={handleClick}
78-
ref={searchButtonRef}
55+
{/* Keep buttons in DOM even when overlay is open so returnFocusRef
56+
can restore focus to the trigger element on overlay close. */}
57+
<div style={isSearchOpen ? { visibility: 'hidden', position: 'absolute' } : undefined}>
58+
{/* On mobile only the IconButton is shown. The "small" instance is
59+
the mobile one, so it gets the ref on the icon button. */}
60+
<IconButton
61+
data-testid="mobile-search-button"
62+
data-instance={instanceId}
63+
ref={instanceId === 'small' ? searchButtonRef : undefined}
64+
className={styles.searchIconButton}
65+
onClick={handleClick}
66+
tabIndex={isSearchOpen ? -1 : 0}
67+
aria-label={t('search.input.placeholder_no_icon')}
68+
icon={SearchIcon}
69+
/>
70+
{/* On large and up the SearchBarButton is shown. The "large" instance
71+
is the desktop one, so it gets the ref on the desktop button. */}
72+
<button
73+
data-testid="search"
74+
data-instance={instanceId}
75+
tabIndex={isSearchOpen ? -1 : 0}
76+
aria-label={t('search.input.placeholder_no_icon')}
77+
className={styles.searchInputButton}
78+
onKeyDown={handleKeyDown}
79+
onClick={handleClick}
80+
ref={instanceId === 'large' ? searchButtonRef : undefined}
81+
>
82+
{/* Styled to look like an input */}
83+
<div
84+
className={cx('d-flex align-items-center flex-grow-1', styles.searchInputContainer)}
85+
aria-hidden
86+
tabIndex={-1}
7987
>
80-
{/* Styled to look like an input */}
81-
<div
82-
className={cx('d-flex align-items-center flex-grow-1', styles.searchInputContainer)}
83-
aria-hidden
84-
tabIndex={-1}
88+
<span
89+
className={cx(styles.queryText, !urlSearchInputQuery ? styles.placeholder : null)}
8590
>
86-
<span
87-
className={cx(styles.queryText, !urlSearchInputQuery ? styles.placeholder : null)}
88-
>
89-
{urlSearchInputQuery ? (
90-
urlSearchInputQuery
91-
) : (
92-
<>
93-
<span className={styles.placeholderText}>{placeHolderElements}</span>
94-
</>
95-
)}
96-
</span>
97-
</div>
98-
<span className={styles.searchIconContainer} aria-hidden tabIndex={-1}>
99-
<SearchIcon />
91+
{urlSearchInputQuery ? (
92+
urlSearchInputQuery
93+
) : (
94+
<>
95+
<span className={styles.placeholderText}>{placeHolderElements}</span>
96+
</>
97+
)}
10098
</span>
101-
</button>
102-
</>
103-
) : null}
99+
</div>
100+
<span className={styles.searchIconContainer} aria-hidden tabIndex={-1}>
101+
<SearchIcon />
102+
</span>
103+
</button>
104+
</div>
104105
</>
105106
)
106107
}

0 commit comments

Comments
 (0)