From bb9c497faa5ea303d263e9822f18868a727b788c Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Mon, 21 Oct 2024 16:31:49 +0100 Subject: [PATCH] ntp: keyboard support for stats (#1149) * ntp: keyboard support * linting --------- Co-authored-by: Shane Osbourne --- .../new-tab/app/components/Components.jsx | 15 ++-- .../app/components/Components.module.css | 6 ++ .../pages/new-tab/app/components/Examples.jsx | 19 ++++- .../pages/new-tab/app/components/Icons.js | 13 ++++ .../new-tab/app/components/Icons.module.css | 23 ++++++ .../app/components/ShowHide.module.css | 30 ++++++++ .../new-tab/app/components/ShowHideButton.jsx | 24 ++++++ .../new-tab/app/privacy-stats/PrivacyStats.js | 76 +++++++------------ .../app/privacy-stats/PrivacyStats.module.css | 39 ++-------- .../integration-tests/privacy-stats.spec.js} | 3 +- .../pages/new-tab/app/styles/ntp-theme.css | 4 + special-pages/pages/new-tab/app/utils.js | 9 +++ .../integration-tests/new-tab.page.js} | 18 +++-- .../integration-tests}/new-tab.spec.js | 2 +- .../pages/new-tab/src/locales/en/newtab.json | 14 +++- special-pages/playwright.config.js | 2 +- 16 files changed, 195 insertions(+), 102 deletions(-) create mode 100644 special-pages/pages/new-tab/app/components/Icons.js create mode 100644 special-pages/pages/new-tab/app/components/Icons.module.css create mode 100644 special-pages/pages/new-tab/app/components/ShowHide.module.css create mode 100644 special-pages/pages/new-tab/app/components/ShowHideButton.jsx rename special-pages/{tests/new-tab-widgets.spec.js => pages/new-tab/app/privacy-stats/integration-tests/privacy-stats.spec.js} (90%) rename special-pages/{tests/page-objects/newtab.js => pages/new-tab/integration-tests/new-tab.page.js} (82%) rename special-pages/{tests => pages/new-tab/integration-tests}/new-tab.spec.js (97%) diff --git a/special-pages/pages/new-tab/app/components/Components.jsx b/special-pages/pages/new-tab/app/components/Components.jsx index 190a4469b..993cc6d0e 100644 --- a/special-pages/pages/new-tab/app/components/Components.jsx +++ b/special-pages/pages/new-tab/app/components/Components.jsx @@ -67,16 +67,17 @@ function Stage({ entries }) { return (
- {id}{" "} - -
- select{" "} +
+ {id} Open 🔗{" "} + +
+
+ isolate{" "} + title="show this component only">select{" "} import("preact").ComponentChild}>} */ export const mainExamples = { @@ -22,13 +23,25 @@ export const mainExamples = { data={stats.norecent}> }, 'stats.list': { - factory: () => + factory: () => }, 'stats.heading': { - factory: () => + factory: () => }, 'stats.heading.none': { - factory: () => + factory: () => ( + + ) }, } diff --git a/special-pages/pages/new-tab/app/components/Icons.js b/special-pages/pages/new-tab/app/components/Icons.js new file mode 100644 index 000000000..a57b29f3c --- /dev/null +++ b/special-pages/pages/new-tab/app/components/Icons.js @@ -0,0 +1,13 @@ +import { h } from 'preact' +import styles from './Icons.module.css' + +export function ChevronButton () { + return ( + + + + + ) +} diff --git a/special-pages/pages/new-tab/app/components/Icons.module.css b/special-pages/pages/new-tab/app/components/Icons.module.css new file mode 100644 index 000000000..c419f2dab --- /dev/null +++ b/special-pages/pages/new-tab/app/components/Icons.module.css @@ -0,0 +1,23 @@ +.chevron { + &:hover .chevronCircle { + fill: black; + fill-opacity: 0.06; + + @media screen and (prefers-color-scheme: dark) { + fill: white; + fill-opacity: 0.12 + } + } +} + +.chevronCircle { + transition: all .3s; + fill-opacity: 0; +} + +.chevronArrow { + @media screen and (prefers-color-scheme: dark) { + fill: white; + fill-opacity: 0.5 + } +} diff --git a/special-pages/pages/new-tab/app/components/ShowHide.module.css b/special-pages/pages/new-tab/app/components/ShowHide.module.css new file mode 100644 index 000000000..acef65d02 --- /dev/null +++ b/special-pages/pages/new-tab/app/components/ShowHide.module.css @@ -0,0 +1,30 @@ +.button { + opacity: 0; + transition: opacity 0.3s; + cursor: pointer; + background: none; + border: none; + outline: none; + display: flex; + justify-content: center; + align-items: center; + color: var(--ntp-text-normal); + background: var(--ntp-background-color); + height: 32px; + width: 32px; + line-height: 32px; + gap: 6px; + padding-left: 0; + padding-right: 0; + font-size: 11px; + border-radius: 50%; + + &[aria-pressed=true] svg { + transform: rotate(180deg); + } + + &:focus-visible { + opacity: 1; + outline: 1px dotted var(--ntp-focus-outline-color); + } +} diff --git a/special-pages/pages/new-tab/app/components/ShowHideButton.jsx b/special-pages/pages/new-tab/app/components/ShowHideButton.jsx new file mode 100644 index 000000000..67f5e58f0 --- /dev/null +++ b/special-pages/pages/new-tab/app/components/ShowHideButton.jsx @@ -0,0 +1,24 @@ +import styles from "./ShowHide.module.css"; +import { ChevronButton } from "./Icons.js"; +import { h } from "preact"; + +/** + * Function to handle showing or hiding content based on certain conditions. + * + * @param {Object} props - Input parameters for controlling the behavior of the ShowHide functionality. + * @param {string} props.text + * @param {() => void} props.onClick + * @param {import("preact").ComponentProps<'button'>} [props.buttonAttrs] + */ +export function ShowHideButton({ text, onClick, buttonAttrs = {} }) { + return ( + + ) +} 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 888932796..16bf0809a 100644 --- a/special-pages/pages/new-tab/app/privacy-stats/PrivacyStats.js +++ b/special-pages/pages/new-tab/app/privacy-stats/PrivacyStats.js @@ -1,13 +1,11 @@ import { h } from 'preact' -import cn from 'classnames' import styles from './PrivacyStats.module.css' import { useTypedTranslation } from '../types.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' +import { ShowHideButton } from '../components/ShowHideButton.jsx' /** * @typedef {import('../../../../types/new-tab').TrackerCompany} TrackerCompany @@ -26,10 +24,6 @@ import { viewTransition } from '../utils.js' * @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 } @@ -51,23 +45,12 @@ function WithViewTransitions ({ expansion, data, 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] + * @param {()=>void} props.toggle */ function PrivacyStatsConfigured ({ parentRef, expansion, data, toggle }) { const expanded = expansion === 'expanded' @@ -82,16 +65,14 @@ function PrivacyStatsConfigured ({ parentRef, expansion, data, toggle }) { totalCount={data.totalCount} trackerCompanies={data.trackerCompanies} onToggle={toggle} + expansion={expansion} buttonAttrs={{ - 'aria-expanded': expansion === 'expanded', - 'aria-pressed': expansion === 'expanded', 'aria-controls': WIDGET_ID, id: TOGGLE_ID }} /> - {expanded && someCompanies && ( - + )}
) @@ -99,12 +80,13 @@ function PrivacyStatsConfigured ({ parentRef, expansion, data, toggle }) { /** * @param {object} props + * @param {Expansion} props.expansion * @param {TrackerCompany[]} props.trackerCompanies * @param {number} props.totalCount - * @param {() => void} [props.onToggle] - * @param {import("preact").ComponentProps<'button'>} [props.buttonAttrs] - The maximum capacity for items to be displayed before hiding. + * @param {() => void} props.onToggle + * @param {import("preact").ComponentProps<'button'>} [props.buttonAttrs] */ -export function Heading ({ trackerCompanies, totalCount, onToggle, buttonAttrs = {} }) { +export function Heading ({ expansion, trackerCompanies, totalCount, onToggle, buttonAttrs = {} }) { const { t } = useTypedTranslation() const [formatter] = useState(() => new Intl.NumberFormat()) const recent = trackerCompanies.reduce((sum, item) => sum + item.count, 0) @@ -131,16 +113,18 @@ export function Heading ({ trackerCompanies, totalCount, onToggle, buttonAttrs =

{alltimeTitle}

)} - + text={expansion === 'expanded' + ? t('trackerStatsHideLabel') + : t('trackerStatsToggleLabel')} + />

{recent === 0 && t('trackerStatsNoActivity')} @@ -152,19 +136,15 @@ export function Heading ({ trackerCompanies, totalCount, onToggle, buttonAttrs = /** * @param {object} props + * @param {import("preact").ComponentProps<'ul'>} [props.listAttrs] * @param {TrackerCompany[]} props.trackerCompanies - * @param {string} props.id */ -export function Body ({ trackerCompanies, id }) { +export function Body ({ trackerCompanies, listAttrs = {} }) { const max = trackerCompanies[0]?.count ?? 0 const [formatter] = useState(() => new Intl.NumberFormat()) - const bodyClasses = cn({ - [styles.list]: true - }) - return ( -

    +
      {trackerCompanies.map(company => { const percentage = Math.min((company.count * 100) / max, 100) const valueOrMin = Math.max(percentage, 10) @@ -174,14 +154,14 @@ export function Body ({ trackerCompanies, id }) { const countText = formatter.format(company.count) return (
    • -
      -
      +
      +
      - {company.displayName} + {company.displayName}
      - {countText} - - + {countText} + +
    • ) diff --git a/special-pages/pages/new-tab/app/privacy-stats/PrivacyStats.module.css b/special-pages/pages/new-tab/app/privacy-stats/PrivacyStats.module.css index 94f078d97..677f417f3 100644 --- a/special-pages/pages/new-tab/app/privacy-stats/PrivacyStats.module.css +++ b/special-pages/pages/new-tab/app/privacy-stats/PrivacyStats.module.css @@ -17,7 +17,7 @@ .expanded {} .heading { display: grid; - grid-template-columns: 24px auto 24px; + grid-template-columns: 24px auto 32px; grid-column-gap: 12px; grid-row-gap: 12px; grid-template-rows: auto auto; @@ -46,44 +46,15 @@ font-weight: var(--title-2-font-weight); line-height: var(--title-2-line-height); } -.expander { - grid-area: expander; -} -.toggle { - width: 24px; - height: 24px; +.expander { position: relative; - background: none; - outline: none; - border: none; - border-radius: 4px; - cursor: pointer; - transition: background-color ease-in-out .2s; - - color: var(--color-black-at-48); - @media screen and (prefers-color-scheme: dark) { - color: var(--color-white-at-60); - } - &:hover { - background: var(--color-black-at-6); - @media screen and (prefers-color-scheme: dark) { - background: var(--color-white-at-9); - } - - } - - svg { + & [aria-controls] { position: absolute; top: 50%; - left: 50%; - transform: translate(-50%, -50%); - pointer-events: none; - } - - &[aria-pressed=false] { - transform: rotate(180deg); + transform: translateY(-50%); + opacity: 1; } } diff --git a/special-pages/tests/new-tab-widgets.spec.js b/special-pages/pages/new-tab/app/privacy-stats/integration-tests/privacy-stats.spec.js similarity index 90% rename from special-pages/tests/new-tab-widgets.spec.js rename to special-pages/pages/new-tab/app/privacy-stats/integration-tests/privacy-stats.spec.js index a0e7b68f6..9266760dc 100644 --- a/special-pages/tests/new-tab-widgets.spec.js +++ b/special-pages/pages/new-tab/app/privacy-stats/integration-tests/privacy-stats.spec.js @@ -1,11 +1,12 @@ import { test, expect } from '@playwright/test' -import { NewtabPage } from './page-objects/newtab' +import { NewtabPage } from '../../../integration-tests/new-tab.page.js' test.describe('newtab privacy stats', () => { test('fetches config + stats', async ({ page }, workerInfo) => { const ntp = NewtabPage.create(page, workerInfo) await ntp.reducedMotion() await ntp.openPage() + const calls1 = await ntp.mocks.waitForCallCount({ method: 'initialSetup', count: 1 }) const calls2 = await ntp.mocks.waitForCallCount({ method: 'stats_getData', count: 1 }) const calls3 = await ntp.mocks.waitForCallCount({ method: 'stats_getConfig', count: 1 }) diff --git a/special-pages/pages/new-tab/app/styles/ntp-theme.css b/special-pages/pages/new-tab/app/styles/ntp-theme.css index df638485a..94f0e6ca9 100644 --- a/special-pages/pages/new-tab/app/styles/ntp-theme.css +++ b/special-pages/pages/new-tab/app/styles/ntp-theme.css @@ -19,11 +19,15 @@ --title-3-em-font-weight: 590; --title-3-em-line-height: 20px; + --ntp-focus-outline-color: black; + + @media (prefers-color-scheme: dark) { --ntp-background-color: var(--color-gray-85); --ntp-surface-background-color: var(--color-black-at-18); --ntp-surface-border-color: var(--color-black-at-6); --ntp-text-normal: white; + --ntp-focus-outline-color: white; } } diff --git a/special-pages/pages/new-tab/app/utils.js b/special-pages/pages/new-tab/app/utils.js index 12f9d9cac..89225921e 100644 --- a/special-pages/pages/new-tab/app/utils.js +++ b/special-pages/pages/new-tab/app/utils.js @@ -49,3 +49,12 @@ export function viewTransition (fn) { } return fn() } + +/** + * + */ +export function noop (named) { + return () => { + console.log(named, 'noop') + } +} diff --git a/special-pages/tests/page-objects/newtab.js b/special-pages/pages/new-tab/integration-tests/new-tab.page.js similarity index 82% rename from special-pages/tests/page-objects/newtab.js rename to special-pages/pages/new-tab/integration-tests/new-tab.page.js index b57e4cf9e..75f645ec1 100644 --- a/special-pages/tests/page-objects/newtab.js +++ b/special-pages/pages/new-tab/integration-tests/new-tab.page.js @@ -1,10 +1,10 @@ -import { Mocks } from './mocks.js' -import { perPlatform } from '../../../injected/integration-test/type-helpers.mjs' +import { Mocks } from '../../../tests/page-objects/mocks.js' import { join } from 'node:path' +import { perPlatform } from 'injected/integration-test/type-helpers.mjs' /** - * @typedef {import('../../../injected/integration-test/type-helpers.mjs').Build} Build - * @typedef {import('../../../injected/integration-test/type-helpers.mjs').PlatformInfo} PlatformInfo + * @typedef {import('injected/integration-test/type-helpers.mjs').Build} Build + * @typedef {import('injected/integration-test/type-helpers.mjs').PlatformInfo} PlatformInfo */ export class NewtabPage { @@ -27,7 +27,7 @@ export class NewtabPage { this.mocks.defaultResponses({ requestSetAsDefault: {}, requestImport: {}, - /** @type {import('../../types/new-tab.js').InitialSetupResponse} */ + /** @type {import('../../../types/new-tab.ts').InitialSetupResponse} */ initialSetup: { widgets: [ { id: 'favorites' }, @@ -56,8 +56,9 @@ export class NewtabPage { * @param {Object} [params] - Optional parameters for opening the page. * @param {'debug' | 'production'} [params.mode] - Optional parameters for opening the page. * @param {boolean} [params.willThrow] - Optional flag to simulate an exception + * @param {number} [params.favoritesCount] - Optional flag to preload a list of favorites */ - async openPage ({ mode = 'debug', willThrow = false } = { }) { + async openPage ({ mode = 'debug', willThrow = false, favoritesCount } = { }) { await this.mocks.install() await this.page.route('/**', (route, req) => { const url = new URL(req.url()) @@ -71,6 +72,11 @@ export class NewtabPage { }) }) const searchParams = new URLSearchParams({ mode, willThrow: String(willThrow) }) + + if (favoritesCount !== undefined) { + searchParams.set('favorites', String(favoritesCount)) + } + await this.page.goto('/' + '?' + searchParams.toString()) } diff --git a/special-pages/tests/new-tab.spec.js b/special-pages/pages/new-tab/integration-tests/new-tab.spec.js similarity index 97% rename from special-pages/tests/new-tab.spec.js rename to special-pages/pages/new-tab/integration-tests/new-tab.spec.js index 574438e44..42cad91b4 100644 --- a/special-pages/tests/new-tab.spec.js +++ b/special-pages/pages/new-tab/integration-tests/new-tab.spec.js @@ -1,5 +1,5 @@ import { test, expect } from '@playwright/test' -import { NewtabPage } from './page-objects/newtab' +import { NewtabPage } from './new-tab.page.js' test.describe('newtab widgets', () => { test('widget config single click', async ({ page }, workerInfo) => { diff --git a/special-pages/pages/new-tab/src/locales/en/newtab.json b/special-pages/pages/new-tab/src/locales/en/newtab.json index e3038c6e6..0c1173016 100644 --- a/special-pages/pages/new-tab/src/locales/en/newtab.json +++ b/special-pages/pages/new-tab/src/locales/en/newtab.json @@ -39,6 +39,18 @@ }, "trackerStatsToggleLabel": { "title": "Show recent activity", - "note": "The aria-label text for a toggle button that shows/hides the detailed feed" + "note": "The aria-label text for a toggle button that shows the detailed activity feed" + }, + "trackerStatsHideLabel": { + "title": "Hide recent activity", + "note": "The aria-label text for a toggle button that hides the detailed activity feed" + }, + "favorites_show_less": { + "title": "Show less", + "note": "" + }, + "favorites_show_more": { + "title": "Show more ({count} remaining)", + "note": "" } } diff --git a/special-pages/playwright.config.js b/special-pages/playwright.config.js index 19e337414..2fe20c951 100644 --- a/special-pages/playwright.config.js +++ b/special-pages/playwright.config.js @@ -18,7 +18,7 @@ export default defineConfig({ { name: 'integration', testMatch: [ - 'new-tab-widgets.spec.js', + 'privacy-stats.spec.js', 'new-tab.spec.js' ], use: {