Skip to content

Commit ac1518c

Browse files
authored
ntp: customizer polish 💅 (#1336)
* ntp: matching designs for customizer drawer * re-enable tests
1 parent 4c6371f commit ac1518c

36 files changed

+776
-332
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { h } from 'preact';
2+
import { ErrorBoundary } from '../../../shared/components/ErrorBoundary.js';
3+
import { useMessaging } from './types.js';
4+
5+
/**
6+
* @param {object} props
7+
* @param {import("preact").ComponentChild} props.children
8+
* @param {string} props.named
9+
* @param {(message: string) => import("preact").ComponentChild} [props.fallback]
10+
*/
11+
export function InlineError({ children, named, fallback }) {
12+
const messaging = useMessaging();
13+
/**
14+
* @param {any} error
15+
* @param {string} id
16+
*/
17+
const didCatch = (error, id) => {
18+
const message = error?.message || error?.error || 'unknown';
19+
const composed = `Customizer section '${id}' threw an exception: ` + message;
20+
messaging.reportPageException({ message: composed });
21+
};
22+
const inlineMessage = 'A problem occurred with this feature. DuckDuckGo was notified';
23+
const fallbackElement = fallback?.(inlineMessage) || <p>{inlineMessage}</p>;
24+
return (
25+
<ErrorBoundary didCatch={(error) => didCatch(error, named)} fallback={fallbackElement}>
26+
{children}
27+
</ErrorBoundary>
28+
);
29+
}

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,7 @@ export function App() {
7575
data-browser-panel
7676
>
7777
<div class={styles.asideContent}>
78-
<div class={styles.asideContentInner}>
79-
<CustomizerDrawer displayChildren={displayChildren} />
80-
</div>
78+
<CustomizerDrawer displayChildren={displayChildren} />
8179
</div>
8280
</aside>
8381
)}

special-pages/pages/new-tab/app/components/App.module.css

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,18 +89,18 @@ body:has([data-reset-layout="true"]) .tube {
8989
.layout[data-animating="true"] & {
9090
overflow: hidden;
9191
}
92+
93+
.layout[data-animating="false"] &[aria-hidden=true] {
94+
visibility: hidden;
95+
opacity: 0;
96+
}
9297
}
9398

9499
.asideContent {
95100
opacity: 1;
96101
width: var(--ntp-drawer-width);
97102
}
98103

