Skip to content

Commit 089d908

Browse files
authored
NTP: More omnibar design feedback (#1876)
* Adjust spacing and borders to match design spec pixel for pixel * Add close button to search input * Add fade-in animation to submit button * Make chat textarea have same padding as search input * Extra height on .spacer to accomodate border * Don't let close button sit against input text * Add search form close button label to translations * Update snapshot tests * Fix close button in WebKit
1 parent bacb6d9 commit 089d908

30 files changed

+223
-43
lines changed

special-pages/pages/new-tab/app/components/Icons.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,3 +499,18 @@ export function ArrowIndentCenteredIcon(props) {
499499
</svg>
500500
);
501501
}
502+
503+
/**
504+
* From https://dub.duckduckgo.com/duckduckgo/Icons/blob/Main/Glyphs/16px/Close-Small-16.svg.
505+
* @param {import('preact').JSX.SVGAttributes<SVGSVGElement>} props
506+
*/
507+
export function CloseSmallIcon(props) {
508+
return (
509+
<svg width="16" height="16" fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" {...props}>
510+
<path
511+
fill="currentColor"
512+
d="M10.433 4.683a.625.625 0 1 1 .884.884L8.884 8l2.433 2.433a.625.625 0 1 1-.884.884L8 8.884l-2.433 2.433a.625.625 0 1 1-.884-.884L7.116 8 4.683 5.567a.625.625 0 1 1 .884-.884L8 7.116l2.433-2.433Z"
513+
/>
514+
</svg>
515+
);
516+
}

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

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
box-sizing: content-box;
1313
color: var(--ntp-text-primary);
1414
max-height: 10lh;
15-
padding: 11px 15px 0;
15+
padding: var(--sp-3) var(--sp-3) 0;
1616
resize: none;
1717

1818
[data-platform="macos"] & {
@@ -34,7 +34,7 @@
3434

3535
.hasScroll & {
3636
border-bottom: 1px solid var(--ntp-decoration-tertiary);
37-
padding-bottom: 11px;
37+
padding-bottom: var(--sp-3);
3838
}
3939
}
4040

@@ -50,6 +50,7 @@
5050

5151
.submitButton {
5252
align-items: center;
53+
animation: fadeIn 0.3s ease-in-out;
5354
background: var(--ntp-accent-primary);
5455
border-radius: 100%;
5556
border: none;
@@ -64,12 +65,28 @@
6465

6566
&[disabled] {
6667
background: none;
67-
color: var(--ntp-icons-primary);
68+
color: color-mix(in srgb, var(--ntp-icons-primary) 30%, transparent);
6869
cursor: default;
69-
opacity: 0.3;
70+
}
71+
72+
&:hover:not([disabled]) {
73+
background: var(--ntp-accent-secondary);
74+
}
75+
76+
&:active:not([disabled]) {
77+
background: var(--ntp-accent-tertiary);
7078
}
7179

7280
&:focus-visible {
7381
box-shadow: var(--focus-ring);
7482
}
7583
}
84+
85+
@keyframes fadeIn {
86+
from {
87+
opacity: 0;
88+
}
89+
to {
90+
opacity: 1;
91+
}
92+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,13 @@ export function Omnibar({ mode, setMode, enableAi }) {
6565
};
6666

6767
return (
68-
<div key={resetKey} class={styles.root} data-mode={mode}>
68+
<div key={resetKey} class={styles.root} data-mode={mode} data-focus-ring={focusRing}>
6969
<LogoStacked class={styles.logo} aria-label={t('omnibar_logoAlt')} />
7070
{enableAi && <TabSwitcher mode={mode} onChange={handleChangeMode} />}
7171
<SearchFormProvider term={query} setTerm={setQuery}>
7272
<div class={styles.spacer}>
7373
<div class={styles.popup}>
74-
<ResizingContainer className={styles.field} data-focus-ring={focusRing}>
74+
<ResizingContainer className={styles.field}>
7575
{mode === 'search' ? (
7676
<SearchForm autoFocus={autoFocus} onOpenSuggestion={handleOpenSuggestion} onSubmit={handleSubmitSearch} />
7777
) : (

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

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
/*
1515
* There are three containers:
1616
* 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.
17+
* 2) .popup - Has border. Auto height. No animation.
18+
* 3) .field - Fixed height set to scrollHeight of children. Height animates.
1919
*
2020
* The intention is that:
2121
* - Other widgets are smoothly pushed away when switching to the Duck.ai tab.
@@ -25,10 +25,6 @@
2525
*/
2626

2727
.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-
3228
align-self: stretch;
3329
transition: height 200ms ease;
3430
z-index: 1;
@@ -38,15 +34,22 @@
3834
}
3935

4036
[data-mode="search"] & {
41-
height: var(--sp-10);
37+
height: calc(var(--sp-10) + 3px * 2);
4238
}
4339

4440
[data-mode="ai"] & {
45-
height: var(--sp-20);
41+
height: calc(var(--sp-20) + 3px * 2);
4642
}
4743
}
4844

45+
.popup {
46+
backdrop-filter: blur(16px);
47+
background: var(--ntp-surface-tertiary);
48+
display: flow-root; /* Prevent margin collapse on .field */
49+
}
50+
4951
.field {
52+
box-sizing: content-box;
5053
overflow: hidden;
5154
position: relative;
5255
transition: height 200ms ease;
@@ -56,22 +59,34 @@
5659
}
5760
}
5861

59-
.root:has([role="listbox"]) .popup,
60-
.root:not(:has([role="listbox"])) .field {
61-
backdrop-filter: blur(16px);
62-
background: var(--ntp-surface-tertiary);
63-
border-radius: var(--border-radius-lg);
62+
/* Rest */
63+
.root:not(:has([role="listbox"])) .popup {
64+
border-radius: 12px;
65+
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.1), 0 4px 8px 0 rgba(0, 0, 0, 0.08);
66+
margin: 3px;
6467
}
6568

66-
.root:not(:has([role="listbox"])) .field {
67-
box-shadow: var(--box-shadow-rest);
68-
}
69+
/* Focused */
70+
.root:not(:has([role="listbox"])):focus-within:not([data-focus-ring="false"]) .popup,
71+
.root:not(:has([role="listbox"]))[data-focus-ring="true"] .popup {
72+
border-radius: 15px;
73+
border: 2px solid var(--ntp-accent-primary);
74+
box-shadow: none;
75+
margin: 0;
76+
outline: 2px solid var(--ntp-accent-glow);
6977

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);
78+
.field {
79+
margin: 1px; /* Use margin on .field instead of padding on .popup so that position: absolute respects the spacing */
80+
}
7381
}
7482

