Skip to content

Commit ea10dbb

Browse files
authored
NTP: Make omnibar always overflow (#1866)
* Refactor Omnibar to use SuggestionsProvider context * Don't animate omnibar height when suggestions list changes * Inline Container into Omnibar, since they're so tightly coupled * Refactor so that view-related concerns are in SearchForm and data-related concerns are in SearchFormProvider/useSuggestions * Fix focus ring and use spacing vars * DRY popup and field styling * Rephrase comment * Strip newlines closer to where it matters * Update ResizingContainer props type to reflect that style prop won't work
1 parent 979caac commit ea10dbb

File tree

10 files changed

+338
-291
lines changed

10 files changed

+338
-291
lines changed

special-pages/pages/new-tab/app/omnibar/components/Container.js

Lines changed: 0 additions & 47 deletions
This file was deleted.

special-pages/pages/new-tab/app/omnibar/components/Container.module.css

Lines changed: 0 additions & 27 deletions
This file was deleted.

special-pages/pages/new-tab/app/omnibar/components/Omnibar.js

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import { useContext, useState } from 'preact/hooks';
33
import { LogoStacked } from '../../components/Icons';
44
import { useTypedTranslationWith } from '../../types';
55
import { AiChatForm } from './AiChatForm';
6-
import { Container } from './Container';
76
import styles from './Omnibar.module.css';
87
import { OmnibarContext } from './OmnibarProvider';
8+
import { ResizingContainer } from './ResizingContainer';
99
import { SearchForm } from './SearchForm';
10+
import { SearchFormProvider } from './SearchFormProvider';
11+
import { SuggestionsList } from './SuggestionsList';
1012
import { TabSwitcher } from './TabSwitcher';
1113

1214
/**
@@ -24,6 +26,7 @@ import { TabSwitcher } from './TabSwitcher';
2426
*/
2527
export function Omnibar({ mode, setMode, enableAi }) {
2628
const { t } = useTypedTranslationWith(/** @type {Strings} */ ({}));
29+
2730
const [query, setQuery] = useState(/** @type {String} */ (''));
2831
const [resetKey, setResetKey] = useState(0);
2932
const [autoFocus, setAutoFocus] = useState(false);
@@ -62,33 +65,31 @@ export function Omnibar({ mode, setMode, enableAi }) {
6265
};
6366

6467
return (
65-
<div class={styles.root} data-mode={mode}>
68+
<div key={resetKey} class={styles.root} data-mode={mode}>
6669
<LogoStacked class={styles.logo} aria-label={t('omnibar_logoAlt')} />
6770
{enableAi && <TabSwitcher mode={mode} onChange={handleChangeMode} />}
68-
<Container overflow={mode === 'search'} focusRing={focusRing}>
69-
{mode === 'search' ? (
70-
<SearchForm
71-
key={`search-${resetKey}`}
72-
// Remove any newlines that come from switching from chat to search
73-
term={query.replace(/\n/g, '')}
74-
autoFocus={autoFocus}
75-
onChangeTerm={setQuery}
76-
onOpenSuggestion={handleOpenSuggestion}
77-
onSubmitSearch={handleSubmitSearch}
78-
/>
79-
) : (
80-
<AiChatForm
81-
key={`chat-${resetKey}`}
82-
chat={query}
83-
autoFocus={autoFocus}
84-
onFocus={() => setFocusRing(true)}
85-
onBlur={() => setFocusRing(false)}
86-
onInput={() => setFocusRing(false)}
87-
onChange={setQuery}
88-
onSubmit={handleSubmitChat}
89-
/>
90-
)}
91-
</Container>
71+
<SearchFormProvider term={query} setTerm={setQuery}>
72+
<div class={styles.spacer}>
73+
<div class={styles.popup}>
74+
<ResizingContainer className={styles.field} data-focus-ring={focusRing}>
75+
{mode === 'search' ? (
76+
<SearchForm autoFocus={autoFocus} onOpenSuggestion={handleOpenSuggestion} onSubmit={handleSubmitSearch} />
77+
) : (
78+
<AiChatForm
79+
chat={query}
80+
autoFocus={autoFocus}
81+
onFocus={() => setFocusRing(true)}
82+
onBlur={() => setFocusRing(false)}
83+
onInput={() => setFocusRing(false)}
84+
onChange={setQuery}
85+
onSubmit={handleSubmitChat}
86+
/>
87+
)}
88+
</ResizingContainer>
89+
{mode === 'search' && <SuggestionsList onOpenSuggestion={handleOpenSuggestion} />}
90+
</div>
91+
</div>
92+
</SearchFormProvider>
9293
</div>
9394
);
9495
}

special-pages/pages/new-tab/app/omnibar/components/Omnibar.module.css

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,68 @@
1010
margin-bottom: var(--sp-4);
1111
width: 164px;
1212
}
13+
14+
/*
15+
* There are three containers:
16+
* 1) .spacer - Pushes other widgets away. Fixed height. Contents overflow. Height animates.
17+
* 2) .popup - Has border when suggestions list open. Auto height. No animation.
18+
* 3) .field - Has border when suggestions list closed. Fixed height set to scrollHeight of children. Height animates.
19+
*
20+
* The intention is that:
21+
* - Other widgets are smoothly pushed away when switching to the Duck.ai tab.
22+
* - Suggestions list and expanded Duck.ai textarea always overflows over other widgets.
23+
* - Omnibar height expands smoothly when switching tab or entering mutiline text into Duck.ai textarea.
24+
* - Omnibar height does NOT animate when suggestions list opens or changes.
25+
*/
26+
27+
.spacer {
28+
--box-shadow-rest: 0px 1px 4px 0px rgba(0, 0, 0, 0.1), 0px 4px 8px 0px rgba(0, 0, 0, 0.08);
29+
--box-shadow-focused: 0 0 0 1px var(--ntp-surface-tertiary), 0 0 0 3px var(--ntp-accent-primary), 0 0 0 5px var(--ntp-accent-glow);
30+
--box-shadow-suggestions: 0 0 0 3px var(--ntp-surface-tertiary), 0px 4px 12px 3px rgba(0, 0, 0, 0.10), 0px 20px 40px 3px rgba(0, 0, 0, 0.08);
31+
32+
align-self: stretch;
33+
transition: height 200ms ease;
34+
z-index: 1;
35+
36+
@media (prefers-reduced-motion: reduce) {
37+
transition: none;
38+
}
39+
40+
[data-mode="search"] & {
41+
height: var(--sp-10);
42+
}
43+
44+
[data-mode="ai"] & {
45+
height: var(--sp-20);
46+
}
47+
}
48+
49+
.field {
50+
overflow: hidden;
51+
position: relative;
52+
transition: height 200ms ease;
53+
54+
@media (prefers-reduced-motion: reduce) {
55+
transition: none;
56+
}
57+
}
58+
59+
.root:has([role="listbox"]) .popup,
60+
.root:not(:has([role="listbox"])) .field {
61+
backdrop-filter: blur(48px);
62+
background: var(--ntp-surface-tertiary);
63+
border-radius: var(--border-radius-lg);
64+
}
65+
66+
.root:not(:has([role="listbox"])) .field {
67+
box-shadow: var(--box-shadow-rest);
68+
}
69+
70+
.root:not(:has([role="listbox"])) .field[data-focus-ring="true"],
71+
.root:not(:has([role="listbox"])) .field:focus-within:not([data-focus-ring="false"]) {
72+
box-shadow: var(--box-shadow-focused);
73+
}
74+
75+
.root:has([role="listbox"]) .popup {
76+
box-shadow: var(--box-shadow-suggestions);
77+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { h } from 'preact';
2+
import { useLayoutEffect, useRef, useState } from 'preact/hooks';
3+
4+
/**
5+
* @param {Omit<import('preact').JSX.HTMLAttributes<HTMLDivElement>, 'style'>} props
6+
*/
7+
export function ResizingContainer({ children, ...props }) {
8+
const contentRef = useRef(/** @type {HTMLDivElement|null} */ (null));
9+
const [currentHeight, setCurrentHeight] = useState(/** @type {number|null} */ (null));
10+
11+
useLayoutEffect(() => {
12+
const content = contentRef.current;
13+
if (!content) return;
14+
15+
setCurrentHeight(content.scrollHeight);
16+
17+
const resizeObserver = new ResizeObserver(() => setCurrentHeight(content.scrollHeight));
18+
resizeObserver.observe(content);
19+
return () => resizeObserver.disconnect();
20+
}, []);
21+
22+
return (
23+
<div {...props} style={{ height: currentHeight ?? 'auto' }}>
24+
<div ref={contentRef}>{children}</div>
25+
</div>
26+
);
27+
}

0 commit comments

Comments
 (0)