99-
.asideContentInner {
100-
padding: 1rem;
101-
padding-right: calc(1rem - var(--ntp-drawer-scroll-width));
102-
}
103-
104104
.asideScroller {
105105
&::-webkit-scrollbar {
106106
width: var(--ntp-drawer-scroll-width);

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

Lines changed: 54 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { Fragment, h } from 'preact';
2+
import cn from 'classnames';
23
import styles from './BackgroundReceiver.module.css';
34
import { values } from '../customizer/values.js';
4-
import { useContext } from 'preact/hooks';
5+
import { useContext, useState } from 'preact/hooks';
56
import { CustomizerContext } from '../customizer/CustomizerProvider.js';
67
import { detectThemeFromHex } from '../customizer/utils.js';
78

@@ -92,20 +93,9 @@ export function BackgroundConsumer({ browser }) {
9293
const gradient = values.gradients[background.value];
9394
return (
9495
<Fragment key="gradient">
96+
<ImageCrossFade src={gradient.path}></ImageCrossFade>
9597
<div
96-
class={styles.root}
97-
data-animate="false"
98-
data-testid="BackgroundConsumer"
99-
style={{
100-
backgroundColor: gradient.fallback,
101-
backgroundImage: `url(${gradient.path})`,
102-
backgroundSize: 'cover',
103-
backgroundRepeat: 'no-repeat',
104-
}}
105-
/>
106-
<div
107-
class={styles.root}
108-
data-animate="false"
98+
className={styles.root}
10999
style={{
110100
backgroundImage: `url(gradients/grain.png)`,
111101
backgroundRepeat: 'repeat',
@@ -118,23 +108,60 @@ export function BackgroundConsumer({ browser }) {
118108
}
119109
case 'userImage': {
120110
const img = background.value;
121-
return (
122-
<div
123-
class={styles.root}
124-
data-animate="true"
125-
data-testid="BackgroundConsumer"
126-
style={{
127-
backgroundImage: `url(${img.src})`,
128-
backgroundSize: 'cover',
129-
backgroundRepeat: 'no-repeat',
130-
backgroundPosition: 'center center',
131-
}}
132-
></div>
133-
);
111+
return <ImageCrossFade src={img.src} />;
134112
}
135113
default: {
136114
console.warn('Unreachable!');
137115
return <div className={styles.root}></div>;
138116
}
139117
}
140118
}
119+
120+
/**
121+
* @param {object} props
122+
* @param {string} props.src
123+
*/
124+
function ImageCrossFade({ src }) {
125+
/**
126+
* Proxy the image source, so that we can keep the old
127+
* image around whilst the new one is loading.
128+
*/
129+
const [stable, setStable] = useState(src);
130+
/**
131+
* Trigger the animation:
132+
*
133+
* NOTE: this animation is deliberately NOT done purely with CSS-triggered state.
134+
* Whilst debugging in WebKit, I found the technique below to be 100% reliable
135+
* in terms of fading a new image over the top of an existing one.
136+
*
137+
* If you find a better way, please test in webkit-based browsers
138+
*/
139+
return (
140+
<Fragment>
141+
<img src={stable} class={styles.root} style={{ display: src === stable ? 'none' : 'block' }} />
142+
<img
143+
src={src}
144+
class={cn(styles.root, styles.over)}
145+
onLoad={(e) => {
146+
const elem = /** @type {HTMLImageElement} */ (e.target);
147+
148+
// HACK: This is what I needed to force, to get 100% predictability. 🤷
149+
elem.style.opacity = '0';
150+
151+
const anim = elem.animate([{ opacity: '0' }, { opacity: '1' }], {
152+
duration: 250,
153+
iterations: 1,
154+
easing: 'ease-in-out',
155+
fill: 'both',
156+
});
157+
158+
// when the fade completes, we want to reset the stable `src`.
159+
// This allows the image underneath to be updated but also allows us to un-mount the fader on top.
160+
anim.onfinish = () => {
161+
setStable(src);
162+
};
163+
}}
164+
/>
165+
</Fragment>
166+
);
167+
}

special-pages/pages/new-tab/app/components/BackgroundReceiver.module.css

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,23 @@
44
inset: 0;
55
width: 100vw;
66
height: 100vh;
7+
object-fit: cover;
78
pointer-events: none;
89

910
&[data-animate="true"] {
10-
transition: all .3s ease-in-out;
11+
transition: background .25s ease-in-out;
1112
}
12-
1313
&[data-background-kind="default"][data-theme=dark] {
1414
background: var(--default-dark-bg);
1515
}
1616
&[data-background-kind="default"][data-theme=light] {
1717
background: var(--default-light-bg);
1818
}
1919
}
20+
21+
.under {
22+
opacity: 1;
23+
}
24+
.over {
25+
opacity: 0;
26+
}

special-pages/pages/new-tab/app/components/Components.jsx

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { mainExamples, otherExamples } from './Examples.jsx';
44
import { useThemes } from '../customizer/themes.js';
55
import { useSignal } from '@preact/signals';
66
import { BackgroundConsumer } from './BackgroundProvider.js';
7+
import { CustomizerThemesContext } from '../customizer/CustomizerProvider.js';
78
const url = new URL(window.location.href);
89

910
const list = {
@@ -32,18 +33,20 @@ export function Components() {
3233
const { main, browser } = useThemes(dataSignal);
3334

3435
return (
35-
<div class={styles.main} data-main-scroller data-theme={main}>
36-
<BackgroundConsumer browser={browser} />
37-
<div data-content-tube class={styles.contentTube}>
38-
{isolated && <Isolated entries={filtered} e2e={e2e} />}
39-
{!isolated && (
40-
<Fragment>
41-
<DebugBar id={ids[0]} ids={ids} entries={entries} />
42-
<Stage entries={/** @type {any} */ (filtered)} />
43-
</Fragment>
44-
)}
36+
<CustomizerThemesContext.Provider value={{ main, browser }}>
37+
<div class={styles.main} data-main-scroller data-theme={main}>
38+
<BackgroundConsumer browser={browser} />
39+
<div data-content-tube class={styles.contentTube}>
40+
{isolated && <Isolated entries={filtered} e2e={e2e} />}
41+
{!isolated && (
42+
<Fragment>
43+
<DebugBar id={ids[0]} ids={ids} entries={entries} />
44+
<Stage entries={/** @type {any} */ (filtered)} />
45+
</Fragment>
46+
)}
47+
</div>
4548
</div>
46-
</div>
49+
</CustomizerThemesContext.Provider>
4750
);
4851
}
4952

special-pages/pages/new-tab/app/components/DismissButton.jsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@ import styles from './DismissButton.module.css';
88
* @param {object} props
99
* @param {string} [props.className]
1010
* @param {() => void} [props.onClick]
11+
* @param {import("preact").ComponentProps<"button"> & Record<string, string>} [props.buttonProps]
1112
*/
12-
export function DismissButton({ className, onClick }) {
13+
export function DismissButton({ className, onClick, buttonProps = {} }) {
1314
const { t } = useTypedTranslation();
1415

1516
return (
16-
<button class={cn(styles.btn, className)} onClick={onClick} aria-label={t('ntp_dismiss')} data-testid="dismissBtn">
17+
<button class={cn(styles.btn, className)} onClick={onClick} aria-label={t('ntp_dismiss')} data-testid="dismissBtn" {...buttonProps}>
1718
<Cross />
1819
</button>
1920
);

special-pages/pages/new-tab/app/components/DismissButton.module.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@
1212
border-radius: 50%;
1313
transition: all .3s;
1414

15+
svg {
16+
position: absolute;
17+
top: 50%;
18+
left: 50%;
19+
transform: translateX(-50%) translateY(-50%);
20+
}
21+
1522
&:hover {
1623
background-color: var(--color-black-at-9);
1724
cursor: pointer;

special-pages/pages/new-tab/app/customizer/components/BackgroundSection.js

Lines changed: 27 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -42,40 +42,33 @@ export function BackgroundSection({ data, onNav, onUpload, select }) {
4242
}
4343

4444
return (
45-
<div class={styles.section}>
46-
<h3 class={styles.sectionTitle}>Background</h3>
47-
<ul class={cn(styles.sectionBody, styles.bgList)} role="radiogroup">
48-
<li class={styles.bgListItem}>
49-
<DefaultPanel
50-
checked={data.value.background.kind === 'default'}
51-
onClick={() => select({ background: { kind: 'default' } })}
52-
/>
53-
</li>
54-
<li class={styles.bgListItem}>
55-
<ColorPanel
56-
checked={data.value.background.kind === 'color' || data.value.background.kind === 'hex'}
57-
color={displayColor}
58-
onClick={() => onNav('color')}
59-
/>
60-
</li>
61-
<li class={styles.bgListItem}>
62-
<GradientPanel
63-
checked={data.value.background.kind === 'gradient'}
64-
gradient={gradient}
65-
onClick={() => onNav('gradient')}
66-
/>
67-
</li>
68-
<li class={styles.bgListItem}>
69-
<BackgroundImagePanel
70-
checked={data.value.background.kind === 'userImage'}
71-
onClick={() => onNav('image')}
72-
data={data}
73-
upload={onUpload}
74-
browserTheme={browser}
75-
/>
76-
</li>
77-
</ul>
78-
</div>
45+
<ul class={cn(styles.bgList)} role="radiogroup">
46+
<li class={styles.bgListItem}>
47+
<DefaultPanel
48+
checked={data.value.background.kind === 'default'}
49+
onClick={() => select({ background: { kind: 'default' } })}
50+
/>
51+
</li>
52+
<li class={styles.bgListItem}>
53+
<ColorPanel
54+
checked={data.value.background.kind === 'color' || data.value.background.kind === 'hex'}
55+
color={displayColor}
56+
onClick={() => onNav('color')}
57+
/>
58+
</li>
59+
<li class={styles.bgListItem}>
60+
<GradientPanel checked={data.value.background.kind === 'gradient'} gradient={gradient} onClick={() => onNav('gradient')} />
61+
</li>
62+
<li class={styles.bgListItem}>
63+
<BackgroundImagePanel
64+
checked={data.value.background.kind === 'userImage'}
65+
onClick={() => onNav('image')}
66+
data={data}
67+
upload={onUpload}
68+
browserTheme={browser}
69+
/>
70+
</li>
71+
</ul>
7972
);
8073
}
8174

0 commit comments

Comments
 (0)