|
1 | 1 | import { Fragment, h } from 'preact';
|
2 | 2 | import styles from './PrivacyStats.module.css';
|
3 |
| -import { useMessaging, useTypedTranslationWith } from '../../types.js'; |
| 3 | +import { useTypedTranslationWith } from '../../types.js'; |
4 | 4 | import { useCallback, useContext, useId, useMemo, useState } from 'preact/hooks';
|
5 |
| -import { PrivacyStatsContext, PrivacyStatsProvider } from '../PrivacyStatsProvider.js'; |
| 5 | +import { PrivacyStatsContext, PrivacyStatsProvider, useBodyExpansion } from '../PrivacyStatsProvider.js'; |
6 | 6 | import { useVisibility } from '../../widget-list/widget-config.provider.js';
|
7 | 7 | import { viewTransition } from '../../utils.js';
|
8 |
| -import { ShowHideButtonFullWidth } from '../../components/ShowHideButton.jsx'; |
| 8 | +import { ShowHideButtonPill } from '../../components/ShowHideButton.jsx'; |
9 | 9 | 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'; |
11 | 11 | import { displayNameForCompany, sortStatsForDisplay } from '../privacy-stats.utils.js';
|
12 | 12 | import { useCustomizerDrawerSettings } from '../../settings.provider.js';
|
13 | 13 | import { CompanyIcon } from '../../components/CompanyIcon.js';
|
@@ -91,84 +91,183 @@ function PrivacyStatsConfigured({ parentRef, expansion, data, toggle }) {
|
91 | 91 | id: TOGGLE_ID,
|
92 | 92 | }}
|
93 | 93 | />
|
94 |
| - {hasNamedCompanies && expanded && <PrivacyStatsBody trackerCompanies={data.trackerCompanies} listAttrs={{ id: WIDGET_ID }} />} |
| 94 | + {hasNamedCompanies && expanded && <PrivacyStatsBody trackerCompanies={data.trackerCompanies} id={WIDGET_ID} />} |
95 | 95 | </div>
|
96 | 96 | );
|
97 | 97 | }
|
98 | 98 |
|
99 | 99 | /**
|
100 | 100 | * @param {object} props
|
101 |
| - * @param {import("preact").ComponentProps<'ul'>} [props.listAttrs] |
102 | 101 | * @param {TrackerCompany[]} props.trackerCompanies
|
| 102 | + * @param {string} props.id |
103 | 103 | */
|
104 |
| -export function PrivacyStatsBody({ trackerCompanies, listAttrs = {} }) { |
105 |
| - const { t } = useTypedTranslationWith(/** @type {Strings} */ ({})); |
106 |
| - const messaging = useMessaging(); |
| 104 | +export function PrivacyStatsBody({ trackerCompanies, id }) { |
107 | 105 | const [formatter] = useState(() => new Intl.NumberFormat());
|
108 |
| - const defaultRowMax = 5; |
109 | 106 | 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(); |
112 | 174 |
|
113 | 175 | const toggleListExpansion = () => {
|
114 | 176 | if (expansion === 'collapsed') {
|
115 |
| - messaging.statsShowMore(); |
| 177 | + showMore(); |
116 | 178 | } else {
|
117 |
| - messaging.statsShowLess(); |
| 179 | + showLess(); |
118 | 180 | }
|
119 |
| - setExpansion(expansion === 'collapsed' ? 'expanded' : 'collapsed'); |
120 | 181 | };
|
121 | 182 |
|
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 | + })(); |
123 | 257 |
|
124 | 258 | 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> |
169 | 262 | );
|
170 | 263 | }
|
171 | 264 |
|
| 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 | + |
172 | 271 | /**
|
173 | 272 | * Use this when rendered within a widget list.
|
174 | 273 | *
|
|
0 commit comments