83+
/* Suggestions */
7584
.root:has([role="listbox"]) .popup {
76-
box-shadow: var(--box-shadow-suggestions);
85+
border-radius: 15px;
86+
box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.1), 0 20px 40px 0 rgba(0, 0, 0, 0.08);
87+
margin: 0;
88+
89+
.field {
90+
margin: 3px; /* Use margin on .field instead of padding on .popup so that position: absolute respects the spacing */
91+
}
7792
}

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

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import styles from './SearchForm.module.css';
88
import { useSearchFormContext } from './SearchFormProvider.js';
99
import { useSuffixText } from './SuffixText.js';
1010
import { useCompletionInput } from './useSuggestionInput.js';
11+
import { CloseSmallIcon } from '../../components/Icons.js';
1112

1213
/**
1314
* @typedef {import('../strings.json')} Strings
@@ -108,14 +109,6 @@ export function SearchForm({ autoFocus, onOpenSuggestion, onSubmit }) {
108109
<form
109110
class={styles.form}
110111
style={{ '--input-font': inputFont, '--suffix-text-width': `${inputSuffixWidth}px` }}
111-
// Using onBlurCapture to work around WebKit which doesn't fire blur event when user selects address bar.
112-
onBlurCapture={(event) => {
113-
// Ignore blur events caused by clicking on a suggestion
114-
if (event.relatedTarget instanceof Element && event.relatedTarget.role === 'option') {
115-
return;
116-
}
117-
hideSuggestions();
118-
}}
119112
onSubmit={(event) => {
120113
event.preventDefault();
121114
onSubmit({
@@ -146,6 +139,16 @@ export function SearchForm({ autoFocus, onOpenSuggestion, onSubmit }) {
146139
updateSuggestions(term);
147140
}}
148141
onClick={() => acceptSuggestion()}
142+
// Using onBlurCapture to work around WebKit which doesn't fire blur event when user selects address bar.
143+
onBlurCapture={(event) => {
144+
if (event.relatedTarget instanceof Element) {
145+
// Ignore blur events caused by clicking on a suggestion
146+
if (event.relatedTarget.role === 'option') return;
147+
// Ignore blur events caused by clicking on the close button
148+
if (event.relatedTarget.classList.contains(styles.closeButton)) return;
149+
}
150+
hideSuggestions();
151+
}}
149152
/>
150153
{inputSuffix && (
151154
<>
@@ -158,6 +161,23 @@ export function SearchForm({ autoFocus, onOpenSuggestion, onSubmit }) {
158161
</span>
159162
</>
160163
)}
164+
{term.length > 0 && (
165+
<button
166+
class={styles.closeButton}
167+
aria-label={t('omnibar_searchFormCloseButtonLabel')}
168+
tabIndex={0} // Needed so that WebKit sets event.relatedTarget when firing blur event
169+
onClick={(event) => {
170+
event.preventDefault();
171+
if (suggestions.length > 0) {
172+
hideSuggestions();
173+
} else {
174+
setTerm('');
175+
}
176+
}}
177+
>
178+
<CloseSmallIcon />
179+
</button>
180+
)}
161181
</form>
162182
);
163183
}

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

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,19 @@
22
align-items: center;
33
display: flex;
44
height: var(--sp-10);
5-
padding: var(--sp-1) 7px var(--sp-1) calc(7px + var(--sp-2));
5+
padding: var(--sp-3);
66
}
77

88
.input {
99
background: none;
1010
border: none;
1111
color: var(--ntp-text-primary);
1212
font: var(--input-font);
13-
height: var(--sp-8);
14-
left: 7px;
15-
padding-bottom: 0;
16-
padding-left: var(--sp-2);
17-
padding-right: var(--suffix-text-width);
18-
padding-top: 0;
13+
left: 0;
14+
padding: var(--sp-3);
15+
padding-right: calc(var(--suffix-text-width) + var(--sp-1) + var(--sp-8) + var(--sp-3)); /* Suffix + padding + close button + padding */
1916
position: absolute;
20-
right: 7px;
17+
right: 0;
2118

