Skip to content

Commit d1e093d

Browse files
committed
separate
1 parent ed8bfb2 commit d1e093d

File tree

10 files changed

+502
-73
lines changed

10 files changed

+502
-73
lines changed

special-pages/pages/new-tab/app/privacy-stats/PrivacyStatsProvider.js

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { createContext, h } from 'preact';
2-
import { useEffect, useReducer, useRef } from 'preact/hooks';
2+
import { useCallback, useContext, useEffect, useReducer, useRef, useState } from 'preact/hooks';
33
import { useMessaging } from '../types.js';
44
import { PrivacyStatsService } from './privacy-stats.service.js';
55
import { reducer, useConfigSubscription, useDataSubscription, useInitialDataAndConfig } from '../service.hooks.js';
66

77
/**
88
* @typedef {import('../../types/new-tab.js').PrivacyStatsData} PrivacyStatsData
99
* @typedef {import('../../types/new-tab.js').StatsConfig} StatsConfig
10+
* @typedef {import('../../types/new-tab.js').Expansion} Expansion
1011
* @typedef {import('../service.hooks.js').State<PrivacyStatsData, StatsConfig>} State
1112
* @typedef {import('../service.hooks.js').Events<PrivacyStatsData, StatsConfig>} Events
1213
*/
@@ -25,6 +26,19 @@ export const PrivacyStatsContext = createContext({
2526

2627
export const PrivacyStatsDispatchContext = createContext(/** @type {import("preact/hooks").Dispatch<Events>} */ ({}));
2728

29+
/**
30+
* These are the values exposed to consumers.
31+
*/
32+
export const BodyExpansionContext = createContext({
33+
expansion: /** @type {Expansion} */ ('collapsed'),
34+
showMore() {},
35+
showLess() {},
36+
});
37+
38+
export function useBodyExpansion() {
39+
return useContext(BodyExpansionContext);
40+
}
41+
2842
/**
2943
* A data provider that will use `PrivacyStatsService` to fetch data, subscribe
3044
* to updates and modify state.
@@ -41,6 +55,7 @@ export function PrivacyStatsProvider(props) {
4155

4256
// const [state, dispatch] = useReducer(withLog('PrivacyStatsProvider', reducer), initial)
4357
const [state, dispatch] = useReducer(reducer, initial);
58+
const messaging = useMessaging();
4459

4560
// create an instance of `PrivacyStatsService` for the lifespan of this component.
4661
const service = useService();
@@ -54,9 +69,24 @@ export function PrivacyStatsProvider(props) {
5469
// subscribe to toggle + expose a fn for sync toggling
5570
const { toggle } = useConfigSubscription({ dispatch, service });
5671

72+
const [bodyExpansion, setBodyExpansion] = useState(/** @type {Expansion} */ ('collapsed'));
73+
74+
const showMore = useCallback(() => {
75+
messaging.statsShowMore();
76+
setBodyExpansion('expanded');
77+
}, [bodyExpansion, service]);
78+
const showLess = useCallback(() => {
79+
messaging.statsShowLess();
80+
setBodyExpansion('collapsed');
81+
}, [bodyExpansion, service]);
82+
5783
return (
5884
<PrivacyStatsContext.Provider value={{ state, toggle }}>
59-
<PrivacyStatsDispatchContext.Provider value={dispatch}>{props.children}</PrivacyStatsDispatchContext.Provider>
85+
<PrivacyStatsDispatchContext.Provider value={dispatch}>
86+
<BodyExpansionContext.Provider value={{ showMore, showLess, expansion: bodyExpansion }}>
87+
{props.children}
88+
</BodyExpansionContext.Provider>
89+
</PrivacyStatsDispatchContext.Provider>
6090
</PrivacyStatsContext.Provider>
6191
);
6292
}

