Skip to content

Commit b1e6f22

Browse files
shakyShaneShane Osbourne
andauthored
ntp: customizer drawer (#1291)
* ntp: customizer drawer, feature flag * CSS fix * comments * remove hook --------- Co-authored-by: Shane Osbourne <[email protected]>
1 parent bb32cbf commit b1e6f22

29 files changed

+553
-88
lines changed

package-lock.json

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

special-pages/messages/new-tab/initialSetup.response.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
"widgets": {
77
"$ref": "./types/widget-list.json"
88
},
9+
"settings": {
10+
"$ref": "./types/settings.json"
11+
},
912
"widgetConfigs": {
1013
"$ref": "types/widget-configs.json"
1114
},
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"title": "NewTabPageSettings",
4+
"type": "object",
5+
"properties": {
6+
"customizerDrawer": {
7+
"type": "object",
8+
"required": ["state"],
9+
"properties": {
10+
"state": {
11+
"type": "string",
12+
"enum": ["enabled", "disabled"]
13+
}
14+
}
15+
}
16+
}
17+
}

special-pages/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
},
3333
"dependencies": {
3434
"preact": "^10.24.3",
35+
"@preact/signals": "^1.3.1",
3536
"classnames": "^2.5.1",
3637
"@formkit/auto-animate": "^0.8.2",
3738
"@rive-app/canvas-single": "^2.23.10",
Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import { h } from 'preact';
2+
import cn from 'classnames';
23
import styles from './App.module.css';
3-
import { usePlatformName } from '../settings.provider.js';
4+
import { useCustomizerDrawerSettings, usePlatformName } from '../settings.provider.js';
45
import { WidgetList } from '../widget-list/WidgetList.js';
56
import { useGlobalDropzone } from '../dropzone.js';
7+
import { Customizer, CustomizerButton, CustomizerMenuPositionedFixed, useContextMenu } from '../customizer/components/Customizer.js';
8+
import { useDrawer, useDrawerControls } from './Drawer.js';
9+
import { CustomizerDrawer } from '../customizer/components/CustomizerDrawer.js';
610

711
/**
812
* Renders the App component.
@@ -12,11 +16,43 @@ import { useGlobalDropzone } from '../dropzone.js';
1216
*/
1317
export function App({ children }) {
1418
const platformName = usePlatformName();
19+
const settings = useCustomizerDrawerSettings();
20+
21+
const customizerKind = settings.state === 'enabled' ? 'drawer' : 'menu';
22+
1523
useGlobalDropzone();
24+
useContextMenu();
25+
26+
const { buttonRef, wrapperRef, visibility, displayChildren, hidden, buttonId, drawerId } = useDrawer();
27+
const { toggle, close } = useDrawerControls();
28+
1629
return (
17-
<div className={styles.layout} data-platform={platformName}>
18-
<WidgetList />
19-
{children}
30+
<div class={cn(styles.layout)} ref={wrapperRef} data-drawer-visibility={visibility}>
31+
<main class={cn(styles.main)}>
32+
<div class={styles.tube} data-platform={platformName}>
33+
<WidgetList />
34+
<CustomizerMenuPositionedFixed>
35+
{customizerKind === 'menu' && <Customizer />}
36+
{customizerKind === 'drawer' && (
37+
<CustomizerButton
38+
buttonId={buttonId}
39+
menuId={drawerId}
40+
toggleMenu={toggle}
41+
buttonRef={buttonRef}
42+
isOpen={false}
43+
/>
44+
)}
45+
</CustomizerMenuPositionedFixed>
46+
{children}
47+
</div>
48+
</main>
49+
{customizerKind === 'drawer' && (
50+
<aside id={drawerId} class={styles.aside} aria-hidden={hidden}>
51+
<div class={styles.asideContent}>
52+
<CustomizerDrawer onClose={close} wrapperRef={wrapperRef} displayChildren={displayChildren} />
53+
</div>
54+
</aside>
55+
)}
2056
</div>
2157
);
2258
}

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

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ body {
1010
line-height: var(--body-line-height);
1111
}
1212

13-
.layout {
13+
.tube {
1414
padding-top: var(--sp-16);
1515
padding-bottom: var(--sp-16);
1616
margin-left: auto;
1717
margin-right: auto;
1818
}
1919

20-
body:has([data-reset-layout="true"]) .layout {
20+
body:has([data-reset-layout="true"]) .tube {
2121
padding-top: 0;
2222
}
2323

@@ -31,3 +31,40 @@ body:has([data-reset-layout="true"]) .layout {
3131
:global(.layout-centered:empty) {
3232
display: contents;
3333
}
34+
35+
.layout {
36+
display: grid;
37+
grid-template-columns: auto 0;
38+
transition: all ease .3s;
39+
position: relative;
40+
41+
&[data-drawer-visibility='visible'] {
42+
grid-template-columns: auto var(--ntp-drawer-width);
43+
}
44+
}
45+
46+
.main {
47+
overflow: hidden;
48+
height: 100vh;
49+
}
50+
.active {}
51+
52+
.aside {
53+
overflow: hidden;
54+
height: 100vh;
55+
background: var(--ntp-surfaces-panel-background-color);
56+
z-index: 1;
57+
58+
/** todo: is this re-usable in any way, or unique? */
59+
box-shadow: 0px 0px 1px 0px #FFF inset, 0px 0px 2px 0px rgba(0, 0, 0, 0.08), 0px 8px 12px 0px rgba(0, 0, 0, 0.12);
60+
@media screen and (prefers-color-scheme: dark) {
61+
box-shadow: 0px 0px 1px 0px rgba(0, 0, 0, 0.60) inset, 0px 0px 2px 0px rgba(0, 0, 0, 0.16), 0px 8px 12px 0px rgba(0, 0, 0, 0.24);
62+
}
63+
}
64+
65+
.asideContent {
66+
box-sizing: border-box;
67+
height: 100vh;
68+
width: var(--ntp-drawer-width);
69+
padding: var(--sp-2);
70+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { useRef, useId, useLayoutEffect } from 'preact/hooks';
2+
import { computed, effect, useSignal } from '@preact/signals';
3+
4+
const CLOSE_DRAWER_EVENT = 'close-drawer';
5+
const TOGGLE_DRAWER_EVENT = 'toggle-drawer';
6+
const OPEN_DRAWER_EVENT = 'open-drawer';
7+
const REQUEST_VISIBILITY_EVENT = 'request-visibility';
8+
9+
/**
10+
* @typedef {'hidden' | 'visible'} DrawerVisibility
11+
*/
12+
13+
/**
14+
* Hook that manages the state and behavior of a drawer component.
15+
*
16+
* There are three main considerations here:
17+
* - 1: we make the API available with events (via `useDrawerControls`)
18+
* - 2: we use signals to trigger animations for performance (to prevent VDOM diffing)
19+
* - 3: we provide a way for child components to render AFTER animations have ended, again for performance.
20+
*
21+
* @return {{
22+
* wrapperRef: import("preact").RefObject<HTMLDivElement>,
23+
* buttonRef: import("preact").RefObject<HTMLButtonElement>,
24+
* visibility: import("@preact/signals").Signal<DrawerVisibility>,
25+
* buttonId: string,
26+
* drawerId: string,
27+
* hidden: import("@preact/signals").Signal<boolean>,
28+
* displayChildren: import("@preact/signals").Signal<boolean>,
29+
* }}
30+
*/
31+
export function useDrawer() {
32+
const wrapperRef = useRef(/** @type {HTMLDivElement|null} */ (null));
33+
const buttonRef = useRef(/** @type {HTMLButtonElement|null} */ (null));
34+
35+
// id's for accessibility
36+
const buttonId = useId();
37+
const drawerId = useId();
38+
39+
// the immediate value
40+
const visibility = useSignal(/** @type {DrawerVisibility} */ ('hidden'));
41+
42+
// The value that determines if it's safe to render children
43+
// This takes animations into account, which is why is can't be a regular-derived state
44+
const displayChildren = useSignal(false);
45+
46+
// Derive a 'hidden' signal that can be used as an aria-hidden={hidden}
47+
// it needs to be done this way to `.value` being accessed in the top level of the application
48+
const hidden = computed(() => displayChildren.value === false);
49+
50+
// react to the global API events
51+
useLayoutEffect(() => {
52+
const controller = new AbortController();
53+
const wrapper = wrapperRef.current;
54+
if (!wrapper) return;
55+
56+
/**
57+
* @param {DrawerVisibility} value
58+
*/
59+
const update = (value) => {
60+
visibility.value = value;
61+
};
62+
63+
// Event handlers
64+
const close = () => update('hidden');
65+
const open = () => update('visible');
66+
const toggle = () => {
67+
const next = visibility.value === 'hidden' ? 'visible' : 'hidden';
68+
update(next);
69+
};
70+
71+
// the 3 methods that can be triggered from anywhere in the app
72+
window.addEventListener(CLOSE_DRAWER_EVENT, close, { signal: controller.signal });
73+
window.addEventListener(TOGGLE_DRAWER_EVENT, toggle, { signal: controller.signal });
74+
window.addEventListener(OPEN_DRAWER_EVENT, open, { signal: controller.signal });
75+
76+
// allow anywhere in the application to read the current state
77+
wrapper.addEventListener(
78+
REQUEST_VISIBILITY_EVENT,
79+
(/** @type {CustomEvent} */ e) => {
80+
e.detail.value = visibility.value;
81+
},
82+
{ signal: controller.signal },
83+
);
84+
85+
// update `displayChildren` when animations complete.
86+
wrapper?.addEventListener(
87+
'transitionend',
88+
(e) => {
89+
// ignore child animations
90+
if (e.target !== e.currentTarget) return;
91+
displayChildren.value = visibility.value === 'visible';
92+
},
93+
{ signal: controller.signal },
94+
);
95+
96+
return () => {
97+
controller.abort();
98+
};
99+
}, []);
100+
101+
// move focus back to the button when the drawer is closed
102+
// this needs to be done otherwise it's a violation of aria rules
103+
effect(() => {
104+
if (displayChildren.value === false) {
105+
buttonRef.current?.focus?.();
106+
}
107+
});
108+
109+
return {
110+
wrapperRef,
111+
buttonRef,
112+
visibility,
113+
displayChildren,
114+
buttonId,
115+
drawerId,
116+
hidden,
117+
};
118+
}
119+
120+
/**
121+
*
122+
*/
123+
export function useDrawerControls() {
124+
return {
125+
toggle: () => {
126+
window.dispatchEvent(new CustomEvent(TOGGLE_DRAWER_EVENT));
127+
},
128+
close: () => {
129+
window.dispatchEvent(new CustomEvent(CLOSE_DRAWER_EVENT));
130+
},
131+
open: () => {
132+
window.dispatchEvent(new CustomEvent(OPEN_DRAWER_EVENT));
133+
},
134+
};
135+
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { h } from 'preact';
22
import { useEffect, useRef, useState, useCallback, useId } from 'preact/hooks';
33
import styles from './Customizer.module.css';
4-
import { VisibilityMenu } from './VisibilityMenu.js';
54
import { CustomizeIcon } from '../../components/Icons.js';
65
import cn from 'classnames';
76
import { useMessaging, useTypedTranslation } from '../../types.js';
7+
import { VisibilityMenu, VisibilityMenuPopover } from './VisibilityMenu.js';
88

99
/**
1010
* @import { Widgets, WidgetConfigItem, WidgetVisibility, VisibilityMenuItem } from '../../../../../types/new-tab.js'
@@ -17,8 +17,6 @@ export function Customizer() {
1717
const { setIsOpen, buttonRef, dropdownRef, isOpen } = useDropdown();
1818
const [rowData, setRowData] = useState(/** @type {VisibilityRowData[]} */ ([]));
1919

20-
useContextMenu();
21-
2220
/**
2321
* Dispatch an event every time the customizer is opened - this
2422
* allows widgets to register themselves and provide titles/icons etc.
@@ -47,7 +45,9 @@ export function Customizer() {
4745
<div class={styles.root} ref={dropdownRef}>
4846
<CustomizerButton buttonId={BUTTON_ID} menuId={MENU_ID} toggleMenu={toggleMenu} buttonRef={buttonRef} isOpen={isOpen} />
4947
<div id={MENU_ID} class={cn(styles.dropdownMenu, { [styles.show]: isOpen })} aria-labelledby={BUTTON_ID}>
50-
<VisibilityMenu rows={rowData} />
48+
<VisibilityMenuPopover>
49+
<VisibilityMenu rows={rowData} variant={'popover'} />
50+
</VisibilityMenuPopover>
5151
</div>
5252
</div>
5353
);

0 commit comments

Comments
 (0)