Skip to content

Commit 2676db2

Browse files
authored
NTP: Omnibar suffix text (#1831)
1 parent 110a4c9 commit 2676db2

File tree

22 files changed

+1126
-109
lines changed

22 files changed

+1126
-109
lines changed

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -401,13 +401,13 @@ export function BrowserIcon(props) {
401401
return (
402402
<svg width="16" height="16" fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" {...props}>
403403
<path
404-
fill="#000"
404+
fill="currentColor"
405405
fill-rule="evenodd"
406406
d="M0 5a4 4 0 0 1 4-4h8a4 4 0 0 1 4 4v1.792c0 .478-.681.721-1.053.422a.52.52 0 0 1-.197-.4v-.819H1.25V11A2.75 2.75 0 0 0 4 13.75h2.135a.57.57 0 0 1 .497.312c.21.398-.055.938-.506.938H4a4 4 0 0 1-4-4zm1.262-.255h13.476A2.75 2.75 0 0 0 12 2.25H4a2.75 2.75 0 0 0-2.738 2.495"
407407
clip-rule="evenodd"
408408
/>
409409
<path
410-
fill="#000"
410+
fill="currentColor"
411411
fill-rule="evenodd"
412412
d="M11.5 7a4.5 4.5 0 1 0 0 9 4.5 4.5 0 0 0 0-9m-1.972 7.084A3.25 3.25 0 0 1 8.288 12h.85a7.6 7.6 0 0 0 .39 2.084M8.337 10.75h.818a7.5 7.5 0 0 1 .373-1.834 3.25 3.25 0 0 0-1.191 1.834m2.495 3.233c-.226-.5-.392-1.19-.441-1.983h2.218c-.05.793-.215 1.482-.441 1.983-.299.66-.583.767-.668.767s-.37-.106-.668-.767m0-4.966c-.202.447-.356 1.045-.422 1.733h2.18c-.066-.688-.22-1.286-.422-1.733-.299-.66-.583-.767-.668-.767s-.37.106-.668.767m2.64 5.067c.213-.606.348-1.32.39-2.084h.85a3.25 3.25 0 0 1-1.24 2.084m.373-3.334h.818a3.25 3.25 0 0 0-1.19-1.834c.188.54.316 1.164.371 1.834Z"
413413
clip-rule="evenodd"
@@ -416,6 +416,21 @@ export function BrowserIcon(props) {
416416
);
417417
}
418418

419+
/**
420+
* From https://dub.duckduckgo.com/duckduckgo/Icons/blob/Main/Glyphs/16px/Tab-Desktop-16.svg
421+
* @param {import('preact').JSX.SVGAttributes<SVGSVGElement>} props
422+
*/
423+
export function TabDesktopIcon(props) {
424+
return (
425+
<svg width="16" height="16" fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" {...props}>
426+
<path
427+
fill="currentColor"
428+
d="M11 2a4 4 0 0 1 4 4v4c0 .854.39 1.617 1 2.121v.357c0 .517-.541.841-.925.495A4 4 0 0 1 13.75 10V6A2.75 2.75 0 0 0 11 3.25H5A2.75 2.75 0 0 0 2.25 6v4a4 4 0 0 1-1.325 2.973c-.384.346-.925.022-.925-.495v-.357c.61-.504 1-1.267 1-2.121V6a4 4 0 0 1 4-4z"
429+
/>
430+
</svg>
431+
);
432+
}
433+
419434
/**
420435
* @param {import('preact').JSX.SVGAttributes<SVGSVGElement>} props
421436
*/

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

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
import { h } from 'preact';
1+
import { h, Fragment } from 'preact';
22
import { useEffect, useId } from 'preact/hooks';
33
import { SearchIcon } from '../../components/Icons.js';
44
import { useTypedTranslationWith } from '../../types';
55
import styles from './SearchForm.module.css';
66
import { SuggestionsList } from './SuggestionsList.js';
7-
import { useSuggestionInput } from './useSuggestionInput.js';
7+
import { useCompletionInput } from './useSuggestionInput.js';
88
import { useSuggestions } from './useSuggestions';
9+
import { useSuffixText } from './SuffixText.js';
10+
import { getInputSuffix } from '../utils.js';
911

1012
/**
1113
* @typedef {import('../strings.json')} Strings
@@ -30,8 +32,8 @@ export function SearchForm({ term, autoFocus, onChangeTerm, onOpenSuggestion, on
3032
selectedSuggestion,
3133
setSelectedSuggestion,
3234
clearSelectedSuggestion,
33-
termBase,
34-
termSuggestion,
35+
inputBase,
36+
inputCompletion,
3537
handleChange,
3638
handleKeyDown,
3739
handleClick,
@@ -43,7 +45,10 @@ export function SearchForm({ term, autoFocus, onChangeTerm, onOpenSuggestion, on
4345
onSubmitSearch,
4446
});
4547

46-
const inputRef = useSuggestionInput(termBase, termSuggestion);
48+
const inputSuffix = getInputSuffix(term, selectedSuggestion);
49+
const inputSuffixText = useSuffixText(inputSuffix);
50+
51+
const inputRef = useCompletionInput(inputBase, inputCompletion);
4752

4853
useEffect(() => {
4954
if (autoFocus && inputRef.current) {
@@ -61,8 +66,8 @@ export function SearchForm({ term, autoFocus, onChangeTerm, onOpenSuggestion, on
6166
};
6267

6368
return (
64-
<form class={styles.form} onClick={() => inputRef.current?.focus()} onBlur={handleBlur} onSubmit={handleSubmit}>
65-
<div class={styles.inputContainer}>
69+
<form class={styles.form} onBlur={handleBlur} onSubmit={handleSubmit}>
70+
<div class={styles.inputContainer} style={{ '--suffix-text-width': `${measureText(inputSuffixText)}px` }}>
6671
<SearchIcon inert />
6772
<input
6873
ref={inputRef}
@@ -83,10 +88,21 @@ export function SearchForm({ term, autoFocus, onChangeTerm, onOpenSuggestion, on
8388
onKeyDown={handleKeyDown}
8489
onClick={handleClick}
8590
/>
91+
{inputSuffix && (
92+
<>
93+
<span class={styles.suffixSpacer} inert>
94+
{inputBase + inputCompletion || t('omnibar_searchFormPlaceholder')}
95+
</span>
96+
<span class={styles.suffix} inert>
97+
{inputSuffixText}
98+
</span>
99+
</>
100+
)}
86101
</div>
87102
{suggestions.length > 0 && (
88103
<SuggestionsList
89104
id={suggestionsListId}
105+
term={term}
90106
suggestions={suggestions}
91107
selectedSuggestion={selectedSuggestion}
92108
onSelectSuggestion={setSelectedSuggestion}
@@ -97,3 +113,15 @@ export function SearchForm({ term, autoFocus, onChangeTerm, onOpenSuggestion, on
97113
</form>
98114
);
99115
}
116+
117+
/**
118+
* @param {string} text
119+
* @returns {number}
120+
*/
121+
function measureText(text) {
122+
const canvas = document.createElement('canvas');
123+
const context = canvas.getContext('2d');
124+
if (!context) return 0;
125+
context.font = '13px / 16px system-ui';
126+
return context.measureText(text).width;
127+
}

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

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
padding: var(--sp-1);
55

66
svg {
7+
flex-shrink: 0;
78
margin: var(--sp-2);
89
}
910
}
@@ -12,11 +13,27 @@
1213
background: none;
1314
border: none;
1415
color: var(--ntp-text-normal);
15-
flex: 1 0 0;
1616
height: var(--sp-8);
17-
padding: 0;
17+
left: var(--sp-1);
18+
padding-bottom: 0;
19+
padding-left: calc(var(--sp-2) + var(--sp-4) + var(--sp-2));
20+
padding-right: var(--suffix-text-width);
21+
padding-top: 0;
22+
position: absolute;
23+
right: var(--sp-1);
1824

1925
&:focus {
2026
outline: none;
2127
}
2228
}
29+
30+
.suffixSpacer {
31+
overflow: hidden;
32+
visibility: hidden;
33+
white-space: pre;
34+
}
35+
36+
.suffix {
37+
color: var(--ntp-color-primary);
38+
flex-shrink: 0;
39+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { useTypedTranslationWith } from '../../types';
2+
3+
/**
4+
* @typedef {import('../strings.json')} Strings
5+
* @typedef {import('../utils').Suffix} Suffix
6+
*/
7+
8+
/**
9+
* @param {Suffix} suffix
10+
* @returns {string}
11+
*/
12+
export function useSuffixText(suffix) {
13+
const { t } = useTypedTranslationWith(/** @type {Strings} */ ({}));
14+
if (!suffix) return '';
15+
switch (suffix.kind) {
16+
case 'searchDuckDuckGo':
17+
return ' – ' + t('omnibar_searchDuckDuckGoSuffix');
18+
case 'duckDuckGo':
19+
return ' – ' + t('omnibar_duckDuckGoSuffix');
20+
case 'visit':
21+
return ' – ' + t('omnibar_visitSuffix', { url: suffix.url });
22+
case 'raw':
23+
return ' – ' + suffix.text;
24+
}
25+
}
26+
27+
/**
28+
* @param {object} props
29+
* @param {Suffix} props.suffix
30+
*/
31+
export function SuffixText({ suffix }) {
32+
return useSuffixText(suffix);
33+
}

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

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
1-
import { h } from 'preact';
1+
import { h, Fragment } from 'preact';
22
import { eventToTarget } from '../../../../../shared/handlers';
3-
import { BookmarkIcon, BrowserIcon, FavoriteIcon, GlobeIcon, HistoryIcon, SearchIcon } from '../../components/Icons';
3+
import {
4+
ArrowRightIcon,
5+
BookmarkIcon,
6+
BrowserIcon,
7+
FavoriteIcon,
8+
GlobeIcon,
9+
HistoryIcon,
10+
SearchIcon,
11+
TabDesktopIcon,
12+
} from '../../components/Icons';
413
import { usePlatformName } from '../../settings.provider';
514
import styles from './SuggestionsList.module.css';
15+
import { getSuggestionSuffix, getSuggestionTitle, startsWithIgnoreCase } from '../utils';
16+
import { SuffixText } from './SuffixText';
617

718
/**
819
* @typedef {import('./useSuggestions').SuggestionModel} SuggestionModel
@@ -13,17 +24,20 @@ import styles from './SuggestionsList.module.css';
1324
/**
1425
* @param {object} props
1526
* @param {string} props.id
27+
* @param {string} props.term
1628
* @param {SuggestionModel[]} props.suggestions
1729
* @param {SuggestionModel | null} props.selectedSuggestion
1830
* @param {(suggestion: SuggestionModel) => void} props.onSelectSuggestion
1931
* @param {() => void} props.onClearSuggestion
2032
* @param {(params: {suggestion: Suggestion, target: OpenTarget}) => void} props.onOpenSuggestion
2133
*/
22-
export function SuggestionsList({ id, suggestions, selectedSuggestion, onSelectSuggestion, onClearSuggestion, onOpenSuggestion }) {
34+
export function SuggestionsList({ id, term, suggestions, selectedSuggestion, onSelectSuggestion, onClearSuggestion, onOpenSuggestion }) {
2335
const platformName = usePlatformName();
2436
return (
2537
<div role="listbox" id={id} class={styles.list}>
2638
{suggestions.map((suggestion) => {
39+
const title = getSuggestionTitle(suggestion, term);
40+
const suffix = getSuggestionSuffix(suggestion);
2741
return (
2842
<button
2943
key={suggestion.id}
@@ -40,7 +54,26 @@ export function SuggestionsList({ id, suggestions, selectedSuggestion, onSelectS
4054
}}
4155
>
4256
<SuggestionIcon suggestion={suggestion} />
43-
{suggestion.title}
57+
<span class={styles.title}>
58+
{startsWithIgnoreCase(title, term) ? (
59+
<>
60+
<b>{title.slice(0, term.length)}</b>
61+
{title.slice(term.length)}
62+
</>
63+
) : (
64+
title
65+
)}
66+
</span>
67+
{suffix && (
68+
<span class={styles.suffix}>
69+
<SuffixText suffix={suffix} />
70+
</span>
71+
)}
72+
{suggestion.kind === 'openTab' && (
73+
<span class={styles.badge}>
74+
Switch to Tab <ArrowRightIcon />
75+
</span>
76+
)}
4477
</button>
4578
);
4679
})}
@@ -63,6 +96,7 @@ function SuggestionIcon({ suggestion }) {
6396
case 'bookmark':
6497
return suggestion.isFavorite ? <FavoriteIcon /> : <BookmarkIcon />;
6598
case 'openTab':
99+
return <TabDesktopIcon />;
66100
case 'internalPage':
67101
return <BrowserIcon />;
68102
default:

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,43 @@
1616
justify-content: flex-start;
1717

1818
svg {
19+
flex-shrink: 0;
1920
margin: var(--sp-2);
2021
}
2122

2223
&[aria-selected="true"] {
2324
background: var(--ddg-color-primary);
2425
border-radius: 4px;
2526
color: var(--color-white);
27+
28+
.suffix {
29+
color: var(--color-white);
30+
}
31+
}
32+
}
33+
34+
.title {
35+
white-space: pre;
36+
}
37+
38+
.suffix {
39+
color: var(--ntp-text-muted);
40+
font-size: 12px;
41+
}
42+
43+
.badge {
44+
align-items: center;
45+
background: var(--ntp-controls-raised-backdrop);
46+
border-radius: var(--border-radius-sm);
47+
display: flex;
48+
gap: 6px;
49+
height: 22px;
50+
margin-left: auto;
51+
padding: 0px var(--sp-3);
52+
53+
svg {
54+
height: 9px;
55+
margin: 0;
56+
width: 9px;
2657
}
2758
}

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,19 @@ import { useLayoutEffect, useRef } from 'preact/hooks';
22

33
/**
44
* @param {string} base
5-
* @param {string} suggestion
5+
* @param {string} completion
66
*/
7-
export function useSuggestionInput(base, suggestion) {
7+
export function useCompletionInput(base, completion) {
88
const ref = useRef(/** @type {HTMLInputElement|null} */ (null));
99

1010
useLayoutEffect(() => {
1111
if (!ref.current) return;
12-
const value = base + suggestion;
12+
const value = base + completion;
1313
ref.current.value = value;
14-
if (suggestion) {
14+
if (completion) {
1515
ref.current.setSelectionRange(base.length, value.length);
1616
}
17-
}, [base, suggestion]);
17+
}, [base, completion]);
1818

1919
return ref;
2020
}

0 commit comments

Comments
 (0)