Skip to content

Commit 6637e02

Browse files
authored
ntp: allow visibility menu items to be disabled (#1930)
1 parent cd7bf73 commit 6637e02

File tree

14 files changed

+134
-50
lines changed

14 files changed

+134
-50
lines changed

special-pages/pages/new-tab/app/customizer/components/Customizer.examples.js

Lines changed: 63 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,47 @@ import { ColorSelection } from './ColorSelection.js';
77
import { GradientSelection } from './GradientSelection.js';
88
import { useSignal } from '@preact/signals';
99
import { ImageSelection } from './ImageSelection.js';
10+
import { ArrowIndentCenteredIcon, DuckFoot, SearchIcon, Shield } from '../../components/Icons.js';
11+
12+
/** @type {import('./CustomizerMenu.js').VisibilityRowData[]} */
13+
const ROWS = [
14+
{
15+
id: 'omnibar',
16+
title: 'Search',
17+
icon: <SearchIcon />,
18+
toggle: noop('toggle search'),
19+
visibility: 'visible',
20+
index: 1,
21+
enabled: true,
22+
},
23+
{
24+
id: 'omnibar-toggleAi',
25+
title: 'Duck.ai',
26+
icon: <ArrowIndentCenteredIcon style={{ color: 'var(--ntp-icons-tertiary)' }} />,
27+
toggle: noop('toggle Duck.ai'),
28+
visibility: 'visible',
29+
index: 1.1,
30+
enabled: true,
31+
},
32+
{
33+
id: 'favorites',
34+
title: 'Favorites',
35+
icon: <Shield />,
36+
toggle: noop('toggle favorites'),
37+
visibility: 'hidden',
38+
index: 0,
39+
enabled: true,
40+
},
41+
{
42+
id: 'privacyStats',
43+
title: 'Privacy Stats',
44+
icon: <DuckFoot />,
45+
toggle: noop('toggle favorites'),
46+
visibility: 'visible',
47+
index: 1,
48+
enabled: true,
49+
},
50+
];
1051

1152
/** @type {Record<string, {factory: () => import("preact").ComponentChild}>} */
1253
export const customizerExamples = {
@@ -64,30 +105,34 @@ export const customizerExamples = {
64105
},
65106
},
66107
'customizer-menu': {
108+
factory: () => (
109+
<MaxContent>
110+
<CustomizerButton isOpen={true} kind="menu" />
111+
<br />
112+
<div style="width: 206px; border: 1px dotted black">
113+
<EmbeddedVisibilityMenu rows={ROWS} />
114+
</div>
115+
</MaxContent>
116+
),
117+
},
118+
'customizer-menu-disabled-item': {
67119
factory: () => (
68120
<MaxContent>
69121
<CustomizerButton isOpen={true} kind="menu" />
70122
<br />
71123
<div style="width: 206px; border: 1px dotted black">
72124
<EmbeddedVisibilityMenu
73-
rows={[
74-
{
75-
id: 'favorites',
76-
title: 'Favorites',
77-
icon: 'star',
78-
toggle: noop('toggle favorites'),
79-
visibility: 'hidden',
80-
index: 0,
81-
},
82-
{
83-
id: 'privacyStats',
84-
title: 'Privacy Stats',
85-
icon: 'shield',
86-
toggle: noop('toggle favorites'),
87-
visibility: 'visible',
88-
index: 1,
89-
},
90-
]}
125+
rows={ROWS.map((row) => {
126+
if (row.id === 'omnibar') {
127+
/** @type {import('./CustomizerMenu.js').VisibilityRowData}} */
128+
return { ...row, visibility: 'hidden' };
129+
}
130+
if (row.id === 'omnibar-toggleAi') {
131+
/** @type {import('./CustomizerMenu.js').VisibilityRowData}} */
132+
return { ...row, enabled: false, visibility: 'hidden' };
133+
}
134+
return row;
135+
})}
91136
/>
92137
</div>
93138
</MaxContent>

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { useMessaging, useTypedTranslation } from '../../types.js';
1111
/**
1212
* @typedef {object} VisibilityRowData
1313
* @property {string} id - a unique id
14+
* @property {boolean} enabled - whether this row can be interacted with
1415
* @property {string} title - the title as it should appear in the menu
1516
* @property {import('preact').ComponentChild} icon - icon to display in the menu
1617
* @property {(id: string) => void} toggle - toggle function for this item
@@ -99,14 +100,14 @@ export function CustomizerMenuPositionedFixed({ children }) {
99100
* Call this to opt-in to the visibility menu
100101
* @param {VisibilityRowData} row
101102
*/
102-
export function useCustomizer({ title, id, icon, toggle, visibility, index }) {
103+
export function useCustomizer({ title, id, icon, toggle, visibility, index, enabled }) {
103104
useEffect(() => {
104-
const handler = (/** @type {CustomEvent<any>} */ e) => {
105-
e.detail.register({ title, id, icon, toggle, visibility, index });
105+
const handler = (/** @type {CustomEvent<{register: (d: VisibilityRowData) => void}>} */ e) => {
106+
e.detail.register({ title, id, icon, toggle, visibility, index, enabled });
106107
};
107108
window.addEventListener(OPEN_EVENT, handler);
108109
return () => window.removeEventListener(OPEN_EVENT, handler);
109-
}, [title, id, icon, toggle, visibility, index]);
110+
}, [title, id, icon, toggle, visibility, index, enabled]);
110111

111112
useEffect(() => {
112113
window.dispatchEvent(new Event(UPDATE_EVENT));

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ export function EmbeddedVisibilityMenu({ rows }) {
2424
return (
2525
<li key={row.id}>
2626
<div class={cn(styles.menuItemLabel, styles.menuItemLabelEmbedded)}>
27-
<span className={styles.svg}>{row.icon}</span>
28-
<span>{row.title ?? row.id}</span>
27+
<span class={styles.svg}>{row.icon}</span>
28+
<span class={styles.title}>{row.title ?? row.id}</span>
2929
<Switch
3030
theme={browser.value}
3131
platformName={platformName}
@@ -35,6 +35,9 @@ export function EmbeddedVisibilityMenu({ rows }) {
3535
onUnchecked={() => row.toggle?.(row.id)}
3636
ariaLabel={`Toggle ${row.title}`}
3737
pending={false}
38+
inputProps={{
39+
disabled: row.enabled === false,
40+
}}
3841
/>
3942
</div>
4043
</li>

special-pages/pages/new-tab/app/customizer/components/VisibilityMenu.module.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
gap: 10px;
2020
white-space: nowrap;
2121
height: calc(28 * var(--px-in-rem));
22+
23+
&:has(input[disabled]) .title {
24+
color: var(--ntp-text-tertiary)
25+
}
2226
}
2327

2428
.menuItemLabelEmbedded {

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,20 @@ export class CustomizerPage {
485485
await expect(this.context().getByRole('switch', { name })).toBeVisible();
486486
}
487487

488+
/**
489+
* @param {string} name
490+
*/
491+
async switchIsDisabled(name) {
492+
await expect(this.context().getByRole('switch', { name })).toBeDisabled();
493+
}
494+
495+
/**
496+
* @param {string} name
497+
*/
498+
async switchIsEnabled(name) {
499+
await expect(this.context().getByRole('switch', { name })).toBeEnabled();
500+
}
501+
488502
/**
489503
* @param {string} name
490504
*/

special-pages/pages/new-tab/app/favorites/components/FavoritesCustomized.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export function FavoritesCustomized() {
6767

6868
// register with the visibility menu
6969
const title = t('favorites_menu_title');
70-
useCustomizer({ title, id, icon: <Shield />, toggle, visibility: visibility.value, index });
70+
useCustomizer({ title, id, icon: <Shield />, toggle, visibility: visibility.value, index, enabled: true });
7171

7272
if (visibility.value === 'hidden') {
7373
return null;

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

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,20 @@ import { useTabState } from '../../tabs/TabsProvider.js';
2828
* ```
2929
*/
3030
export function OmnibarConsumer() {
31-
const { state } = useContext(OmnibarContext);
31+
const { state, setEnableAi } = useContext(OmnibarContext);
3232
const { current } = useTabState();
33-
if (state.status === 'ready') {
34-
return <OmnibarReadyState config={state.config} key={current.value} tabId={current.value} />;
35-
}
36-
return null;
33+
const { visibility } = useVisibility();
34+
if (state.status !== 'ready') return null;
35+
36+
const visible = visibility.value === 'visible';
37+
return (
38+
<>
39+
{state.config.showAiSetting && (
40+
<AiSetting enableAi={state.config?.enableAi === true} setEnableAi={setEnableAi} omnibarVisible={visible} />
41+
)}
42+
{visible && <OmnibarReadyState config={state.config} key={current.value} tabId={current.value} />}
43+
</>
44+
);
3745
}
3846

3947
/**
@@ -43,32 +51,32 @@ export function OmnibarConsumer() {
4351
*/
4452
function OmnibarReadyState({ config, tabId }) {
4553
const { enableAi = true, showAiSetting = true, mode: defaultMode } = config;
46-
const { setEnableAi, setMode } = useContext(OmnibarContext);
54+
const { setMode } = useContext(OmnibarContext);
4755
const modeForCurrentTab = useModeWithLocalPersistence(tabId, defaultMode);
4856

49-
return (
50-
<>
51-
{showAiSetting && <AiSetting enableAi={enableAi} setEnableAi={setEnableAi} />}
52-
<Omnibar mode={modeForCurrentTab} setMode={setMode} enableAi={showAiSetting && enableAi} tabId={tabId} />
53-
</>
54-
);
57+
return <Omnibar mode={modeForCurrentTab} setMode={setMode} enableAi={showAiSetting && enableAi} tabId={tabId} />;
5558
}
5659

5760
/**
5861
* @param {object} props
5962
* @param {boolean} props.enableAi
6063
* @param {(enable: boolean) => void} props.setEnableAi
64+
* @param {boolean} props.omnibarVisible
6165
*/
62-
function AiSetting({ enableAi, setEnableAi }) {
66+
export function AiSetting({ enableAi, setEnableAi, omnibarVisible }) {
6367
const { t } = useTypedTranslationWith(/** @type {Strings} */ ({}));
6468
const { id, index } = useVisibility();
6569
useCustomizer({
6670
title: t('omnibar_toggleDuckAi'),
6771
id: `_${id}-toggleAi`,
6872
icon: <ArrowIndentCenteredIcon style={{ color: 'var(--ntp-icons-tertiary)' }} />,
6973
toggle: () => setEnableAi(!enableAi),
70-
visibility: enableAi ? 'visible' : 'hidden',
74+
/**
75+
* Duck.ai is only ever shown as 'visible' (eg: switch is checked) if the omnibar is also visible.
76+
*/
77+
visibility: omnibarVisible && enableAi ? 'visible' : 'hidden',
7178
index: index + 0.1,
79+
enabled: omnibarVisible,
7280
});
7381
return null;
7482
}

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

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,14 @@ export function OmnibarCustomized() {
2929

3030
const { visibility, id, toggle, index } = useVisibility();
3131

32-
useCustomizer({ title: sectionTitle, id, icon: <SearchIcon />, toggle, visibility: visibility.value, index });
32+
useCustomizer({ title: sectionTitle, id, icon: <SearchIcon />, toggle, visibility: visibility.value, index, enabled: true });
3333

3434
return (
3535
<PersistentTextInputProvider>
3636
<PersistentModeProvider>
37-
{visibility.value === 'visible' && (
38-
<OmnibarProvider>
39-
<OmnibarConsumer />
40-
</OmnibarProvider>
41-
)}
37+
<OmnibarProvider>
38+
<OmnibarConsumer />
39+
</OmnibarProvider>
4240
</PersistentModeProvider>
4341
</PersistentTextInputProvider>
4442
);

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { test, expect } from '@playwright/test';
22
import { NewtabPage } from '../../../integration-tests/new-tab.page.js';
33
import { OmnibarPage } from './omnibar.page.js';
4+
import { CustomizerPage } from '../../customizer/integration-tests/customizer.page.js';
45

56
test.describe('omnibar widget', () => {
67
test('fetches config on load', async ({ page }, workerInfo) => {
@@ -306,18 +307,19 @@ test.describe('omnibar widget', () => {
306307
test('hiding Omnibar widget hides Duck.ai toggle', async ({ page }, workerInfo) => {
307308
const ntp = NewtabPage.create(page, workerInfo);
308309
const omnibar = new OmnibarPage(ntp);
310+
const customizer = new CustomizerPage(ntp);
309311
await ntp.reducedMotion();
310312

311313
await ntp.openPage({ additional: { omnibar: true } });
312314
await omnibar.ready();
313315

314316
// Open Customize panel - Duck.ai toggle should be visible
315-
await omnibar.customizeButton().click();
316-
await expect(omnibar.toggleDuckAiButton()).toBeVisible();
317+
await customizer.opensCustomizer();
318+
await customizer.switchIsEnabled('Duck.ai');
317319

318320
// Hide the Omnibar widget - Duck.ai toggle should be hidden
319321
await omnibar.toggleSearchButton().click();
320-
await expect(omnibar.toggleDuckAiButton()).toHaveCount(0);
322+
await customizer.switchIsDisabled('Duck.ai');
321323
});
322324

323325
test('Duck.ai toggle is hidden when showAiSetting is false', async ({ page }, workerInfo) => {

special-pages/pages/new-tab/app/omnibar/mocks/omnibar.mock-transport.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export function omnibarMockTransport() {
77
/** @type {import('../../../types/new-tab.ts').OmnibarConfig} */
88
const config = {
99
mode: 'search',
10+
enableAi: true,
11+
showAiSetting: true,
1012
};
1113

1214
/** @type {Map<string, (d: any) => void>} */

0 commit comments

Comments
 (0)