special-pages/pages/new-tab/app/privacy-stats/components/PrivacyStats.examples.js

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,34 @@ export const privacyStatsExamples = {
1313
</PrivacyStatsMockProvider>
1414
),
1515
},
16+
'stats.many': {
17+
factory: () => (
18+
<PrivacyStatsMockProvider ticker={true} data={stats.many}>
19+
<PrivacyStatsConsumer />
20+
</PrivacyStatsMockProvider>
21+
),
22+
},
23+
'stats.topAndOneOther': {
24+
factory: () => (
25+
<PrivacyStatsMockProvider ticker={true} data={stats.topAndOneOther}>
26+
<PrivacyStatsConsumer />
27+
</PrivacyStatsMockProvider>
28+
),
29+
},
30+
'stats.onlyTop': {
31+
factory: () => (
32+
<PrivacyStatsMockProvider ticker={true} data={stats.onlyTop}>
33+
<PrivacyStatsConsumer />
34+
</PrivacyStatsMockProvider>
35+
),
36+
},
37+
'stats.onlyOther': {
38+
factory: () => (
39+
<PrivacyStatsMockProvider ticker={true} data={stats.onlyother}>
40+
<PrivacyStatsConsumer />
41+
</PrivacyStatsMockProvider>
42+
),
43+
},
1644
'stats.few.collapsed': {
1745
factory: () => (
1846
<PrivacyStatsMockProvider config={{ expansion: 'collapsed' }}>
@@ -42,7 +70,7 @@ export const privacyStatsExamples = {
4270
),
4371
},
4472
'stats.list': {
45-
factory: () => <PrivacyStatsBody trackerCompanies={stats.few.trackerCompanies} listAttrs={{ id: 'example-stats.list' }} />,
73+
factory: () => <PrivacyStatsBody trackerCompanies={stats.few.trackerCompanies} id={'example-stats.list'} />,
4674
},
4775
};
4876

special-pages/pages/new-tab/app/privacy-stats/components/PrivacyStats.js

Lines changed: 159 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { Fragment, h } from 'preact';
22
import styles from './PrivacyStats.module.css';
3-
import { useMessaging, useTypedTranslationWith } from '../../types.js';
3+
import { useTypedTranslationWith } from '../../types.js';
44
import { useCallback, useContext, useId, useMemo, useState } from 'preact/hooks';
5-
import { PrivacyStatsContext, PrivacyStatsProvider } from '../PrivacyStatsProvider.js';
5+
import { PrivacyStatsContext, PrivacyStatsProvider, useBodyExpansion } from '../PrivacyStatsProvider.js';
66
import { useVisibility } from '../../widget-list/widget-config.provider.js';
77
import { viewTransition } from '../../utils.js';
8-
import { ShowHideButtonFullWidth } from '../../components/ShowHideButton.jsx';
8+
import { ShowHideButtonPill } from '../../components/ShowHideButton.jsx';
99
import { useCustomizer } from '../../customizer/components/CustomizerMenu.js';
10-
import { DDG_STATS_OTHER_COMPANY_IDENTIFIER } from '../constants.js';
10+
import { DDG_STATS_DEFAULT_ROWS, DDG_STATS_OTHER_COMPANY_IDENTIFIER } from '../constants.js';
1111
import { displayNameForCompany, sortStatsForDisplay } from '../privacy-stats.utils.js';
1212
import { useCustomizerDrawerSettings } from '../../settings.provider.js';
1313
import { CompanyIcon } from '../../components/CompanyIcon.js';
@@ -91,84 +91,183 @@ function PrivacyStatsConfigured({ parentRef, expansion, data, toggle }) {
9191
id: TOGGLE_ID,
9292
}}
9393
/>
94-
{hasNamedCompanies && expanded && <PrivacyStatsBody trackerCompanies={data.trackerCompanies} listAttrs={{ id: WIDGET_ID }} />}
94+
{hasNamedCompanies && expanded && <PrivacyStatsBody trackerCompanies={data.trackerCompanies} id={WIDGET_ID} />}
9595
</div>
9696
);
9797
}
9898

