Skip to content

Commit f8d72a7

Browse files
maxxcrawfordsstanca-cell
authored andcommitted
Bug 2053667 - Add 'New' badge to crossword, remove it upon widget interaction. r=thecount
Differential Revision: https://phabricator.services.mozilla.com/D311237
1 parent 18f1628 commit f8d72a7

6 files changed

Lines changed: 184 additions & 11 deletions

File tree

browser/extensions/newtab/content-src/components/Widgets/Crossword/Crossword.jsx

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,19 @@ const MENU_ACTION_ITEMS = [
8787

8888
const CROSSWORD_ENTRY = WIDGET_REGISTRY.find(w => w.id === "crossword");
8989

90-
function Crossword({ dispatch, widgetsMayBeMaximized, widgetEnabledMap }) {
90+
// Flipped to true the first time the user interacts with the crossword. Used to
91+
// hide the "New" badge once the widget has been used.
92+
const PREF_CROSSWORD_INTERACTION = "widgets.crossword.interaction";
93+
94+
function Crossword({
95+
dispatch,
96+
handleUserInteraction,
97+
widgetsMayBeMaximized,
98+
widgetEnabledMap,
99+
}) {
91100
const prefs = useSelector(state => state.Prefs.values);
92101
const widgetSize = resolveWidgetSize(CROSSWORD_ENTRY, prefs);
102+
const hasInteracted = prefs[PREF_CROSSWORD_INTERACTION];
93103
const crosswordEndpoint = resolveCrosswordEndpoint(prefs);
94104
const impressionFired = useRef(false);
95105
const iframeRef = useRef(null);
@@ -98,6 +108,13 @@ function Crossword({ dispatch, widgetsMayBeMaximized, widgetEnabledMap }) {
98108
// only apply to an in-progress game (Solve puzzle) can be hidden.
99109
const [puzzleCompleted, setPuzzleCompleted] = useState(false);
100110

111+
// Any real interaction flips the interaction pref, which hides the "New"
112+
// badge. The helper is a no-op once the pref is already true.
113+
const handleInteraction = useCallback(
114+
() => handleUserInteraction("crossword"),
115+
[handleUserInteraction]
116+
);
117+
101118
// The single origin we accept inbound messages from and target for outbound
102119
// ones.
103120
const merinoOrigin = useMemo(() => {
@@ -166,6 +183,7 @@ function Crossword({ dispatch, widgetsMayBeMaximized, widgetEnabledMap }) {
166183
);
167184
break;
168185
case EVENT_TYPES.INTERACTION:
186+
handleInteraction();
169187
dispatch(
170188
ac.AlsoToMain({
171189
type: at.WIDGETS_USER_EVENT,
@@ -183,7 +201,7 @@ function Crossword({ dispatch, widgetsMayBeMaximized, widgetEnabledMap }) {
183201
break;
184202
}
185203
},
186-
[dispatch, widgetSize]
204+
[dispatch, handleInteraction, widgetSize]
187205
);
188206

189207
// Listen for events from the widget, discarding anything that fails origin,
@@ -220,6 +238,7 @@ function Crossword({ dispatch, widgetsMayBeMaximized, widgetEnabledMap }) {
220238

221239
const handleMenuAction = useCallback(
222240
action => {
241+
handleInteraction();
223242
postMenuAction(action);
224243
dispatch(
225244
ac.OnlyToMain({
@@ -234,7 +253,7 @@ function Crossword({ dispatch, widgetsMayBeMaximized, widgetEnabledMap }) {
234253
})
235254
);
236255
},
237-
[postMenuAction, dispatch, widgetSize]
256+
[handleInteraction, postMenuAction, dispatch, widgetSize]
238257
);
239258

240259
const handleIntersection = useCallback(() => {
@@ -279,6 +298,7 @@ function Crossword({ dispatch, widgetsMayBeMaximized, widgetEnabledMap }) {
279298

280299
const handleChangeSize = useCallback(
281300
size => {
301+
handleInteraction();
282302
batch(() => {
283303
dispatch(
284304
ac.OnlyToMain({
@@ -300,12 +320,13 @@ function Crossword({ dispatch, widgetsMayBeMaximized, widgetEnabledMap }) {
300320
);
301321
});
302322
},
303-
[dispatch]
323+
[dispatch, handleInteraction]
304324
);
305325

306326
const sizeSubmenuRef = useSizeSubmenu(handleChangeSize);
307327

308328
function handleLearnMore() {
329+
handleInteraction();
309330
batch(() => {
310331
dispatch(
311332
ac.OnlyToMain({
@@ -330,6 +351,7 @@ function Crossword({ dispatch, widgetsMayBeMaximized, widgetEnabledMap }) {
330351
}
331352

332353
function handlePoweredByParticle() {
354+
handleInteraction();
333355
batch(() => {
334356
dispatch(
335357
ac.OnlyToMain({
@@ -361,7 +383,15 @@ function Crossword({ dispatch, widgetsMayBeMaximized, widgetEnabledMap }) {
361383
}}
362384
>
363385
<div className="crossword-title-wrapper">
364-
<h3 className="newtab-crossword-title">Daily crossword</h3>
386+
<div className="crossword-badge-title-wrapper">
387+
{!hasInteracted && (
388+
<moz-badge
389+
className="crossword-new-badge"
390+
data-l10n-id="newtab-widget-lists-label-new"
391+
></moz-badge>
392+
)}
393+
<h3 className="newtab-crossword-title">Daily crossword</h3>
394+
</div>
365395
<div className="crossword-context-menu-wrapper">
366396
<moz-button
367397
className="crossword-context-menu-button"

browser/extensions/newtab/content-src/components/Widgets/Crossword/_Crossword.scss

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,44 @@
2222
justify-content: space-between;
2323
margin-block-end: var(--space-xsmall);
2424

25+
.crossword-badge-title-wrapper {
26+
display: flex;
27+
align-items: center;
28+
gap: var(--space-small);
29+
min-inline-size: 0;
30+
}
31+
2532
h3 {
2633
margin-block: 0;
2734
font-weight: var(--font-weight);
2835
font-size: var(--font-size-root);
2936
}
37+
38+
// moz-badge renders its visible content in shadow DOM, so it can only be
39+
// styled here via its token custom properties (padding/background) plus
40+
// inherited properties (text-transform). The green "filled" look normally
41+
// comes from type="new", which errors on the unregistered moz-badge-new2
42+
// string, so paint the pill on the host instead and recolor via the same
43+
// tokens the default badge reads.
44+
.crossword-new-badge {
45+
flex: none;
46+
background-color: var(--badge-background-color-filled);
47+
border-radius: var(--badge-border-radius);
48+
text-transform: uppercase;
49+
50+
--badge-padding-block: 1px;
51+
--badge-padding-inline: var(--space-small);
52+
--badge-background-color-filled: var(--color-green-40);
53+
--badge-text-color: var(--badge-text-color-filled);
54+
--badge-border-color: transparent;
55+
}
56+
}
57+
58+
// Keep the badge readable in High Contrast Mode by using the system accent.
59+
@media (forced-colors: active) {
60+
.crossword-new-badge {
61+
--badge-background-color-filled: AccentColor;
62+
}
3063
}
3164

3265
.crossword-context-menu-wrapper {

browser/extensions/newtab/css/activity-stream.css

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10083,12 +10083,34 @@ dialog:dir(rtl)::after {
1008310083
justify-content: space-between;
1008410084
margin-block-end: var(--space-xsmall);
1008510085
}
10086+
.crossword-title-wrapper .crossword-badge-title-wrapper {
10087+
display: flex;
10088+
align-items: center;
10089+
gap: var(--space-small);
10090+
min-inline-size: 0;
10091+
}
1008610092
.crossword-title-wrapper h3 {
1008710093
margin-block: 0;
1008810094
font-weight: var(--font-weight);
1008910095
font-size: var(--font-size-root);
1009010096
}
10097+
.crossword-title-wrapper .crossword-new-badge {
10098+
flex: none;
10099+
background-color: var(--badge-background-color-filled);
10100+
border-radius: var(--badge-border-radius);
10101+
text-transform: uppercase;
10102+
--badge-padding-block: 1px;
10103+
--badge-padding-inline: var(--space-small);
10104+
--badge-background-color-filled: var(--color-green-40);
10105+
--badge-text-color: var(--badge-text-color-filled);
10106+
--badge-border-color: transparent;
10107+
}
1009110108

10109+
@media (forced-colors: active) {
10110+
.crossword-new-badge {
10111+
--badge-background-color-filled: AccentColor;
10112+
}
10113+
}
1009210114
.crossword-context-menu-wrapper {
1009310115
opacity: 0;
1009410116
pointer-events: none;

browser/extensions/newtab/data/content/activity-stream.bundle.js

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22148,13 +22148,19 @@ const MENU_ACTION_ITEMS = [{
2214822148
hideWhenCompleted: true
2214922149
}];
2215022150
const CROSSWORD_ENTRY = WIDGET_REGISTRY.find(w => w.id === "crossword");
22151+
22152+
// Flipped to true the first time the user interacts with the crossword. Used to
22153+
// hide the "New" badge once the widget has been used.
22154+
const PREF_CROSSWORD_INTERACTION = "widgets.crossword.interaction";
2215122155
function Crossword({
2215222156
dispatch,
22157+
handleUserInteraction,
2215322158
widgetsMayBeMaximized,
2215422159
widgetEnabledMap
2215522160
}) {
2215622161
const prefs = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.Prefs.values);
2215722162
const widgetSize = resolveWidgetSize(CROSSWORD_ENTRY, prefs);
22163+
const hasInteracted = prefs[PREF_CROSSWORD_INTERACTION];
2215822164
const crosswordEndpoint = resolveCrosswordEndpoint(prefs);
2215922165
const impressionFired = (0,external_React_namespaceObject.useRef)(false);
2216022166
const iframeRef = (0,external_React_namespaceObject.useRef)(null);
@@ -22163,6 +22169,10 @@ function Crossword({
2216322169
// only apply to an in-progress game (Solve puzzle) can be hidden.
2216422170
const [puzzleCompleted, setPuzzleCompleted] = (0,external_React_namespaceObject.useState)(false);
2216522171

22172+
// Any real interaction flips the interaction pref, which hides the "New"
22173+
// badge. The helper is a no-op once the pref is already true.
22174+
const handleInteraction = (0,external_React_namespaceObject.useCallback)(() => handleUserInteraction("crossword"), [handleUserInteraction]);
22175+
2216622176
// The single origin we accept inbound messages from and target for outbound
2216722177
// ones.
2216822178
const merinoOrigin = (0,external_React_namespaceObject.useMemo)(() => {
@@ -22221,6 +22231,7 @@ function Crossword({
2222122231
}));
2222222232
break;
2222322233
case EVENT_TYPES.INTERACTION:
22234+
handleInteraction();
2222422235
dispatch(actionCreators.AlsoToMain({
2222522236
type: actionTypes.WIDGETS_USER_EVENT,
2222622237
data: {
@@ -22235,7 +22246,7 @@ function Crossword({
2223522246
default:
2223622247
break;
2223722248
}
22238-
}, [dispatch, widgetSize]);
22249+
}, [dispatch, handleInteraction, widgetSize]);
2223922250

2224022251
// Listen for events from the widget, discarding anything that fails origin,
2224122252
// source, channel, or payload validation before it can touch Redux/telemetry.
@@ -22265,6 +22276,7 @@ function Crossword({
2226522276
return () => window.removeEventListener("message", handleMessage);
2226622277
}, [merinoOrigin, handleWidgetEvent]);
2226722278
const handleMenuAction = (0,external_React_namespaceObject.useCallback)(action => {
22279+
handleInteraction();
2226822280
postMenuAction(action);
2226922281
dispatch(actionCreators.OnlyToMain({
2227022282
type: actionTypes.WIDGETS_USER_EVENT,
@@ -22276,7 +22288,7 @@ function Crossword({
2227622288
widget_size: widgetSize
2227722289
}
2227822290
}));
22279-
}, [postMenuAction, dispatch, widgetSize]);
22291+
}, [handleInteraction, postMenuAction, dispatch, widgetSize]);
2228022292
const handleIntersection = (0,external_React_namespaceObject.useCallback)(() => {
2228122293
if (impressionFired.current) {
2228222294
return;
@@ -22312,6 +22324,7 @@ function Crossword({
2231222324
});
2231322325
}
2231422326
const handleChangeSize = (0,external_React_namespaceObject.useCallback)(size => {
22327+
handleInteraction();
2231522328
(0,external_ReactRedux_namespaceObject.batch)(() => {
2231622329
dispatch(actionCreators.OnlyToMain({
2231722330
type: actionTypes.SET_PREF,
@@ -22331,9 +22344,10 @@ function Crossword({
2233122344
}
2233222345
}));
2233322346
});
22334-
}, [dispatch]);
22347+
}, [dispatch, handleInteraction]);
2233522348
const sizeSubmenuRef = useSizeSubmenu(handleChangeSize);
2233622349
function handleLearnMore() {
22350+
handleInteraction();
2233722351
(0,external_ReactRedux_namespaceObject.batch)(() => {
2233822352
dispatch(actionCreators.OnlyToMain({
2233922353
type: actionTypes.OPEN_LINK,
@@ -22353,6 +22367,7 @@ function Crossword({
2235322367
});
2235422368
}
2235522369
function handlePoweredByParticle() {
22370+
handleInteraction();
2235622371
(0,external_ReactRedux_namespaceObject.batch)(() => {
2235722372
dispatch(actionCreators.OnlyToMain({
2235822373
type: actionTypes.OPEN_LINK,
@@ -22378,9 +22393,14 @@ function Crossword({
2237822393
}
2237922394
}, /*#__PURE__*/external_React_default().createElement("div", {
2238022395
className: "crossword-title-wrapper"
22381-
}, /*#__PURE__*/external_React_default().createElement("h3", {
22396+
}, /*#__PURE__*/external_React_default().createElement("div", {
22397+
className: "crossword-badge-title-wrapper"
22398+
}, !hasInteracted && /*#__PURE__*/external_React_default().createElement("moz-badge", {
22399+
className: "crossword-new-badge",
22400+
"data-l10n-id": "newtab-widget-lists-label-new"
22401+
}), /*#__PURE__*/external_React_default().createElement("h3", {
2238222402
className: "newtab-crossword-title"
22383-
}, "Daily crossword"), /*#__PURE__*/external_React_default().createElement("div", {
22403+
}, "Daily crossword")), /*#__PURE__*/external_React_default().createElement("div", {
2238422404
className: "crossword-context-menu-wrapper"
2238522405
}, /*#__PURE__*/external_React_default().createElement("moz-button", {
2238622406
className: "crossword-context-menu-button",

browser/extensions/newtab/lib/ActivityStream.sys.mjs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1525,6 +1525,14 @@ export const PREFS_CONFIG = new Map([
15251525
value: true,
15261526
},
15271527
],
1528+
[
1529+
"widgets.crossword.interaction",
1530+
{
1531+
title:
1532+
"Boolean flag for determining if a user has interacted with the crossword widget",
1533+
value: false,
1534+
},
1535+
],
15281536
[
15291537
"widgets.stocks.enabled",
15301538
{

0 commit comments

Comments
 (0)