2219
&::placeholder {
2320
color: var(--ntp-text-tertiary);
@@ -45,3 +42,42 @@
4542
flex: none;
4643
font: var(--input-font);
4744
}
45+
46+
.closeButton {
47+
align-items: center;
48+
background: none;
49+
border-radius: 9px;
50+
border: none;
51+
color: var(--ntp-text-primary);
52+
cursor: pointer;
53+
display: flex;
54+
flex: none;
55+
height: var(--sp-8);
56+
justify-content: center;
57+
margin-left: auto;
58+
outline: none;
59+
padding: 0;
60+
width: var(--sp-8);
61+
z-index: 1;
62+
visibility: hidden;
63+
64+
&:hover {
65+
background: var(--ntp-controls-fill-primary);
66+
}
67+
68+
&:active {
69+
background: var(--ntp-controls-fill-secondary);
70+
}
71+
72+
&:focus-visible {
73+
box-shadow: var(--focus-ring);
74+
}
75+
76+
.form:hover & {
77+
visibility: visible;
78+
}
79+
80+
[data-entry-point="omnibar"]:has([role="listbox"]) & {
81+
visibility: visible;
82+
}
83+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
border-top: 1px solid var(--ntp-surface-border-color);
33
display: flex;
44
flex-direction: column;
5-
padding: 7px;
5+
padding: calc(3px + var(--sp-1)); /* 3px for popup borders + 1sp of padding */
6+
padding-top: calc(3px + var(--sp-1) - 1px); /* - 1px to account for the border-top */
67
}
78

89
.item {

special-pages/pages/new-tab/app/omnibar/integration-tests/omnibar.page.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ export class OmnibarPage {
6565
return this.page.getByRole('switch', { name: 'Toggle Duck.ai' });
6666
}
6767

68+
closeButton() {
69+
return this.context().getByRole('button', { name: 'Close' });
70+
}
71+
6872
/**
6973
* @param {number} count
7074
*/

special-pages/pages/new-tab/app/omnibar/integration-tests/omnibar.spec.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -908,4 +908,32 @@ test.describe('omnibar widget', () => {
908908
// Content should still be there
909909
await expect(omnibar.chatInput()).toHaveValue(multilineText);
910910
});
911+
912+
test('close button hides suggestions then clears input', async ({ page }, workerInfo) => {
913+
const ntp = NewtabPage.create(page, workerInfo);
914+
const omnibar = new OmnibarPage(ntp);
915+
916+
await ntp.reducedMotion();
917+
await ntp.openPage({ additional: { omnibar: true } });
918+
await omnibar.ready();
919+
920+
// Select the input and type 'pizza'
921+
await omnibar.searchInput().click();
922+
await omnibar.searchInput().fill('pizza');
923+
924+
// Wait for suggestions to appear
925+
await omnibar.waitForSuggestions();
926+
await omnibar.expectSuggestionsCount(18);
927+
928+
// Click close button - should hide suggestions
929+
await omnibar.closeButton().click();
930+
await expect(omnibar.suggestionsList()).not.toBeVisible();
931+
932+
// Input should still have 'pizza'
933+
await omnibar.expectInputValue('pizza');
934+
935+
// Click close button again - should clear input
936+
await omnibar.closeButton().click();
937+
await omnibar.expectInputValue('');
938+
});
911939
});

special-pages/pages/new-tab/app/omnibar/strings.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@
3131
"title": "Search privately",
3232
"description": "Placeholder text for the search input field."
3333
},
34+
"omnibar_searchFormCloseButtonLabel": {
35+
"title": "Close",
36+
"description": "Accessible label for the close button in the search form."
37+
},
3438
"omnibar_toggleDuckAi": {
3539
"title": "Duck.ai",
3640
"description": "Label for the button to toggle the Duck.ai chat interface."

0 commit comments

Comments
 (0)