9999
/**
100100
* @param {object} props
101-
* @param {import("preact").ComponentProps<'ul'>} [props.listAttrs]
102101
* @param {TrackerCompany[]} props.trackerCompanies
102+
* @param {string} props.id
103103
*/
104-
export function PrivacyStatsBody({ trackerCompanies, listAttrs = {} }) {
105-
const { t } = useTypedTranslationWith(/** @type {Strings} */ ({}));
106-
const messaging = useMessaging();
104+
export function PrivacyStatsBody({ trackerCompanies, id }) {
107105
const [formatter] = useState(() => new Intl.NumberFormat());
108-
const defaultRowMax = 5;
109106
const sorted = sortStatsForDisplay(trackerCompanies);
110-
const max = sorted[0]?.count ?? 0;
111-
const [expansion, setExpansion] = useState(/** @type {Expansion} */ ('collapsed'));
107+
const { expansion } = useBodyExpansion();
108+
109+
// prettier-ignore
110+
const visibleRows = expansion === 'expanded'
111+
/**
112+
* When expanded, show everything
113+
*/
114+
? sorted
115+
/**
116+
* When collapsed, show upto the default
117+
*/
118+
: sorted.slice(0, DDG_STATS_DEFAULT_ROWS);
119+
120+
return (
121+
<div id={id} data-testid="PrivacyStatsBody">
122+
<CompanyList rows={visibleRows} all={sorted} formatter={formatter} />
123+
<ListFooter all={sorted} />
124+
</div>
125+
);
126+
}
127+
128+
/**
129+
* @param {object} props
130+
* @param {any} props.formatter
131+
* @param {TrackerCompany[]} props.rows
132+
* @param {TrackerCompany[]} props.all
133+
*/
134+
function CompanyList({ rows, formatter, all }) {
135+
const max = all[0]?.count ?? 0;
136+
return (
137+
<ul class={styles.list} data-testid="CompanyList">
138+
{rows.map((company) => {
139+
const percentage = Math.min((company.count * 100) / max, 100);
140+
const valueOrMin = Math.max(percentage, 10);
141+
const inlineStyles = {
142+
width: `${valueOrMin}%`,
143+
};
144+
const countText = formatter.format(company.count);
145+
const displayName = displayNameForCompany(company.displayName);
146+
if (company.displayName === DDG_STATS_OTHER_COMPANY_IDENTIFIER) {
147+
return null;
148+
}
149+
return (
150+
<li key={company.displayName} class={styles.row}>
151+
<div className={styles.company}>
152+
<CompanyIcon displayName={displayName} />
153+
<span class={styles.name}>{displayName}</span>
154+
</div>
155+
<span class={styles.count}>{countText}</span>
156+
<span class={styles.bar}></span>
157+
<span class={styles.fill} style={inlineStyles}></span>
158+
</li>
159+
);
160+
})}
161+
</ul>
162+
);
163+
}
164+
165+
/**
166+
* Renders a footer element that adapts its content and behavior based on provided data and state.
167+
*
168+
* @param {Object} props - The properties passed to the Footer component.
169+
* @param {TrackerCompany[]} props.all - An array of data objects used to determine content and state of the footer.
170+
*/
171+
function ListFooter({ all }) {
172+
const { t } = useTypedTranslationWith(/** @type {Strings} */ ({}));
173+
const { expansion, showMore, showLess } = useBodyExpansion();
112174

113175
const toggleListExpansion = () => {
114176
if (expansion === 'collapsed') {
115-
messaging.statsShowMore();
177+
showMore();
116178
} else {
117-
messaging.statsShowLess();
179+
showLess();
118180
}
119-
setExpansion(expansion === 'collapsed' ? 'expanded' : 'collapsed');
120181
};
121182

122-
const rows = expansion === 'expanded' ? sorted : sorted.slice(0, defaultRowMax);
183+
const states = /** @type {const} */ ([
184+
'few_top_other',
185+
'few_top',
186+
'few_other',
187+
'many_top_other_collapsed',
188+
'many_top_other_expanded',
189+
'many_top_collapsed',
190+
'many_top_expanded',
191+
]);
192+
193+
const other = all[all.length - 1];
194+
const hasOther = other?.displayName === DDG_STATS_OTHER_COMPANY_IDENTIFIER;
195+
196+
const pill = (
197+
<div class={styles.listExpander}>
198+
<ShowHideButtonPill
199+
onClick={toggleListExpansion}
200+
label={undefined}
201+
text={expansion === 'collapsed' ? t('ntp_show_more') : t('ntp_show_less')}
202+
buttonAttrs={{
203+
'aria-expanded': expansion === 'expanded',
204+
'aria-pressed': expansion === 'expanded',
205+
}}
206+
/>
207+
</div>
208+
);
209+
210+
/** @type {states[number]} */
211+
const state = (() => {
212+
const comparison = hasOther ? DDG_STATS_DEFAULT_ROWS + 1 : DDG_STATS_DEFAULT_ROWS;
213+
if (all.length <= comparison) {
214+
if (hasOther) {
215+
if (all.length === 1) {
216+
return 'few_other';
217+
}
218+
return 'few_top_other';
219+
}
220+
return 'few_top';
221+
} else {
222+
if (hasOther) {
223+
return expansion === 'collapsed' ? 'many_top_other_collapsed' : 'many_top_other_expanded';
224+
}
225+
return expansion === 'collapsed' ? 'many_top_collapsed' : 'many_top_expanded';
226+
}
227+
})();
228+
229+
const contents = (() => {
230+
switch (state) {
231+
case 'few_other':
232+
case 'few_top_other': {
233+
return <OtherText count={other.count} />;
234+
}
235+
case 'few_top':
236+
return null;
237+
case 'many_top_collapsed': {
238+
return pill;
239+
}
240+
case 'many_top_expanded': {
241+
return pill;
242+
}
243+
case 'many_top_other_collapsed': {
244+
return pill;
245+
}
246+
case 'many_top_other_expanded':
247+
return (
248+
<Fragment>
249+
<OtherText count={other.count} />
250+
{pill}
251+
</Fragment>
252+
);
253+
default:
254+
return pill;
255+
}
256+
})();
123257

124258
return (
125-
<Fragment>
126-
<ul {...listAttrs} class={styles.list} data-testid="CompanyList">
127-
{rows.map((company) => {
128-
const percentage = Math.min((company.count * 100) / max, 100);
129-
const valueOrMin = Math.max(percentage, 10);
130-
const inlineStyles = {
131-
width: `${valueOrMin}%`,
132-
};
133-
const countText = formatter.format(company.count);
134-
const displayName = displayNameForCompany(company.displayName);
135-
if (company.displayName === DDG_STATS_OTHER_COMPANY_IDENTIFIER) {
136-
const otherText = t('stats_otherCount', { count: String(company.count) });
137-
return (
138-
<li key={company.displayName} class={styles.otherTrackersRow}>
139-
{otherText}
140-
</li>
141-
);
142-
}
143-
return (
144-
<li key={company.displayName} class={styles.row}>
145-
<div class={styles.company}>
146-
<CompanyIcon displayName={displayName} />
147-
<span class={styles.name}>{displayName}</span>
148-
</div>
149-
<span class={styles.count}>{countText}</span>
150-
<span class={styles.bar}></span>
151-
<span class={styles.fill} style={inlineStyles}></span>
152-
</li>
153-
);
154-
})}
155-
</ul>
156-
{sorted.length > defaultRowMax && (
157-
<div class={styles.listExpander}>
158-
<ShowHideButtonFullWidth
159-
onClick={toggleListExpansion}
160-
text={expansion === 'collapsed' ? t('ntp_show_more') : t('ntp_show_less')}
161-
buttonAttrs={{
162-
'aria-expanded': expansion === 'expanded',
163-
'aria-pressed': expansion === 'expanded',
164-
}}
165-
/>
166-
</div>
167-
)}
168-
</Fragment>
259+
<div class={styles.listFooter} data-testid="ListFooter">
260+
{contents}
261+
</div>
169262
);
170263
}
171264

265+
function OtherText({ count }) {
266+
const { t } = useTypedTranslationWith(/** @type {Strings} */ ({}));
267+
const otherText = t('stats_otherCount', { count: String(count) });
268+
return <div class={styles.otherTrackersRow}>{otherText}</div>;
269+
}
270+
172271
/**
173272
* Use this when rendered within a widget list.
174273
*

0 commit comments

Comments
 (0)