@bearmenu/ui 0.8.19 → 0.8.20
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.
|
@@ -52,7 +52,9 @@ type PosterStackPoster = {
|
|
|
52
52
|
fallback?: React.ReactNode;
|
|
53
53
|
};
|
|
54
54
|
type PosterStackProps = {
|
|
55
|
-
/**
|
|
55
|
+
/** Candidates in order. The first three whose image loads are shown, so
|
|
56
|
+
* pass more than three: a poster whose asset fails yields its slot to
|
|
57
|
+
* the next candidate. */
|
|
56
58
|
posters: PosterStackPoster[];
|
|
57
59
|
/** Defaults to a plain `<a>`. Pass `next/link`, or an anchor that opens the app's preview. */
|
|
58
60
|
linkComponent?: React.ComponentType<PosterStackLinkProps>;
|
|
@@ -65,7 +65,7 @@ function PosterStack({
|
|
|
65
65
|
observer.observe(container);
|
|
66
66
|
return () => observer.disconnect();
|
|
67
67
|
}, []);
|
|
68
|
-
const visiblePosters = posters.slice(0, POSTER_SLOTS.length);
|
|
68
|
+
const visiblePosters = posters.map((poster, index) => ({ poster, index })).filter(({ index }) => !failedIndices.has(index)).slice(0, POSTER_SLOTS.length);
|
|
69
69
|
return /* @__PURE__ */ jsxs(
|
|
70
70
|
"div",
|
|
71
71
|
{
|
|
@@ -73,10 +73,10 @@ function PosterStack({
|
|
|
73
73
|
className: cn("relative h-[196px] md:h-[400px] md:w-auto md:mx-0", PHONE_STAGE_CLASS, className),
|
|
74
74
|
children: [
|
|
75
75
|
/* @__PURE__ */ jsx("style", { children: POSTER_STACK_ANIMATION_STYLE }),
|
|
76
|
-
visiblePosters.map((poster, index) => {
|
|
77
|
-
var _a;
|
|
76
|
+
visiblePosters.map(({ poster, index: candidateIndex }, index) => {
|
|
77
|
+
var _a, _b, _c;
|
|
78
78
|
const slot = POSTER_SLOTS[index];
|
|
79
|
-
const showPoster = !!poster.src
|
|
79
|
+
const showPoster = !!poster.src;
|
|
80
80
|
const face = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
81
81
|
showPoster ? (
|
|
82
82
|
// eslint-disable-next-line @next/next/no-img-element
|
|
@@ -91,7 +91,7 @@ function PosterStack({
|
|
|
91
91
|
"h-full w-full object-cover",
|
|
92
92
|
poster.href && "transition-transform duration-300 group-hover/poster:scale-[1.03] motion-reduce:transition-none"
|
|
93
93
|
),
|
|
94
|
-
onError: () => setFailedIndices((current) => new Set(current).add(
|
|
94
|
+
onError: () => setFailedIndices((current) => new Set(current).add(candidateIndex))
|
|
95
95
|
}
|
|
96
96
|
)
|
|
97
97
|
) : (_a = poster.fallback) != null ? _a : /* @__PURE__ */ jsx(EventPosterFallback, {}),
|
|
@@ -132,7 +132,7 @@ function PosterStack({
|
|
|
132
132
|
}
|
|
133
133
|
) : /* @__PURE__ */ jsx("div", { className: "relative h-full w-full", children: face })
|
|
134
134
|
},
|
|
135
|
-
poster.
|
|
135
|
+
(_c = (_b = poster.href) != null ? _b : poster.src) != null ? _c : candidateIndex
|
|
136
136
|
);
|
|
137
137
|
})
|
|
138
138
|
]
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
3
3
|
import { EventTonightStrip } from "./event-tonight-strip";
|
|
4
4
|
import { PosterStack } from "./poster-stack";
|
|
5
5
|
|
|
@@ -46,4 +46,24 @@ describe("PosterStack", () => {
|
|
|
46
46
|
expect(screen.getAllByRole("link")).toHaveLength(1);
|
|
47
47
|
expect(screen.getByAltText("B poster")).toBeInTheDocument();
|
|
48
48
|
});
|
|
49
|
+
|
|
50
|
+
it("gives a failed poster's slot to the next candidate", () => {
|
|
51
|
+
window.matchMedia = () => ({ matches: true }) as MediaQueryList;
|
|
52
|
+
const poster = (id: string) => ({ src: `/${id}.jpg`, alt: id, caption: id, href: `/e/${id}` });
|
|
53
|
+
render(<PosterStack posters={[poster("a"), poster("b"), poster("c"), poster("d")]} />);
|
|
54
|
+
expect(screen.getAllByRole("link").map((l) => l.getAttribute("href"))).toEqual([
|
|
55
|
+
"/e/a",
|
|
56
|
+
"/e/b",
|
|
57
|
+
"/e/c",
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
// The middle poster's asset dies at load time.
|
|
61
|
+
fireEvent.error(screen.getByRole("link", { name: "b" }).querySelector("img")!);
|
|
62
|
+
|
|
63
|
+
expect(screen.getAllByRole("link").map((l) => l.getAttribute("href"))).toEqual([
|
|
64
|
+
"/e/a",
|
|
65
|
+
"/e/c",
|
|
66
|
+
"/e/d",
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
49
69
|
});
|
|
@@ -63,7 +63,9 @@ export type PosterStackPoster = {
|
|
|
63
63
|
};
|
|
64
64
|
|
|
65
65
|
export type PosterStackProps = {
|
|
66
|
-
/**
|
|
66
|
+
/** Candidates in order. The first three whose image loads are shown, so
|
|
67
|
+
* pass more than three: a poster whose asset fails yields its slot to
|
|
68
|
+
* the next candidate. */
|
|
67
69
|
posters: PosterStackPoster[];
|
|
68
70
|
/** Defaults to a plain `<a>`. Pass `next/link`, or an anchor that opens the app's preview. */
|
|
69
71
|
linkComponent?: React.ComponentType<PosterStackLinkProps>;
|
|
@@ -154,7 +156,14 @@ export function PosterStack({
|
|
|
154
156
|
return () => observer.disconnect();
|
|
155
157
|
}, []);
|
|
156
158
|
|
|
157
|
-
|
|
159
|
+
// The first three candidates whose image has not failed. A poster whose
|
|
160
|
+
// asset 404s or 502s at load time gives its slot to the next candidate —
|
|
161
|
+
// the stack is the hub's first screen, and a fallback glyph in the middle
|
|
162
|
+
// of three posters is a hole in it. Pass more candidates than slots.
|
|
163
|
+
const visiblePosters = posters
|
|
164
|
+
.map((poster, index) => ({ poster, index }))
|
|
165
|
+
.filter(({ index }) => !failedIndices.has(index))
|
|
166
|
+
.slice(0, POSTER_SLOTS.length);
|
|
158
167
|
|
|
159
168
|
return (
|
|
160
169
|
<div
|
|
@@ -163,9 +172,9 @@ export function PosterStack({
|
|
|
163
172
|
>
|
|
164
173
|
<style>{POSTER_STACK_ANIMATION_STYLE}</style>
|
|
165
174
|
|
|
166
|
-
{visiblePosters.map((poster, index) => {
|
|
175
|
+
{visiblePosters.map(({ poster, index: candidateIndex }, index) => {
|
|
167
176
|
const slot = POSTER_SLOTS[index];
|
|
168
|
-
const showPoster = !!poster.src
|
|
177
|
+
const showPoster = !!poster.src;
|
|
169
178
|
const face = (
|
|
170
179
|
<>
|
|
171
180
|
{showPoster ? (
|
|
@@ -180,7 +189,9 @@ export function PosterStack({
|
|
|
180
189
|
poster.href &&
|
|
181
190
|
"transition-transform duration-300 group-hover/poster:scale-[1.03] motion-reduce:transition-none"
|
|
182
191
|
)}
|
|
183
|
-
onError={() =>
|
|
192
|
+
onError={() =>
|
|
193
|
+
setFailedIndices((current) => new Set(current).add(candidateIndex))
|
|
194
|
+
}
|
|
184
195
|
/>
|
|
185
196
|
) : (
|
|
186
197
|
(poster.fallback ?? <EventPosterFallback />)
|
|
@@ -197,7 +208,9 @@ export function PosterStack({
|
|
|
197
208
|
|
|
198
209
|
return (
|
|
199
210
|
<div
|
|
200
|
-
|
|
211
|
+
// Keyed by candidate, not slot: a replacement mounts fresh in the
|
|
212
|
+
// slot the failed poster vacated instead of inheriting its state.
|
|
213
|
+
key={poster.href ?? poster.src ?? candidateIndex}
|
|
201
214
|
ref={(node) => {
|
|
202
215
|
posterRefs.current[index] = node;
|
|
203
216
|
}}
|