From fdba2a4056cab4feffb95c69d946bc1d2896373b Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Fri, 11 Oct 2024 09:48:31 +0100 Subject: [PATCH] ntp: stats feedback --- .gitignore | 1 + Package.swift | 2 +- .../ContentScopeScripts/dist/pages/.gitignore | 1 + package-lock.json | 17 +- .../messages/new-tab/examples/stats.js | 10 +- .../messages/new-tab/types/animation.json | 39 +++ .../messages/new-tab/types/stats-config.json | 3 +- .../new-tab/app/components/App.module.css | 2 + .../pages/new-tab/app/components/Chevron.js | 11 + .../new-tab/app/components/Components.jsx | 231 ++++++++++++------ .../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 ++ 25 files changed, 722 insertions(+), 359 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 921f376001..9fb7768966 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 55fb861f95..5c7881c216 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 0000000000..72e8ffc0db --- /dev/null +++ b/Sources/ContentScopeScripts/dist/pages/.gitignore @@ -0,0 +1 @@ +* diff --git a/package-lock.json b/package-lock.json index ab6bb5ac62..f8da725314 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4347,9 +4347,10 @@ } }, "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.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.2.tgz", + "integrity": "sha512-1cSoF0aCC8uaARATfrlz4VCBqE8LwZwRfLgkxJOQwAlQt6ayTmi0D9OF7nXid1POI5SZidFuG9CnlXbDfLqY/Q==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -5400,7 +5401,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": "*", @@ -8377,9 +8378,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.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.2.tgz", + "integrity": "sha512-1cSoF0aCC8uaARATfrlz4VCBqE8LwZwRfLgkxJOQwAlQt6ayTmi0D9OF7nXid1POI5SZidFuG9CnlXbDfLqY/Q==" }, "prelude-ls": { "version": "1.2.1", @@ -8652,7 +8653,7 @@ "classnames": "^2.3.2", "fast-check": "^3.22.0", "http-server": "^14.1.1", - "preact": "^10.19.3", + "preact": "^10.24.2", "web-resource-inliner": "^6.0.1" } }, diff --git a/special-pages/messages/new-tab/examples/stats.js b/special-pages/messages/new-tab/examples/stats.js index cb4be3af92..4d23135c74 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 0000000000..51a86d43d8 --- /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 44b35cab0c..2c0d53fa52 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/pages/new-tab/app/components/App.module.css b/special-pages/pages/new-tab/app/components/App.module.css index a3f6cf67de..76fb547cfb 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 0000000000..baa5763f00 --- /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 60c58c815b..190a4469b1 100644 --- a/special-pages/pages/new-tab/app/components/Components.jsx +++ b/special-pages/pages/new-tab/app/components/Components.jsx @@ -1,57 +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"; - -/** @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: () => - }, -} +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 (
- - + +
) } @@ -66,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()}
) @@ -76,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 && ( + + )} + +
) } @@ -110,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"); @@ -145,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 73a1a50bcb..f970e75fe7 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 0000000000..e69de29bb2 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 0000000000..4257f515ac --- /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 791631a887..888932796a 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 }) { )}