@jant/core 0.6.1 → 0.6.3
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/dist/app-BX2XKxq0.js +6 -0
- package/dist/{app-DYQdDMs8.js → app-CyysIxj_.js} +464 -240
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/client-BMPMuwvV.css +2 -0
- package/dist/client/_assets/client-CTrEFM5W.js +275 -0
- package/dist/client/_assets/{client-auth-CSNcTJwP.js → client-auth-LBSZxqNC.js} +24 -24
- package/dist/{env-C7e2Nlnt.js → env-CoSe-1y4.js} +1 -1
- package/dist/{export-Bbn86HmS.js → export-CzuQyg5h.js} +32 -19
- package/dist/{github-api-Bh0PH3zr.js → github-api-UD4u_7fa.js} +1 -1
- package/dist/{github-app-D0GvNnqp.js → github-app-DeX6Td1O.js} +1 -1
- package/dist/{github-sync-dXsiZa_e.js → github-sync-CerNYCAn.js} +3 -3
- package/dist/{github-sync-CBQPRZ8H.js → github-sync-Dbrb1DS5.js} +7 -4
- package/dist/index.js +5 -5
- package/dist/node.js +6 -6
- package/dist/{url-umUptr5z.js → url-XF0GbKGO.js} +22 -1
- package/package.json +1 -1
- package/src/__tests__/export-service.test.ts +127 -0
- package/src/client/__tests__/image-processor.test.ts +64 -0
- package/src/client/components/__tests__/jant-collection-directory.test.ts +0 -42
- package/src/client/components/__tests__/jant-media-lightbox.test.ts +79 -8
- package/src/client/components/collection-manager-types.ts +0 -2
- package/src/client/components/jant-collection-directory.ts +0 -23
- package/src/client/components/jant-compose-editor.ts +2 -2
- package/src/client/components/jant-media-lightbox.ts +33 -5
- package/src/client/image-processor.ts +89 -30
- package/src/client/media-scroll-hint.ts +62 -9
- package/src/i18n/coverage.generated.ts +2 -2
- package/src/i18n/locales/public/en.po +0 -12
- package/src/i18n/locales/public/zh-Hans.po +0 -12
- package/src/i18n/locales/public/zh-Hant.po +0 -12
- package/src/i18n/locales/settings/zh-Hans.po +24 -24
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +24 -24
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/lib/__tests__/structured-data.test.ts +87 -0
- package/src/lib/github-sync-site-config.ts +4 -2
- package/src/lib/post-display.ts +78 -1
- package/src/lib/render.tsx +28 -0
- package/src/lib/structured-data.ts +113 -0
- package/src/lib/url.ts +26 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +65 -0
- package/src/routes/api/internal/sites.ts +19 -0
- package/src/routes/pages/home.tsx +21 -1
- package/src/routes/pages/page.tsx +53 -2
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +30 -29
- package/src/services/export-theme/layouts/partials/media-gallery.html +16 -7
- package/src/services/export-theme/styles/main.css +4 -3
- package/src/services/export.ts +47 -17
- package/src/services/github-sync.ts +8 -2
- package/src/services/site-admin.ts +53 -1
- package/src/styles/site-media.css +70 -24
- package/src/styles/ui.css +30 -48
- package/src/ui/layouts/BaseLayout.tsx +110 -16
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +146 -0
- package/src/ui/pages/CollectionsPage.tsx +0 -22
- package/src/ui/shared/CollectionsManager.tsx +6 -41
- package/src/ui/shared/MediaGallery.tsx +50 -7
- package/src/ui/shared/__tests__/media-gallery.test.ts +31 -0
- package/dist/app-CMSW_AYG.js +0 -6
- package/dist/client/_assets/client-BRTh1ii1.js +0 -274
- package/dist/client/_assets/client-CO4b-RKd.css +0 -2
|
@@ -12,6 +12,7 @@ function createContext(
|
|
|
12
12
|
assetBasePath?: string;
|
|
13
13
|
sitePathPrefix?: string;
|
|
14
14
|
siteUrl?: string;
|
|
15
|
+
siteAvatarUrl?: string;
|
|
15
16
|
themeMode?: "auto" | "light" | "dark";
|
|
16
17
|
themeId?: string;
|
|
17
18
|
defaultThemeId?: string;
|
|
@@ -22,6 +23,7 @@ function createContext(
|
|
|
22
23
|
mainRssFeed,
|
|
23
24
|
sitePathPrefix: overrides?.sitePathPrefix ?? "",
|
|
24
25
|
siteUrl: overrides?.siteUrl ?? "https://example.com",
|
|
26
|
+
siteAvatarUrl: overrides?.siteAvatarUrl,
|
|
25
27
|
siteLanguage: "en",
|
|
26
28
|
noindex: false,
|
|
27
29
|
customCSS: "",
|
|
@@ -101,6 +103,27 @@ describe("BaseLayout", () => {
|
|
|
101
103
|
);
|
|
102
104
|
});
|
|
103
105
|
|
|
106
|
+
it("falls back to the bundled social image when appConfig.siteAvatarUrl is an empty string", async () => {
|
|
107
|
+
// resolve-config initializes siteAvatarUrl to "" (not undefined) when no
|
|
108
|
+
// avatar is configured. Regression test: the fallback chain must skip
|
|
109
|
+
// empty strings so og:image / twitter:image are never blank.
|
|
110
|
+
const { BaseLayout } = await loadBaseLayout();
|
|
111
|
+
const html = renderToString(
|
|
112
|
+
BaseLayout({
|
|
113
|
+
title: "Jant",
|
|
114
|
+
c: createContext("featured", { siteAvatarUrl: "" }),
|
|
115
|
+
children: "Test",
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
expect(html).toContain(
|
|
120
|
+
'meta property="og:image" content="https://example.com/_/brand/assets/jant-social-preview.png"',
|
|
121
|
+
);
|
|
122
|
+
expect(html).toContain(
|
|
123
|
+
'meta name="twitter:image" content="https://example.com/_/brand/assets/jant-social-preview.png"',
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
|
|
104
127
|
it("uses an explicit social image when provided", async () => {
|
|
105
128
|
const { BaseLayout } = await loadBaseLayout();
|
|
106
129
|
const html = renderToString(
|
|
@@ -119,6 +142,129 @@ describe("BaseLayout", () => {
|
|
|
119
142
|
);
|
|
120
143
|
});
|
|
121
144
|
|
|
145
|
+
it("defaults og:type to website without article timestamps", async () => {
|
|
146
|
+
const { BaseLayout } = await loadBaseLayout();
|
|
147
|
+
const html = renderToString(
|
|
148
|
+
BaseLayout({
|
|
149
|
+
title: "Jant",
|
|
150
|
+
children: "Test",
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
expect(html).toContain('meta property="og:type" content="website"');
|
|
155
|
+
expect(html).not.toContain("article:published_time");
|
|
156
|
+
expect(html).not.toContain("article:modified_time");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("renders article og:type with published and modified timestamps", async () => {
|
|
160
|
+
const { BaseLayout } = await loadBaseLayout();
|
|
161
|
+
const html = renderToString(
|
|
162
|
+
BaseLayout({
|
|
163
|
+
title: "A post",
|
|
164
|
+
ogType: "article",
|
|
165
|
+
articlePublishedTime: "2026-01-02T03:04:05.000Z",
|
|
166
|
+
articleModifiedTime: "2026-03-04T05:06:07.000Z",
|
|
167
|
+
children: "Test",
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
expect(html).toContain('meta property="og:type" content="article"');
|
|
172
|
+
expect(html).toContain(
|
|
173
|
+
'meta property="article:published_time" content="2026-01-02T03:04:05.000Z"',
|
|
174
|
+
);
|
|
175
|
+
expect(html).toContain(
|
|
176
|
+
'meta property="article:modified_time" content="2026-03-04T05:06:07.000Z"',
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("keeps the small twitter card and omits dimensions for the default social image", async () => {
|
|
181
|
+
const { BaseLayout } = await loadBaseLayout();
|
|
182
|
+
const html = renderToString(
|
|
183
|
+
BaseLayout({
|
|
184
|
+
title: "Jant",
|
|
185
|
+
children: "Test",
|
|
186
|
+
}),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
expect(html).toContain('meta name="twitter:card" content="summary"');
|
|
190
|
+
expect(html).not.toContain('content="summary_large_image"');
|
|
191
|
+
expect(html).not.toContain("og:image:width");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("renders dimensions, alt, and a large twitter card for a landscape post image", async () => {
|
|
195
|
+
const { BaseLayout } = await loadBaseLayout();
|
|
196
|
+
const html = renderToString(
|
|
197
|
+
BaseLayout({
|
|
198
|
+
title: "A post",
|
|
199
|
+
socialImageUrl: "https://cdn.example.com/photo.jpg",
|
|
200
|
+
socialImageWidth: 1600,
|
|
201
|
+
socialImageHeight: 900,
|
|
202
|
+
socialImageAlt: "A wide landscape photo",
|
|
203
|
+
children: "Test",
|
|
204
|
+
}),
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
expect(html).toContain('meta property="og:image:width" content="1600"');
|
|
208
|
+
expect(html).toContain('meta property="og:image:height" content="900"');
|
|
209
|
+
expect(html).toContain(
|
|
210
|
+
'meta property="og:image:alt" content="A wide landscape photo"',
|
|
211
|
+
);
|
|
212
|
+
expect(html).toContain(
|
|
213
|
+
'meta name="twitter:image:alt" content="A wide landscape photo"',
|
|
214
|
+
);
|
|
215
|
+
expect(html).toContain(
|
|
216
|
+
'meta name="twitter:card" content="summary_large_image"',
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("keeps the small twitter card for a portrait post image", async () => {
|
|
221
|
+
const { BaseLayout } = await loadBaseLayout();
|
|
222
|
+
const html = renderToString(
|
|
223
|
+
BaseLayout({
|
|
224
|
+
title: "A post",
|
|
225
|
+
socialImageUrl: "https://cdn.example.com/tall.jpg",
|
|
226
|
+
socialImageWidth: 600,
|
|
227
|
+
socialImageHeight: 900,
|
|
228
|
+
children: "Test",
|
|
229
|
+
}),
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
expect(html).toContain('meta name="twitter:card" content="summary"');
|
|
233
|
+
expect(html).not.toContain('content="summary_large_image"');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("renders JSON-LD structured data and escapes script-breaking characters", async () => {
|
|
237
|
+
const { BaseLayout } = await loadBaseLayout();
|
|
238
|
+
const html = renderToString(
|
|
239
|
+
BaseLayout({
|
|
240
|
+
title: "A post",
|
|
241
|
+
jsonLd: {
|
|
242
|
+
"@type": "BlogPosting",
|
|
243
|
+
headline: "Mind the </script> gap",
|
|
244
|
+
},
|
|
245
|
+
children: "Test",
|
|
246
|
+
}),
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
expect(html).toContain('<script type="application/ld+json">');
|
|
250
|
+
expect(html).toContain("\\u003c/script\\u003e");
|
|
251
|
+
expect(html).not.toContain("</script> gap");
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("skips JSON-LD when the page is noindex", async () => {
|
|
255
|
+
const { BaseLayout } = await loadBaseLayout();
|
|
256
|
+
const html = renderToString(
|
|
257
|
+
BaseLayout({
|
|
258
|
+
title: "A post",
|
|
259
|
+
noindex: true,
|
|
260
|
+
jsonLd: { "@type": "WebSite" },
|
|
261
|
+
children: "Test",
|
|
262
|
+
}),
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
expect(html).not.toContain("application/ld+json");
|
|
266
|
+
});
|
|
267
|
+
|
|
122
268
|
it("exposes the main and alternate feed links without duplicating featured", async () => {
|
|
123
269
|
const { BaseLayout } = await loadBaseLayout();
|
|
124
270
|
const html = renderToString(
|
|
@@ -11,37 +11,18 @@ import type { CollectionsPageProps } from "../../types.js";
|
|
|
11
11
|
import { CollectionDirectory } from "../shared/CollectionDirectory.js";
|
|
12
12
|
import { CollectionsManager } from "../shared/CollectionsManager.js";
|
|
13
13
|
|
|
14
|
-
const countCollections = (items: CollectionsPageProps["items"]) =>
|
|
15
|
-
items.filter((item) => item.type === "collection" && item.collection).length;
|
|
16
|
-
|
|
17
14
|
export const CollectionsPage: FC<CollectionsPageProps> = ({
|
|
18
15
|
items,
|
|
19
16
|
isAuthenticated,
|
|
20
17
|
sitePathPrefix = "",
|
|
21
18
|
}) => {
|
|
22
19
|
const { i18n } = useLingui();
|
|
23
|
-
const collectionCount = countCollections(items);
|
|
24
20
|
const emptyMessage = i18n._(
|
|
25
21
|
msg({
|
|
26
22
|
message: "No collections yet. Start one to organize posts by topic.",
|
|
27
23
|
comment: "@context: Empty state message on collections page",
|
|
28
24
|
}),
|
|
29
25
|
);
|
|
30
|
-
const collectionCountLabel = `${collectionCount} ${
|
|
31
|
-
collectionCount === 1
|
|
32
|
-
? i18n._(
|
|
33
|
-
msg({
|
|
34
|
-
message: "collection",
|
|
35
|
-
comment: "@context: Singular collection count label",
|
|
36
|
-
}),
|
|
37
|
-
)
|
|
38
|
-
: i18n._(
|
|
39
|
-
msg({
|
|
40
|
-
message: "collections",
|
|
41
|
-
comment: "@context: Plural collection count label",
|
|
42
|
-
}),
|
|
43
|
-
)
|
|
44
|
-
}`;
|
|
45
26
|
|
|
46
27
|
if (isAuthenticated) {
|
|
47
28
|
return (
|
|
@@ -66,9 +47,6 @@ export const CollectionsPage: FC<CollectionsPageProps> = ({
|
|
|
66
47
|
)}
|
|
67
48
|
</h1>
|
|
68
49
|
</div>
|
|
69
|
-
<div class="page-intro-meta-row">
|
|
70
|
-
<p class="page-intro-meta">{collectionCountLabel}</p>
|
|
71
|
-
</div>
|
|
72
50
|
</div>
|
|
73
51
|
</header>
|
|
74
52
|
|
|
@@ -13,9 +13,6 @@ import { getCollectionMutationLabels } from "./collection-management-labels.js";
|
|
|
13
13
|
const escapeJson = (data: unknown) =>
|
|
14
14
|
JSON.stringify(data).replace(/</g, "\\u003c");
|
|
15
15
|
|
|
16
|
-
const countCollections = (items: CollectionDirectoryItem[]) =>
|
|
17
|
-
items.filter((item) => item.type === "collection" && item.collection).length;
|
|
18
|
-
|
|
19
16
|
export interface CollectionsManagerProps {
|
|
20
17
|
items: CollectionDirectoryItem[];
|
|
21
18
|
sitePathPrefix?: string;
|
|
@@ -34,22 +31,6 @@ export const CollectionsManager: FC<CollectionsManagerProps> = ({
|
|
|
34
31
|
`${getNewCollectionPath()}?returnTo=${encodeURIComponent(collectionsHref)}`,
|
|
35
32
|
sitePathPrefix,
|
|
36
33
|
);
|
|
37
|
-
const collectionCount = countCollections(items);
|
|
38
|
-
const collectionCountLabel = `${collectionCount} ${
|
|
39
|
-
collectionCount === 1
|
|
40
|
-
? i18n._(
|
|
41
|
-
msg({
|
|
42
|
-
message: "collection",
|
|
43
|
-
comment: "@context: Singular collection count label",
|
|
44
|
-
}),
|
|
45
|
-
)
|
|
46
|
-
: i18n._(
|
|
47
|
-
msg({
|
|
48
|
-
message: "collections",
|
|
49
|
-
comment: "@context: Plural collection count label",
|
|
50
|
-
}),
|
|
51
|
-
)
|
|
52
|
-
}`;
|
|
53
34
|
const mutationLabels = getCollectionMutationLabels(i18n);
|
|
54
35
|
|
|
55
36
|
const labels = {
|
|
@@ -59,18 +40,6 @@ export const CollectionsManager: FC<CollectionsManagerProps> = ({
|
|
|
59
40
|
comment: "@context: Collections page heading",
|
|
60
41
|
}),
|
|
61
42
|
),
|
|
62
|
-
collectionSingular: i18n._(
|
|
63
|
-
msg({
|
|
64
|
-
message: "collection",
|
|
65
|
-
comment: "@context: Singular collection count label",
|
|
66
|
-
}),
|
|
67
|
-
),
|
|
68
|
-
collectionPlural: i18n._(
|
|
69
|
-
msg({
|
|
70
|
-
message: "collections",
|
|
71
|
-
comment: "@context: Plural collection count label",
|
|
72
|
-
}),
|
|
73
|
-
),
|
|
74
43
|
organize: i18n._(
|
|
75
44
|
msg({
|
|
76
45
|
message: "Organize",
|
|
@@ -164,11 +133,6 @@ export const CollectionsManager: FC<CollectionsManagerProps> = ({
|
|
|
164
133
|
<div class="collections-page-heading page-intro">
|
|
165
134
|
<div class="page-intro-title-row">
|
|
166
135
|
<h1 class="page-intro-title">{labels.collectionsTitle}</h1>
|
|
167
|
-
</div>
|
|
168
|
-
<div class="page-intro-meta-row">
|
|
169
|
-
<p class="page-intro-meta" data-collections-count>
|
|
170
|
-
{collectionCountLabel}
|
|
171
|
-
</p>
|
|
172
136
|
<div class="collections-page-actions">
|
|
173
137
|
<div
|
|
174
138
|
class="collections-page-action-group"
|
|
@@ -202,14 +166,15 @@ export const CollectionsManager: FC<CollectionsManagerProps> = ({
|
|
|
202
166
|
>
|
|
203
167
|
<svg
|
|
204
168
|
xmlns="http://www.w3.org/2000/svg"
|
|
205
|
-
width="
|
|
206
|
-
height="
|
|
169
|
+
width="18"
|
|
170
|
+
height="18"
|
|
207
171
|
viewBox="0 0 24 24"
|
|
208
172
|
fill="none"
|
|
209
173
|
stroke="currentColor"
|
|
210
174
|
stroke-width="2"
|
|
211
175
|
stroke-linecap="round"
|
|
212
176
|
stroke-linejoin="round"
|
|
177
|
+
aria-hidden="true"
|
|
213
178
|
>
|
|
214
179
|
<path d="M12 5v14" />
|
|
215
180
|
<path d="M5 12h14" />
|
|
@@ -218,7 +183,7 @@ export const CollectionsManager: FC<CollectionsManagerProps> = ({
|
|
|
218
183
|
<div class="relative">
|
|
219
184
|
<button
|
|
220
185
|
type="button"
|
|
221
|
-
class="collections-page-toolbar-button
|
|
186
|
+
class="collections-page-toolbar-button"
|
|
222
187
|
aria-label={labels.moreActions}
|
|
223
188
|
aria-expanded="false"
|
|
224
189
|
aria-haspopup="menu"
|
|
@@ -227,8 +192,8 @@ export const CollectionsManager: FC<CollectionsManagerProps> = ({
|
|
|
227
192
|
>
|
|
228
193
|
<svg
|
|
229
194
|
xmlns="http://www.w3.org/2000/svg"
|
|
230
|
-
width="
|
|
231
|
-
height="
|
|
195
|
+
width="18"
|
|
196
|
+
height="18"
|
|
232
197
|
viewBox="0 0 24 24"
|
|
233
198
|
fill="currentColor"
|
|
234
199
|
>
|
|
@@ -289,12 +289,15 @@ export const MediaGallery: FC<MediaGalleryProps> = ({
|
|
|
289
289
|
? JSON.stringify(lightboxItems)
|
|
290
290
|
: undefined
|
|
291
291
|
}
|
|
292
|
-
class={`flex gap-2 ${singleVisual ? "" : "overflow-x-auto scroll-smooth
|
|
292
|
+
class={`flex gap-2 ${singleVisual ? "" : "overflow-x-auto scroll-smooth"}`}
|
|
293
293
|
style={
|
|
294
294
|
singleVisual
|
|
295
295
|
? undefined
|
|
296
296
|
: "scrollbar-width: none; -ms-overflow-style: none;"
|
|
297
297
|
}
|
|
298
|
+
tabindex={singleVisual ? undefined : 0}
|
|
299
|
+
role={singleVisual ? undefined : "group"}
|
|
300
|
+
aria-label={singleVisual ? undefined : "Media gallery"}
|
|
298
301
|
>
|
|
299
302
|
{galleryItems.map((item) => {
|
|
300
303
|
if (item._kind === "image") {
|
|
@@ -329,7 +332,7 @@ export const MediaGallery: FC<MediaGalleryProps> = ({
|
|
|
329
332
|
key={item.id}
|
|
330
333
|
href={item.url}
|
|
331
334
|
data-lightbox-index={item._lbIdx}
|
|
332
|
-
class={`${singleVisual ? "" : "shrink-0
|
|
335
|
+
class={`${singleVisual ? "" : "shrink-0"} media-visual-frame`}
|
|
333
336
|
style={{
|
|
334
337
|
...(singleVisual
|
|
335
338
|
? {
|
|
@@ -391,7 +394,7 @@ export const MediaGallery: FC<MediaGalleryProps> = ({
|
|
|
391
394
|
return (
|
|
392
395
|
<div
|
|
393
396
|
key={item.id}
|
|
394
|
-
class={`${singleVisual ? "" : "shrink-0
|
|
397
|
+
class={`${singleVisual ? "" : "shrink-0"} media-video-wrap media-video-wrap-short`}
|
|
395
398
|
style={
|
|
396
399
|
singleVisual
|
|
397
400
|
? {
|
|
@@ -464,7 +467,7 @@ export const MediaGallery: FC<MediaGalleryProps> = ({
|
|
|
464
467
|
key={item.id}
|
|
465
468
|
href={item.url}
|
|
466
469
|
data-lightbox-index={item._lbIdx}
|
|
467
|
-
class={`${singleVisual ? "" : "shrink-0
|
|
470
|
+
class={`${singleVisual ? "" : "shrink-0"} media-video-wrap media-visual-frame`}
|
|
468
471
|
style={
|
|
469
472
|
singleVisual
|
|
470
473
|
? {
|
|
@@ -502,7 +505,7 @@ export const MediaGallery: FC<MediaGalleryProps> = ({
|
|
|
502
505
|
return (
|
|
503
506
|
<div
|
|
504
507
|
key={item.id}
|
|
505
|
-
class={`media-gallery-card media-audio-card shrink-0
|
|
508
|
+
class={`media-gallery-card media-audio-card shrink-0${item.waveform ? " has-waveform" : ""}`}
|
|
506
509
|
style={{
|
|
507
510
|
width: `${docCardWidth}px`,
|
|
508
511
|
height: `${rowHeight}px`,
|
|
@@ -593,7 +596,7 @@ export const MediaGallery: FC<MediaGalleryProps> = ({
|
|
|
593
596
|
href={item.url}
|
|
594
597
|
target="_blank"
|
|
595
598
|
rel="noopener noreferrer"
|
|
596
|
-
class="media-gallery-card shrink-0
|
|
599
|
+
class="media-gallery-card shrink-0"
|
|
597
600
|
style={{
|
|
598
601
|
width: `${docCardWidth}px`,
|
|
599
602
|
height: `${rowHeight}px`,
|
|
@@ -627,7 +630,7 @@ export const MediaGallery: FC<MediaGalleryProps> = ({
|
|
|
627
630
|
? `${postPermalink}/text/${item.id}`
|
|
628
631
|
: undefined
|
|
629
632
|
}
|
|
630
|
-
class="media-gallery-card shrink-0
|
|
633
|
+
class="media-gallery-card shrink-0"
|
|
631
634
|
style={{
|
|
632
635
|
width: `${docCardWidth}px`,
|
|
633
636
|
height: `${rowHeight}px`,
|
|
@@ -650,6 +653,46 @@ export const MediaGallery: FC<MediaGalleryProps> = ({
|
|
|
650
653
|
);
|
|
651
654
|
})}
|
|
652
655
|
</div>
|
|
656
|
+
{!singleVisual && (
|
|
657
|
+
<>
|
|
658
|
+
<button
|
|
659
|
+
type="button"
|
|
660
|
+
class="media-gallery-nav media-gallery-nav-prev"
|
|
661
|
+
tabindex={-1}
|
|
662
|
+
aria-label="Scroll to previous media"
|
|
663
|
+
>
|
|
664
|
+
<svg
|
|
665
|
+
viewBox="0 0 24 24"
|
|
666
|
+
fill="none"
|
|
667
|
+
stroke="currentColor"
|
|
668
|
+
stroke-width="2.5"
|
|
669
|
+
stroke-linecap="round"
|
|
670
|
+
stroke-linejoin="round"
|
|
671
|
+
aria-hidden="true"
|
|
672
|
+
>
|
|
673
|
+
<path d="M15 18l-6-6 6-6" />
|
|
674
|
+
</svg>
|
|
675
|
+
</button>
|
|
676
|
+
<button
|
|
677
|
+
type="button"
|
|
678
|
+
class="media-gallery-nav media-gallery-nav-next"
|
|
679
|
+
tabindex={-1}
|
|
680
|
+
aria-label="Scroll to next media"
|
|
681
|
+
>
|
|
682
|
+
<svg
|
|
683
|
+
viewBox="0 0 24 24"
|
|
684
|
+
fill="none"
|
|
685
|
+
stroke="currentColor"
|
|
686
|
+
stroke-width="2.5"
|
|
687
|
+
stroke-linecap="round"
|
|
688
|
+
stroke-linejoin="round"
|
|
689
|
+
aria-hidden="true"
|
|
690
|
+
>
|
|
691
|
+
<path d="M9 18l6-6-6-6" />
|
|
692
|
+
</svg>
|
|
693
|
+
</button>
|
|
694
|
+
</>
|
|
695
|
+
)}
|
|
653
696
|
</div>
|
|
654
697
|
)}
|
|
655
698
|
</>
|
|
@@ -108,4 +108,35 @@ describe("MediaGallery", () => {
|
|
|
108
108
|
expect(html).not.toContain("media-video-play-overlay");
|
|
109
109
|
expect(html).not.toContain("media-short-video-progress");
|
|
110
110
|
});
|
|
111
|
+
|
|
112
|
+
it("adds scroll arrows and a focusable strip for multi-item galleries", () => {
|
|
113
|
+
const html = renderToString(
|
|
114
|
+
MediaGallery({
|
|
115
|
+
attachments: [
|
|
116
|
+
createMediaView({ id: "m-1", width: 1600, height: 900 }),
|
|
117
|
+
createMediaView({ id: "m-2", width: 1600, height: 900 }),
|
|
118
|
+
createMediaView({ id: "m-3", width: 1600, height: 900 }),
|
|
119
|
+
],
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
expect(html).toContain("media-gallery-nav-prev");
|
|
124
|
+
expect(html).toContain("media-gallery-nav-next");
|
|
125
|
+
expect(html).toContain('aria-label="Scroll to previous media"');
|
|
126
|
+
expect(html).toContain('aria-label="Scroll to next media"');
|
|
127
|
+
// The strip is a keyboard tab stop so Arrow keys can scroll it.
|
|
128
|
+
expect(html).toContain('tabindex="0"');
|
|
129
|
+
expect(html).toContain('role="group"');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("omits scroll arrows and the tab stop for a single visual", () => {
|
|
133
|
+
const html = renderToString(
|
|
134
|
+
MediaGallery({
|
|
135
|
+
attachments: [createMediaView({ width: 1600, height: 900 })],
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
expect(html).not.toContain("media-gallery-nav");
|
|
140
|
+
expect(html).not.toContain('tabindex="0"');
|
|
141
|
+
});
|
|
111
142
|
});
|