From 9ea12c0afcf7390abe7ff341a3c44c4329f2ff4d Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Wed, 16 Oct 2024 16:45:47 +0100 Subject: [PATCH] ntp: stats feedback (#1098) * ntp: stats feedback * deps * move esbuild dependency into special-pages --------- Co-authored-by: Shane Osbourne --- .gitignore | 1 + Package.swift | 2 +- .../ContentScopeScripts/dist/pages/.gitignore | 1 + package-lock.json | 21 +- package.json | 1 - .../messages/new-tab/examples/stats.js | 10 +- .../messages/new-tab/types/animation.json | 39 +++ .../messages/new-tab/types/stats-config.json | 3 +- special-pages/package.json | 3 +- .../new-tab/app/components/App.module.css | 2 + .../pages/new-tab/app/components/Chevron.js | 11 + .../new-tab/app/components/Components.jsx | 286 ++++++++++-------- .../app/components/Components.module.css | 33 +- .../pages/new-tab/app/components/Examples.js | 0 .../pages/new-tab/app/components/Examples.jsx | 54 ++++ .../new-tab/app/privacy-stats/PrivacyStats.js | 118 +++++--- .../app/privacy-stats/PrivacyStats.module.css | 4 +- .../app/privacy-stats/PrivacyStatsProvider.js | 223 ++------------ .../mocks/PrivacyStatsMockProvider.js | 42 ++- .../privacy-stats/privacy-stats.service.js | 10 +- .../pages/new-tab/app/service.hooks.js | 170 +++++++++++ special-pages/pages/new-tab/app/service.js | 6 +- special-pages/pages/new-tab/app/utils.js | 33 ++ .../new-tab/app/widget-list/WidgetList.js | 2 +- .../pages/new-tab/src/js/mock-transport.js | 47 ++- special-pages/tests/new-tab.spec.js | 2 - special-pages/types/new-tab.ts | 20 ++ 27 files changed, 726 insertions(+), 418 deletions(-) create mode 100644 Sources/ContentScopeScripts/dist/pages/.gitignore create mode 100644 special-pages/messages/new-tab/types/animation.json create mode 100644 special-pages/pages/new-tab/app/components/Chevron.js create mode 100644 special-pages/pages/new-tab/app/components/Examples.js create mode 100644 special-pages/pages/new-tab/app/components/Examples.jsx create mode 100644 special-pages/pages/new-tab/app/service.hooks.js diff --git a/.gitignore b/.gitignore index 921f37600..9fb776896 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ test-results/ playwright-report/ Sources/ContentScopeScripts/dist/ test-results +!Sources/ContentScopeScripts/dist/pages/.gitignore # Local Netlify folder .netlify diff --git a/Package.swift b/Package.swift index 55fb861f9..5c7881c21 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.3 +// swift-tools-version:5.5 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription diff --git a/Sources/ContentScopeScripts/dist/pages/.gitignore b/Sources/ContentScopeScripts/dist/pages/.gitignore new file mode 100644 index 000000000..72e8ffc0d --- /dev/null +++ b/Sources/ContentScopeScripts/dist/pages/.gitignore @@ -0,0 +1 @@ +* diff --git a/package-lock.json b/package-lock.json index ab6bb5ac6..6d6dcd257 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,6 @@ ], "devDependencies": { "@typescript-eslint/eslint-plugin": "^6.9.1", - "esbuild": "^0.19.5", "eslint": "^8.57.1", "eslint-config-standard": "^17.1.0", "eslint-plugin-import": "^2.31.0", @@ -4347,9 +4346,9 @@ } }, "node_modules/preact": { - "version": "10.19.3", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.19.3.tgz", - "integrity": "sha512-nHHTeFVBTHRGxJXKkKu5hT8C/YWBkPso4/Gad6xuj5dbptt9iF9NZr9pHbPhBrnT2klheu7mHTxTZ/LjwJiEiQ==", + "version": "10.24.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", + "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -5400,7 +5399,7 @@ "@formkit/auto-animate": "^0.8.0", "@rive-app/canvas-single": "^2.21.5", "classnames": "^2.3.2", - "preact": "^10.19.3" + "preact": "^10.24.2" }, "devDependencies": { "@duckduckgo/messaging": "*", @@ -5416,11 +5415,12 @@ "@formkit/auto-animate": "^0.8.0", "@rive-app/canvas-single": "^2.21.5", "classnames": "^2.3.2", - "preact": "^10.19.3" + "preact": "^10.24.3" }, "devDependencies": { "@duckduckgo/messaging": "*", "@playwright/test": "^1.40.1", + "esbuild": "^0.19.5", "fast-check": "^3.22.0", "http-server": "^14.1.1", "web-resource-inliner": "^6.0.1" @@ -8377,9 +8377,9 @@ "dev": true }, "preact": { - "version": "10.19.3", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.19.3.tgz", - "integrity": "sha512-nHHTeFVBTHRGxJXKkKu5hT8C/YWBkPso4/Gad6xuj5dbptt9iF9NZr9pHbPhBrnT2klheu7mHTxTZ/LjwJiEiQ==" + "version": "10.24.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", + "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==" }, "prelude-ls": { "version": "1.2.1", @@ -8650,9 +8650,10 @@ "@playwright/test": "^1.40.1", "@rive-app/canvas-single": "^2.21.5", "classnames": "^2.3.2", + "esbuild": "^0.19.5", "fast-check": "^3.22.0", "http-server": "^14.1.1", - "preact": "^10.19.3", + "preact": "^10.24.3", "web-resource-inliner": "^6.0.1" } }, diff --git a/package.json b/package.json index ad2aa4d63..5d9d258d9 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,6 @@ "dependencies": {}, "devDependencies": { "@typescript-eslint/eslint-plugin": "^6.9.1", - "esbuild": "^0.19.5", "eslint": "^8.57.1", "eslint-config-standard": "^17.1.0", "eslint-plugin-import": "^2.31.0", diff --git a/special-pages/messages/new-tab/examples/stats.js b/special-pages/messages/new-tab/examples/stats.js index cb4be3af9..4d23135c7 100644 --- a/special-pages/messages/new-tab/examples/stats.js +++ b/special-pages/messages/new-tab/examples/stats.js @@ -13,9 +13,17 @@ const privacyStatsData = { /** * @type {import("../../../types/new-tab").StatsConfig} */ -const statsConfig = { +const minimumConfig = { expansion: "expanded" } +/** + * @type {import("../../../types/new-tab").StatsConfig} + */ +const withAnimation = { + expansion: "expanded", + animation: { kind: "view-transitions" } +} + export {} diff --git a/special-pages/messages/new-tab/types/animation.json b/special-pages/messages/new-tab/types/animation.json new file mode 100644 index 000000000..51a86d43d --- /dev/null +++ b/special-pages/messages/new-tab/types/animation.json @@ -0,0 +1,39 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Animation", + "description": "Generic Animation configuration", + "oneOf": [ + { + "title": "None", + "type": "object", + "required": ["kind"], + "properties": { + "kind": { + "const": "none" + } + } + }, + { + "title": "View Transitions", + "description": "Use CSS view transitions where available", + "type": "object", + "required": ["kind"], + "properties": { + "kind": { + "const": "view-transitions" + } + } + }, + { + "title": "Auto", + "description": "Use the auto-animate library to provide default animation styles", + "type": "object", + "required": ["kind"], + "properties": { + "kind": { + "const": "auto-animate" + } + } + } + ] +} diff --git a/special-pages/messages/new-tab/types/stats-config.json b/special-pages/messages/new-tab/types/stats-config.json index 44b35cab0..2c0d53fa5 100644 --- a/special-pages/messages/new-tab/types/stats-config.json +++ b/special-pages/messages/new-tab/types/stats-config.json @@ -4,6 +4,7 @@ "type": "object", "required": ["expansion"], "properties": { - "expansion": {"$ref": "./expansion.json"} + "expansion": {"$ref": "./expansion.json"}, + "animation": {"$ref": "./animation.json"} } } diff --git a/special-pages/package.json b/special-pages/package.json index 53f479116..afd29671b 100644 --- a/special-pages/package.json +++ b/special-pages/package.json @@ -25,13 +25,14 @@ "license": "ISC", "devDependencies": { "@duckduckgo/messaging": "*", + "esbuild": "^0.19.5", "@playwright/test": "^1.40.1", "http-server": "^14.1.1", "web-resource-inliner": "^6.0.1", "fast-check": "^3.22.0" }, "dependencies": { - "preact": "^10.19.3", + "preact": "^10.24.3", "classnames": "^2.3.2", "@formkit/auto-animate": "^0.8.0", "@rive-app/canvas-single": "^2.21.5" diff --git a/special-pages/pages/new-tab/app/components/App.module.css b/special-pages/pages/new-tab/app/components/App.module.css index a3f6cf67d..76fb547cf 100644 --- a/special-pages/pages/new-tab/app/components/App.module.css +++ b/special-pages/pages/new-tab/app/components/App.module.css @@ -8,6 +8,8 @@ body { font-size: var(--body-font-size); font-weight: var(--body-font-weight); line-height: var(--body-line-height); + padding-left: var(--sp-6); + padding-right: var(--sp-6); } .layout { diff --git a/special-pages/pages/new-tab/app/components/Chevron.js b/special-pages/pages/new-tab/app/components/Chevron.js new file mode 100644 index 000000000..baa5763f0 --- /dev/null +++ b/special-pages/pages/new-tab/app/components/Chevron.js @@ -0,0 +1,11 @@ +import { h } from 'preact' + +export function Chevron () { + return ( + + + + ) +} diff --git a/special-pages/pages/new-tab/app/components/Components.jsx b/special-pages/pages/new-tab/app/components/Components.jsx index e02ccaee2..ec2049032 100644 --- a/special-pages/pages/new-tab/app/components/Components.jsx +++ b/special-pages/pages/new-tab/app/components/Components.jsx @@ -1,112 +1,30 @@ import { Fragment, h } from "preact"; import styles from "./Components.module.css"; -import { Body, Heading, PrivacyStatsConsumer } from "../privacy-stats/PrivacyStats.js"; - -import { PrivacyStatsMockProvider } from "../privacy-stats/mocks/PrivacyStatsMockProvider.js"; -import { stats } from "../privacy-stats/mocks/stats.js"; -import { RemoteMessagingFramework } from "../remote-messaging-framework/RemoteMessagingFramework"; - -/** @type {Record import("preact").ComponentChild}>} */ -const examples = { - 'stats.few': { - factory: () => - }, - 'stats.few.collapsed': { - factory: () => - }, - 'stats.single': { - factory: () => - }, - 'stats.none': { - factory: () => - }, - 'stats.norecent': { - factory: () => - }, - 'stats.list': { - factory: () => - }, - 'stats.heading': { - factory: () => - }, - 'stats.heading.none': { - factory: () => - }, - 'rmf-small': { - factory: () => ( - ) - }, - 'rmf-medium': { - factory: () => ( - ) - }, - 'rmf-big-single-action': { - factory: () => ( - { }} - />) - }, - 'rmf-big-two-action': { - factory: () => ( - { }} - secondaryActionText="Remind Me Later" - secondaryAction={() => { }} - />) - }, - 'rmf-big-two-action-overflow': { - factory: () => ( - { }} - secondaryActionText="Remind me later, but only if Iā€™m actually going to update soon" - secondaryAction={() => { }} - />) - } -} +import { mainExamples, otherExamples } from "./Examples.jsx"; const url = new URL(window.location.href); export function Components() { - const id = url.searchParams.get("id"); + const ids = url.searchParams.getAll("id"); const isolated = url.searchParams.has("isolate"); - const valid = (id || '') in examples; - const entries = Object.entries(examples); - const filtered = id && valid - ? entries.filter(([_id]) => _id === id) + const e2e = url.searchParams.has("e2e"); + const entries = Object.entries(mainExamples).concat(Object.entries(otherExamples)); + const entryIds = entries.map(([id]) => id); + + const validIds = ids.filter(id => entryIds.includes(id)); + + const filtered = validIds.length + ? validIds.map((id) => /** @type {const} */([id, mainExamples[id] || otherExamples[id]])) : entries if (isolated) { - return + return } return (
- - + +
) } @@ -121,8 +39,79 @@ function Stage({ entries }) { return (
{entries.map(([id, item]) => { + const next = new URL(url) + next.searchParams.set('isolate', 'true'); + next.searchParams.set('id', id); + + const selected = new URL(url) + selected.searchParams.set('id', id); + + const e2e = new URL(url) + e2e.searchParams.set('isolate', 'true'); + e2e.searchParams.set('id', id); + e2e.searchParams.set('e2e', 'true'); + + const without = new URL(url); + const current = without.searchParams.getAll('id'); + const others = current.filter(x => x !== id); + const matching = current.filter(x => x === id); + const matchingMinus1 = matching.length === 1 + ? [] + : matching.slice(0, -1) + + without.searchParams.delete('id'); + for (let string of [...others, ...matchingMinus1]) { + without.searchParams.append('id', string) + } + + return ( + +
+ {id}{" "} + +
+ select{" "} + isolate{" "} + edge-to-edge +
+
+
+ {item.factory()} +
+
+ ) + })} +
+ ) +} + +function Isolated({ entries, e2e }) { + if (e2e) { + return ( +
+ {entries.map(([id, item]) => { + return ( + + {item.factory()} + + ) + })} +
+ ) + } + return ( +
+ {entries.map(([id, item], index) => { return ( -
+
{item.factory()}
) @@ -131,14 +120,15 @@ function Stage({ entries }) { ) } -function Isolated({ entries }) { +function DebugBar({ entries, id, ids }) { return ( -
- {entries.map(([id, item]) => { - return - {item.factory()} - - })} +
+ + {ids.length > 0 && ( + + )} + +
) } @@ -165,23 +155,19 @@ function Isolate() { next.searchParams.set('isolate', 'true'); return (
- Isolate -
- ) -} - -function DebugBar({ entries, id }) { - return ( - ) } +/** + * Allows users to select an example from a list and update the URL accordingly. + * + * @param {Object} options - The options object. + * @param {Array} options.entries - The list of examples to choose from, each represented as an array with an id. + * @param {string} options.id - The current selected example id. + */ function ExampleSelector({ entries, id }) { - function onReset() { const url = new URL(window.location.href); url.searchParams.delete("id"); @@ -200,17 +186,63 @@ function ExampleSelector({ entries, id }) { } } return ( -
- - -
+ +
+ + +
+
+ ) +} + +/** + * Allows users to select an example from a list and update the URL accordingly. + * + * @param {Object} options - The options object. + * @param {Array} options.entries - The list of examples to choose from, each represented as an array with an id. + * @param {string} options.id - The current selected example id. + */ +function Append({ entries, id }) { + function onReset() { + const url = new URL(window.location.href); + url.searchParams.delete("id"); + window.location.href = url.toString(); + } + + function onSubmit(event) { + if (!event.target) return; + event.preventDefault(); + const form = event.target; + const data = new FormData(form); + const value = data.get('add-id'); + if (typeof value !== "string") return; + const url = new URL(window.location.href); + url.searchParams.append("id", value); + window.location.href = url.toString(); + } + + return ( + +
+ + +
+
) } diff --git a/special-pages/pages/new-tab/app/components/Components.module.css b/special-pages/pages/new-tab/app/components/Components.module.css index 73a1a50bc..f970e75fe 100644 --- a/special-pages/pages/new-tab/app/components/Components.module.css +++ b/special-pages/pages/new-tab/app/components/Components.module.css @@ -5,6 +5,11 @@ } } +body[data-display="components"] { + padding-left: 0; + padding-right: 0; +} + .componentList { padding-top: var(--sp-16); padding-bottom: var(--sp-16); @@ -15,6 +20,32 @@ display: grid; grid-template-columns: auto; grid-row-gap: 2rem; + + +} + +.itemInfo { + display: flex; + align-items: center; + gap: 1em; +} +.itemLink { + padding: 0.2em 0.3em; + border: 1px solid var(--color-gray-60); + border-radius: 4px; + display: inline-block; + line-height: 1; + text-decoration: none; + + &:hover { + background: var(--color-gray-20); + } + + @media screen and (prefers-color-scheme: dark) { + &:hover { + background: var(--color-gray-90); + } + } } .debugBar { @@ -24,7 +55,7 @@ grid-row-gap: .5rem; align-items: center; grid-column-gap: 1rem; - grid-template-columns: max-content max-content; + grid-template-columns: max-content; } .buttonRow { diff --git a/special-pages/pages/new-tab/app/components/Examples.js b/special-pages/pages/new-tab/app/components/Examples.js new file mode 100644 index 000000000..e69de29bb diff --git a/special-pages/pages/new-tab/app/components/Examples.jsx b/special-pages/pages/new-tab/app/components/Examples.jsx new file mode 100644 index 000000000..4257f515a --- /dev/null +++ b/special-pages/pages/new-tab/app/components/Examples.jsx @@ -0,0 +1,54 @@ +import { h } from "preact"; +import { PrivacyStatsMockProvider } from "../privacy-stats/mocks/PrivacyStatsMockProvider.js"; +import { Body, Heading, PrivacyStatsConsumer } from "../privacy-stats/PrivacyStats.js"; +import { stats } from "../privacy-stats/mocks/stats.js"; + +/** @type {Record import("preact").ComponentChild}>} */ +export const mainExamples = { + 'stats.few': { + factory: () => + }, + 'stats.few.collapsed': { + factory: () => + }, + 'stats.single': { + factory: () => + }, + 'stats.none': { + factory: () => + }, + 'stats.norecent': { + factory: () => + }, + 'stats.list': { + factory: () => + }, + 'stats.heading': { + factory: () => + }, + 'stats.heading.none': { + factory: () => + }, +} + +export const otherExamples = { + 'stats.without-animation': { + factory: () => + }, + 'stats.with-view-transitions': { + factory: () => + }, +} diff --git a/special-pages/pages/new-tab/app/privacy-stats/PrivacyStats.js b/special-pages/pages/new-tab/app/privacy-stats/PrivacyStats.js index 791631a88..888932796 100644 --- a/special-pages/pages/new-tab/app/privacy-stats/PrivacyStats.js +++ b/special-pages/pages/new-tab/app/privacy-stats/PrivacyStats.js @@ -2,40 +2,96 @@ import { h } from 'preact' import cn from 'classnames' import styles from './PrivacyStats.module.css' import { useTypedTranslation } from '../types.js' -import { useCallback, useContext, useState } from 'preact/hooks' -import { PrivacyStatsContext, PrivacyStatsDispatchContext, PrivacyStatsProvider } from './PrivacyStatsProvider.js' +import { useContext, useState, useId, useCallback } from 'preact/hooks' +import { PrivacyStatsContext, PrivacyStatsProvider } from './PrivacyStatsProvider.js' import { useVisibility } from '../widget-list/widget-config.provider.js' +import { Chevron } from '../components/Chevron.js' +import { useAutoAnimate } from '@formkit/auto-animate/preact' +import { viewTransition } from '../utils.js' /** * @typedef {import('../../../../types/new-tab').TrackerCompany} TrackerCompany * @typedef {import('../../../../types/new-tab').Expansion} Expansion + * @typedef {import('../../../../types/new-tab').Animation} Animation * @typedef {import('../../../../types/new-tab').PrivacyStatsData} PrivacyStatsData + * @typedef {import('../../../../types/new-tab').StatsConfig} StatsConfig + * @typedef {import("./PrivacyStatsProvider.js").Events} Events */ /** * @param {object} props * @param {Expansion} props.expansion * @param {PrivacyStatsData} props.data - * @param {import("./PrivacyStatsProvider.js").Ui} [props.ui] - * @param {(evt: import("./PrivacyStatsProvider.js").Events) => void} [props.send] + * @param {()=>void} props.toggle + * @param {Animation['kind']} [props.animation] - optionally configure animations + */ +export function PrivacyStats ({ expansion, data, toggle, animation = 'auto-animate' }) { + if (animation === 'auto-animate') { + return + } + + if (animation === 'view-transitions') { + return + } + + // no animations + return +} + +/** + * @param {object} props + * @param {Expansion} props.expansion + * @param {PrivacyStatsData} props.data + * @param {()=>void} props.toggle + */ +function WithViewTransitions ({ expansion, data, toggle }) { + const willToggle = useCallback(() => { + viewTransition(toggle) + }, [toggle]) + return +} + +/** + * @param {object} props + * @param {Expansion} props.expansion + * @param {PrivacyStatsData} props.data + * @param {()=>void} [props.toggle] + */ +function WithAutoAnimate ({ expansion, data, toggle }) { + const [ref] = useAutoAnimate({ duration: 100 }) + return +} + +/** + * @param {object} props + * @param {import("preact").Ref} [props.parentRef] + * @param {Expansion} props.expansion + * @param {PrivacyStatsData} props.data * @param {()=>void} [props.toggle] */ -export function PrivacyStats ({ ui, expansion, data, send, toggle }) { +function PrivacyStatsConfigured ({ parentRef, expansion, data, toggle }) { const expanded = expansion === 'expanded' - const exiting = ui && ui === 'exiting' const someCompanies = data.trackerCompanies.length > 0 + // see: https://www.w3.org/WAI/ARIA/apg/patterns/accordion/examples/accordion/ + const WIDGET_ID = useId() + const TOGGLE_ID = useId() return ( -
+
- {(expanded || exiting) && someCompanies && ( - + {expanded && someCompanies && ( + )}
) @@ -46,9 +102,9 @@ export function PrivacyStats ({ ui, expansion, data, send, toggle }) { * @param {TrackerCompany[]} props.trackerCompanies * @param {number} props.totalCount * @param {() => void} [props.onToggle] - * @param {boolean} [props.pressed] + * @param {import("preact").ComponentProps<'button'>} [props.buttonAttrs] - The maximum capacity for items to be displayed before hiding. */ -export function Heading ({ trackerCompanies, totalCount, onToggle, pressed }) { +export function Heading ({ trackerCompanies, totalCount, onToggle, buttonAttrs = {} }) { const { t } = useTypedTranslation() const [formatter] = useState(() => new Intl.NumberFormat()) const recent = trackerCompanies.reduce((sum, item) => sum + item.count, 0) @@ -76,10 +132,10 @@ export function Heading ({ trackerCompanies, totalCount, onToggle, pressed }) { )}