@stapel/listings-react 0.25.5 → 0.25.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/dist/default/favorite.d.ts +39 -3
- package/dist/default/favorite.d.ts.map +1 -1
- package/dist/default/favorite.js +53 -3
- package/dist/default/favorite.js.map +1 -1
- package/llms.txt +1 -1
- package/manifest.json +1 -1
- package/nav-manifest.json +1 -1
- package/package.json +4 -4
- package/src/analytics/generated/events.json +1 -1
- package/src/default/favorite.tsx +75 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @stapel/listings-react
|
|
2
2
|
|
|
3
|
+
## 0.25.6
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 33b10b3: The card's favourite heart is never inert for a visitor. Where the surface
|
|
8
|
+
hands in a `signIn` door, a press GOES THROUGH IT — `?next=` and all — and the
|
|
9
|
+
heart is no longer announced as unavailable, because a control that acts on
|
|
10
|
+
press must not call itself disabled. Measured on the phone feed: the heart was
|
|
11
|
+
`aria-disabled`, a tap produced nothing whatever, and the only explanation
|
|
12
|
+
lived in a hover disclosure a finger never triggers. The reason stays where it
|
|
13
|
+
was, wired by `aria-describedby` and disclosed on hover as a hint. With no door
|
|
14
|
+
in hand nothing changes: blocked, alive, saying why.
|
|
15
|
+
|
|
3
16
|
## 0.25.5
|
|
4
17
|
|
|
5
18
|
### Patch Changes
|
|
@@ -49,9 +49,17 @@ export interface FavoriteHeartProps {
|
|
|
49
49
|
* tree either way — see {@link GateReasonPopover}.
|
|
50
50
|
*/
|
|
51
51
|
readonly blockedReason?: "text" | "popover";
|
|
52
|
-
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
52
|
+
/**
|
|
53
|
+
* The surface's sign-in door.
|
|
54
|
+
*
|
|
55
|
+
* Given, it is where a VISITOR'S PRESS GOES, in both volumes (D431) — the
|
|
56
|
+
* heart stops being a control that refuses and becomes the way in, `?next=`
|
|
57
|
+
* and all. The `"popover"` arm additionally renders it inside the
|
|
58
|
+
* disclosure, as it always did.
|
|
59
|
+
*
|
|
60
|
+
* Absent, nothing changes: the control stays blocked-but-alive and states
|
|
61
|
+
* its reason, which is all a surface with no sign-in route can offer.
|
|
62
|
+
*/
|
|
55
63
|
readonly signIn?: SignInCta;
|
|
56
64
|
readonly style?: CSSProperties;
|
|
57
65
|
}
|
|
@@ -72,6 +80,34 @@ export interface FavoriteHeartProps {
|
|
|
72
80
|
* was supposed to explain it. Measured on a phone: a signed-out visitor's
|
|
73
81
|
* heart produced no toast, no reason and no navigation, and on a touch device
|
|
74
82
|
* there is no hover to fall back on.
|
|
83
|
+
*
|
|
84
|
+
* ── And where there is a DOOR, it is not "blocked" at all (D431) ──────────
|
|
85
|
+
*
|
|
86
|
+
* Live-but-refusing was half the answer. On the deployed phone SERP the
|
|
87
|
+
* visitor's heart still carried `aria-disabled="true"`, a tap still produced
|
|
88
|
+
* nothing at all, and the only explanation was a hover disclosure a finger
|
|
89
|
+
* never triggers — the reason had been pooled into the pane's footnote, and
|
|
90
|
+
* neither this control nor `GatedControl` had ever been handed
|
|
91
|
+
* `onBlockedActivate`, so the gesture fell into the same hole `GatedControl`
|
|
92
|
+
* was written to close.
|
|
93
|
+
*
|
|
94
|
+
* The owner's ruling: **the control is never inert for a visitor.** Where the
|
|
95
|
+
* surface hands in a `signIn` door, a press GOES THROUGH IT, and the heart is
|
|
96
|
+
* therefore not announced as disabled either — a control that acts on press
|
|
97
|
+
* and calls itself unavailable is lying to exactly the people who most depend
|
|
98
|
+
* on the announcement. The reason stays where it was (registered with the
|
|
99
|
+
* pane's scope, wired by `aria-describedby`, hover disclosure and all on the
|
|
100
|
+
* surfaces that have one); it is now a HINT beside a live control rather than
|
|
101
|
+
* the only thing behind a dead one.
|
|
102
|
+
*
|
|
103
|
+
* Where the host hands in NO door there is nothing better for a press to do,
|
|
104
|
+
* and the control keeps the gated shape it has always had — blocked, alive,
|
|
105
|
+
* saying why.
|
|
106
|
+
*
|
|
107
|
+
* The `href` arm renders the heart as an anchor, deliberately: the door is a
|
|
108
|
+
* navigation (`/login?next=…`), and an anchor keeps the `next`, the middle
|
|
109
|
+
* click, and the person who is not running our JavaScript. `aria-pressed`
|
|
110
|
+
* comes off in that arm, because a link is not a toggle.
|
|
75
111
|
*/
|
|
76
112
|
export declare function FavoriteHeart(props: FavoriteHeartProps): ReactElement;
|
|
77
113
|
//# sourceMappingURL=favorite.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"favorite.d.ts","sourceRoot":"","sources":["../../src/default/favorite.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AAIzD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAO9C,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;qEACiE;IACjE,QAAQ,CAAC,SAAS,EAAE,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC;IAC/C,sEAAsE;IACtE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;+EAC2E;IAC3E,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;IACrC;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C
|
|
1
|
+
{"version":3,"file":"favorite.d.ts","sourceRoot":"","sources":["../../src/default/favorite.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AAIzD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAO9C,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;qEACiE;IACjE,QAAQ,CAAC,SAAS,EAAE,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC;IAC/C,sEAAsE;IACtE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;+EAC2E;IAC3E,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;IACrC;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,CAAC;IAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC;CAChC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,YAAY,CAkHrE"}
|
package/dist/default/favorite.js
CHANGED
|
@@ -24,6 +24,34 @@ import { HeartIcon } from "./icons.js";
|
|
|
24
24
|
* was supposed to explain it. Measured on a phone: a signed-out visitor's
|
|
25
25
|
* heart produced no toast, no reason and no navigation, and on a touch device
|
|
26
26
|
* there is no hover to fall back on.
|
|
27
|
+
*
|
|
28
|
+
* ── And where there is a DOOR, it is not "blocked" at all (D431) ──────────
|
|
29
|
+
*
|
|
30
|
+
* Live-but-refusing was half the answer. On the deployed phone SERP the
|
|
31
|
+
* visitor's heart still carried `aria-disabled="true"`, a tap still produced
|
|
32
|
+
* nothing at all, and the only explanation was a hover disclosure a finger
|
|
33
|
+
* never triggers — the reason had been pooled into the pane's footnote, and
|
|
34
|
+
* neither this control nor `GatedControl` had ever been handed
|
|
35
|
+
* `onBlockedActivate`, so the gesture fell into the same hole `GatedControl`
|
|
36
|
+
* was written to close.
|
|
37
|
+
*
|
|
38
|
+
* The owner's ruling: **the control is never inert for a visitor.** Where the
|
|
39
|
+
* surface hands in a `signIn` door, a press GOES THROUGH IT, and the heart is
|
|
40
|
+
* therefore not announced as disabled either — a control that acts on press
|
|
41
|
+
* and calls itself unavailable is lying to exactly the people who most depend
|
|
42
|
+
* on the announcement. The reason stays where it was (registered with the
|
|
43
|
+
* pane's scope, wired by `aria-describedby`, hover disclosure and all on the
|
|
44
|
+
* surfaces that have one); it is now a HINT beside a live control rather than
|
|
45
|
+
* the only thing behind a dead one.
|
|
46
|
+
*
|
|
47
|
+
* Where the host hands in NO door there is nothing better for a press to do,
|
|
48
|
+
* and the control keeps the gated shape it has always had — blocked, alive,
|
|
49
|
+
* saying why.
|
|
50
|
+
*
|
|
51
|
+
* The `href` arm renders the heart as an anchor, deliberately: the door is a
|
|
52
|
+
* navigation (`/login?next=…`), and an anchor keeps the `next`, the middle
|
|
53
|
+
* click, and the person who is not running our JavaScript. `aria-pressed`
|
|
54
|
+
* comes off in that arm, because a link is not a toggle.
|
|
27
55
|
*/
|
|
28
56
|
export function FavoriteHeart(props) {
|
|
29
57
|
const t = useT();
|
|
@@ -38,13 +66,35 @@ export function FavoriteHeart(props) {
|
|
|
38
66
|
const icon = (_jsx(HeartIcon, { filled: favorite.favorited, ...(favorite.favorited ? { color: token.colorPrimary } : {}) }));
|
|
39
67
|
// The RESOLVED sentence, not the gate's key — `useActionGate` is the one
|
|
40
68
|
// place a blocked reason becomes words in this fleet.
|
|
41
|
-
const
|
|
69
|
+
const gate = useActionGate(favorite.gate);
|
|
70
|
+
const reason = gate.reason;
|
|
71
|
+
// D431. A blocked gate plus a door the surface handed in: the press is not
|
|
72
|
+
// refused, it is ROUTED — through the door, keeping whatever `?next=` the
|
|
73
|
+
// container put in the href. With no door there is nothing better for the
|
|
74
|
+
// press to do and the gated shape stands, exactly as before.
|
|
75
|
+
const door = props.signIn;
|
|
76
|
+
const opensDoor = gate.disabled && door !== undefined;
|
|
77
|
+
// Exactly one arm, never both — `SignInCta`'s own rule. The `href` arm
|
|
78
|
+
// makes antd render an anchor, which is the honest element for a door.
|
|
79
|
+
const doorPress = !opensDoor || door === undefined
|
|
80
|
+
? {}
|
|
81
|
+
: door.href !== undefined
|
|
82
|
+
? { href: door.href }
|
|
83
|
+
: { onClick: door.onSignIn };
|
|
84
|
+
// The heart, as the visitor with a door meets it: live, not announced as
|
|
85
|
+
// unavailable, and not pretending to be a toggle when it is a link.
|
|
86
|
+
const asDoor = opensDoor
|
|
87
|
+
? {
|
|
88
|
+
...doorPress,
|
|
89
|
+
...(door?.href !== undefined ? {} : { "aria-pressed": favorite.favorited }),
|
|
90
|
+
}
|
|
91
|
+
: { "aria-pressed": favorite.favorited, onClick: favorite.toggle };
|
|
42
92
|
// A failed save, stated where the heart is. The rollback already put the
|
|
43
93
|
// icon back; this says why it went back.
|
|
44
94
|
const failure = (_jsx(ErrorAlert, { testId: `${props.testId}-error`, thrown: favorite.error, variant: "inline" }));
|
|
45
95
|
if (props.blockedReason === "popover" && reason !== undefined) {
|
|
46
|
-
return (_jsx(GateReasonPopover, { reason: reason, cta: props.signIn, testId: `${props.testId}-reason`, signInTestId: `${props.testId}-sign-in`, children: (bind) => (_jsx(Button, { shape: "circle", "aria-disabled": true, ...bind, "aria-label": label, "
|
|
96
|
+
return (_jsx(GateReasonPopover, { reason: reason, cta: props.signIn, testId: `${props.testId}-reason`, signInTestId: `${props.testId}-sign-in`, children: (bind) => (_jsx(Button, { shape: "circle", ...(opensDoor ? {} : { "aria-disabled": true }), ...bind, "aria-label": label, "data-testid": props.testId, "data-favorited": String(favorite.favorited), "data-analytics": "none", "data-analytics-reason": "business action \u2014 host app wraps with its own tracked()", ...asDoor, icon: icon, ...(props.style !== undefined ? { style: props.style } : {}) })) }));
|
|
47
97
|
}
|
|
48
|
-
return (_jsx(GatedControl, { gate: favorite.gate, testId: `${props.testId}-gate`, ...(props.layout !== undefined ? { layout: props.layout } : {}), ...(props.style !== undefined ? { style: props.style } : {}), children: (bind) => (_jsxs(_Fragment, { children: [_jsx(Button, { shape: "circle", ...bind, "aria-label": label, "
|
|
98
|
+
return (_jsx(GatedControl, { gate: favorite.gate, testId: `${props.testId}-gate`, ...(opensDoor ? { whenBlocked: "annotate" } : {}), ...(props.layout !== undefined ? { layout: props.layout } : {}), ...(props.style !== undefined ? { style: props.style } : {}), children: (bind) => (_jsxs(_Fragment, { children: [_jsx(Button, { shape: "circle", ...bind, "aria-label": label, "data-testid": props.testId, "data-favorited": String(favorite.favorited), "data-analytics": "none", "data-analytics-reason": "business action \u2014 host app wraps with its own tracked()", ...asDoor, icon: icon }), failure] })) }));
|
|
49
99
|
}
|
|
50
100
|
//# sourceMappingURL=favorite.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"favorite.js","sourceRoot":"","sources":["../../src/default/favorite.tsx"],"names":[],"mappings":";AA4BA,OAAO,EAAE,KAAK,IAAI,SAAS,EAAE,MAAM,MAAM,CAAC;AAC1C,OAAO,EAAE,UAAU,IAAI,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAEpE,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"favorite.js","sourceRoot":"","sources":["../../src/default/favorite.tsx"],"names":[],"mappings":"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"}
|
package/llms.txt
CHANGED
package/manifest.json
CHANGED
package/nav-manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stapel/listings-react",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.6",
|
|
4
4
|
"description": "Headless React pair for stapel-listings: a typed client over the draft twin, the composer that turns a category schema into a submittable listing, and the owner's dashboard. Two independent axes are rendered as two — lifecycle `status` decides visibility and `moderation_status` decides nothing about it, so an edit to a live listing stays live and says 'under review' instead of vanishing. Feature values are drawn and mirrored by @stapel/attributes-react; photos arrive as an injected upload bag whose `refs` ARE `images_draft` and whose `settled` gates the submit; a publish refusal is routed onto the control that caused it by slug. Zero visual opinion in the main entry; an opt-in /default subpath ships the antd skin.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -102,13 +102,13 @@
|
|
|
102
102
|
"size-limit": "^11.2.0",
|
|
103
103
|
"typescript": "^5.8.3",
|
|
104
104
|
"vitest": "^3.2.4",
|
|
105
|
-
"@stapel/attributes-react": "^0.16.
|
|
105
|
+
"@stapel/attributes-react": "^0.16.2",
|
|
106
106
|
"@stapel/core": "^0.25.4",
|
|
107
107
|
"@stapel/currencies-react": "^0.4.0",
|
|
108
108
|
"@stapel/image": "^0.4.2",
|
|
109
109
|
"@stapel/showcase": "^0.3.0",
|
|
110
|
-
"@stapel/tokens
|
|
111
|
-
"@stapel/tokens": "^0.
|
|
110
|
+
"@stapel/tokens": "^0.8.0",
|
|
111
|
+
"@stapel/tokens-antd": "^0.17.3"
|
|
112
112
|
},
|
|
113
113
|
"engines": {
|
|
114
114
|
"node": ">=22"
|
package/src/default/favorite.tsx
CHANGED
|
@@ -58,9 +58,17 @@ export interface FavoriteHeartProps {
|
|
|
58
58
|
* tree either way — see {@link GateReasonPopover}.
|
|
59
59
|
*/
|
|
60
60
|
readonly blockedReason?: "text" | "popover";
|
|
61
|
-
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
61
|
+
/**
|
|
62
|
+
* The surface's sign-in door.
|
|
63
|
+
*
|
|
64
|
+
* Given, it is where a VISITOR'S PRESS GOES, in both volumes (D431) — the
|
|
65
|
+
* heart stops being a control that refuses and becomes the way in, `?next=`
|
|
66
|
+
* and all. The `"popover"` arm additionally renders it inside the
|
|
67
|
+
* disclosure, as it always did.
|
|
68
|
+
*
|
|
69
|
+
* Absent, nothing changes: the control stays blocked-but-alive and states
|
|
70
|
+
* its reason, which is all a surface with no sign-in route can offer.
|
|
71
|
+
*/
|
|
64
72
|
readonly signIn?: SignInCta;
|
|
65
73
|
readonly style?: CSSProperties;
|
|
66
74
|
}
|
|
@@ -82,6 +90,34 @@ export interface FavoriteHeartProps {
|
|
|
82
90
|
* was supposed to explain it. Measured on a phone: a signed-out visitor's
|
|
83
91
|
* heart produced no toast, no reason and no navigation, and on a touch device
|
|
84
92
|
* there is no hover to fall back on.
|
|
93
|
+
*
|
|
94
|
+
* ── And where there is a DOOR, it is not "blocked" at all (D431) ──────────
|
|
95
|
+
*
|
|
96
|
+
* Live-but-refusing was half the answer. On the deployed phone SERP the
|
|
97
|
+
* visitor's heart still carried `aria-disabled="true"`, a tap still produced
|
|
98
|
+
* nothing at all, and the only explanation was a hover disclosure a finger
|
|
99
|
+
* never triggers — the reason had been pooled into the pane's footnote, and
|
|
100
|
+
* neither this control nor `GatedControl` had ever been handed
|
|
101
|
+
* `onBlockedActivate`, so the gesture fell into the same hole `GatedControl`
|
|
102
|
+
* was written to close.
|
|
103
|
+
*
|
|
104
|
+
* The owner's ruling: **the control is never inert for a visitor.** Where the
|
|
105
|
+
* surface hands in a `signIn` door, a press GOES THROUGH IT, and the heart is
|
|
106
|
+
* therefore not announced as disabled either — a control that acts on press
|
|
107
|
+
* and calls itself unavailable is lying to exactly the people who most depend
|
|
108
|
+
* on the announcement. The reason stays where it was (registered with the
|
|
109
|
+
* pane's scope, wired by `aria-describedby`, hover disclosure and all on the
|
|
110
|
+
* surfaces that have one); it is now a HINT beside a live control rather than
|
|
111
|
+
* the only thing behind a dead one.
|
|
112
|
+
*
|
|
113
|
+
* Where the host hands in NO door there is nothing better for a press to do,
|
|
114
|
+
* and the control keeps the gated shape it has always had — blocked, alive,
|
|
115
|
+
* saying why.
|
|
116
|
+
*
|
|
117
|
+
* The `href` arm renders the heart as an anchor, deliberately: the door is a
|
|
118
|
+
* navigation (`/login?next=…`), and an anchor keeps the `next`, the middle
|
|
119
|
+
* click, and the person who is not running our JavaScript. `aria-pressed`
|
|
120
|
+
* comes off in that arm, because a link is not a toggle.
|
|
85
121
|
*/
|
|
86
122
|
export function FavoriteHeart(props: FavoriteHeartProps): ReactElement {
|
|
87
123
|
const t = useT();
|
|
@@ -103,7 +139,30 @@ export function FavoriteHeart(props: FavoriteHeartProps): ReactElement {
|
|
|
103
139
|
);
|
|
104
140
|
// The RESOLVED sentence, not the gate's key — `useActionGate` is the one
|
|
105
141
|
// place a blocked reason becomes words in this fleet.
|
|
106
|
-
const
|
|
142
|
+
const gate = useActionGate(favorite.gate);
|
|
143
|
+
const reason = gate.reason;
|
|
144
|
+
// D431. A blocked gate plus a door the surface handed in: the press is not
|
|
145
|
+
// refused, it is ROUTED — through the door, keeping whatever `?next=` the
|
|
146
|
+
// container put in the href. With no door there is nothing better for the
|
|
147
|
+
// press to do and the gated shape stands, exactly as before.
|
|
148
|
+
const door = props.signIn;
|
|
149
|
+
const opensDoor = gate.disabled && door !== undefined;
|
|
150
|
+
// Exactly one arm, never both — `SignInCta`'s own rule. The `href` arm
|
|
151
|
+
// makes antd render an anchor, which is the honest element for a door.
|
|
152
|
+
const doorPress =
|
|
153
|
+
!opensDoor || door === undefined
|
|
154
|
+
? {}
|
|
155
|
+
: door.href !== undefined
|
|
156
|
+
? { href: door.href }
|
|
157
|
+
: { onClick: door.onSignIn };
|
|
158
|
+
// The heart, as the visitor with a door meets it: live, not announced as
|
|
159
|
+
// unavailable, and not pretending to be a toggle when it is a link.
|
|
160
|
+
const asDoor = opensDoor
|
|
161
|
+
? {
|
|
162
|
+
...doorPress,
|
|
163
|
+
...(door?.href !== undefined ? {} : { "aria-pressed": favorite.favorited }),
|
|
164
|
+
}
|
|
165
|
+
: { "aria-pressed": favorite.favorited, onClick: favorite.toggle };
|
|
107
166
|
// A failed save, stated where the heart is. The rollback already put the
|
|
108
167
|
// icon back; this says why it went back.
|
|
109
168
|
const failure = (
|
|
@@ -124,15 +183,17 @@ export function FavoriteHeart(props: FavoriteHeartProps): ReactElement {
|
|
|
124
183
|
{(bind) => (
|
|
125
184
|
<Button
|
|
126
185
|
shape="circle"
|
|
127
|
-
|
|
186
|
+
// Announced unavailable ONLY where a press has nothing to do —
|
|
187
|
+
// see D431 in the header. With a door in hand it is a live
|
|
188
|
+
// control, and the disclosure stays as the hint beside it.
|
|
189
|
+
{...(opensDoor ? {} : { "aria-disabled": true })}
|
|
128
190
|
{...bind}
|
|
129
191
|
aria-label={label}
|
|
130
|
-
aria-pressed={favorite.favorited}
|
|
131
192
|
data-testid={props.testId}
|
|
132
193
|
data-favorited={String(favorite.favorited)}
|
|
133
194
|
data-analytics="none"
|
|
134
195
|
data-analytics-reason="business action — host app wraps with its own tracked()"
|
|
135
|
-
|
|
196
|
+
{...asDoor}
|
|
136
197
|
icon={icon}
|
|
137
198
|
{...(props.style !== undefined ? { style: props.style } : {})}
|
|
138
199
|
/>
|
|
@@ -144,6 +205,12 @@ export function FavoriteHeart(props: FavoriteHeartProps): ReactElement {
|
|
|
144
205
|
<GatedControl
|
|
145
206
|
gate={favorite.gate}
|
|
146
207
|
testId={`${props.testId}-gate`}
|
|
208
|
+
// With a door in hand the gate no longer REFUSES the person, it
|
|
209
|
+
// annotates the control: the reason is still registered with the pane's
|
|
210
|
+
// scope and still wired by `aria-describedby`, and the press — which is
|
|
211
|
+
// now the way in, not a hole — reaches the handler untouched. Without a
|
|
212
|
+
// door the gate keeps its default "live" refusal.
|
|
213
|
+
{...(opensDoor ? { whenBlocked: "annotate" as const } : {})}
|
|
147
214
|
{...(props.layout !== undefined ? { layout: props.layout } : {})}
|
|
148
215
|
{...(props.style !== undefined ? { style: props.style } : {})}
|
|
149
216
|
>
|
|
@@ -154,12 +221,11 @@ export function FavoriteHeart(props: FavoriteHeartProps): ReactElement {
|
|
|
154
221
|
// See `<ListingCard>`: the substrate's binding, spread whole.
|
|
155
222
|
{...bind}
|
|
156
223
|
aria-label={label}
|
|
157
|
-
aria-pressed={favorite.favorited}
|
|
158
224
|
data-testid={props.testId}
|
|
159
225
|
data-favorited={String(favorite.favorited)}
|
|
160
226
|
data-analytics="none"
|
|
161
227
|
data-analytics-reason="business action — host app wraps with its own tracked()"
|
|
162
|
-
|
|
228
|
+
{...asDoor}
|
|
163
229
|
icon={icon}
|
|
164
230
|
/>
|
|
165
231
|
{failure}
|