@riebeckite/plugin-lightbox 0.0.13 → 0.0.15

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/README.md CHANGED
@@ -36,6 +36,8 @@ the app calls on page initialization.
36
36
  - Adds `rr-lightbox-image` to the image
37
37
  - Skips images that have `data-lightbox-ignore="true"`, and images already
38
38
  inside an `<a>`, `<button>`, existing trigger, or the dialog
39
+ - Uses `expandLabel` (default `"Expand image"`) for the trigger and dialog
40
+ accessible labels
39
41
 
40
42
  ### Client (`initLightbox`)
41
43
 
@@ -54,9 +56,11 @@ the app calls on page initialization.
54
56
  | Option | Type | Default | Description |
55
57
  | ------ | ---- | ------- | ----------- |
56
58
  | `selectorClass` | `string` | `"rr-lightbox-trigger"` | Trigger class (build and client) |
59
+ | `expandLabel` | `string` | `"Expand image"` | Accessible label for triggers and the dialog |
60
+ | `closeLabel` | `string` | `"Close"` | Accessible label for the dialog close button |
57
61
  | `autoWrapImages` | `boolean` | `true` | Client-only: wrap unhandled images on init |
58
62
 
59
- `LightboxOptions` = `{ selectorClass? }` (build),
63
+ `LightboxOptions` = `{ selectorClass?, expandLabel?, closeLabel? }` (build),
60
64
  `LightboxInitOptions` = `LightboxOptions & { autoWrapImages? }` (client).
61
65
 
62
66
  ## Exports
@@ -65,8 +69,11 @@ the app calls on page initialization.
65
69
  - `rehypeLightbox(options?)` — rehype transform
66
70
  - `initLightbox(root?, options?)` — client initializer, returns a cleanup
67
71
  function
72
+ - `initLightboxFromOptions(options?)` — option-first browser entry the plugin's
73
+ client script calls; wraps `initLightbox`
68
74
  - Types: `LightboxOptions`, `LightboxInitOptions`
69
75
 
70
76
  ## See also
71
77
 
72
- - [Plugin guide](../../../docs/en/reference/plugin-api.md)
78
+ - [Plugin guide](../../../docs/en/docs/reference/plugin-api.md)
79
+
package/README_ja.md CHANGED
@@ -30,6 +30,8 @@ export default defineConfig({
30
30
  | オプション | 型 | 既定値 | 内容 |
31
31
  | --- | --- | --- | --- |
32
32
  | `selectorClass` | `string` | `"rr-lightbox-trigger"` | トリガーに使う CSS クラス |
33
+ | `expandLabel` | `string` | `"Expand image"` | トリガーとダイアログのアクセシブルラベル |
34
+ | `closeLabel` | `string` | `"Close"` | 閉じるボタンのアクセシブルラベル |
33
35
  | `autoWrapImages` | `boolean` | `true` | 初期化時に未処理の画像も囲むか。クライアント側だけの設定 |
34
36
 
35
37
  `initLightbox()` はイベントリスナー、ダイアログ、追加したトリガーを解除する cleanup 関数を返します。
@@ -39,8 +41,10 @@ export default defineConfig({
39
41
  - `lightboxPlugin(options?)` — プラグインファクトリ
40
42
  - `rehypeLightbox(options?)` — Rehype 変換
41
43
  - `initLightbox(root?, options?)` — ブラウザ初期化関数
44
+ - `initLightboxFromOptions(options?)` — プラグインのクライアントスクリプトが呼ぶ、オプションを先に取る初期化関数
42
45
  - `LightboxOptions`、`LightboxInitOptions` — 型
43
46
 
44
47
  ## 関連資料
45
48
 
46
- - [プラグインシステム](../../../docs/ja/reference/plugin-api.md)
49
+ - [プラグインシステム](../../../docs/ja/docs/reference/plugin-api.md)
50
+
package/dist/client.d.ts CHANGED
@@ -1 +1,3 @@
1
+ import type { LightboxInitOptions } from "./src/types.js";
1
2
  export { initLightbox } from "./src/init.js";
3
+ export declare function initLightboxFromOptions(options?: LightboxInitOptions): () => void;
package/dist/client.js CHANGED
@@ -1,10 +1,11 @@
1
1
  // src/dialog.ts
2
- function createDialog() {
2
+ function createDialog(options) {
3
3
  const element = document.createElement("div");
4
4
  element.className = "rr-lightbox";
5
5
  element.setAttribute("role", "dialog");
6
6
  element.setAttribute("aria-modal", "true");
7
- element.setAttribute("aria-label", "画像の拡大表示");
7
+ element.setAttribute("aria-label", options.expandLabel);
8
+ element.tabIndex = -1;
8
9
  element.setAttribute("hidden", "");
9
10
  const frame = document.createElement("figure");
10
11
  frame.className = "rr-lightbox__frame";
@@ -16,7 +17,7 @@ function createDialog() {
16
17
  const closeButton = document.createElement("button");
17
18
  closeButton.className = "rr-lightbox__close";
18
19
  closeButton.type = "button";
19
- closeButton.setAttribute("aria-label", "閉じる");
20
+ closeButton.setAttribute("aria-label", options.closeLabel);
20
21
  closeButton.textContent = "×";
21
22
  frame.appendChild(image);
22
23
  frame.appendChild(caption);
@@ -27,11 +28,15 @@ function createDialog() {
27
28
 
28
29
  // src/types.ts
29
30
  var DEFAULT_TRIGGER_CLASS = "rr-lightbox-trigger";
31
+ var DEFAULT_EXPAND_LABEL = "Expand image";
32
+ var DEFAULT_CLOSE_LABEL = "Close";
30
33
 
31
34
  // src/init.ts
32
35
  function initLightbox(root = document, options = {}) {
33
36
  const triggerClass = options.selectorClass ?? DEFAULT_TRIGGER_CLASS;
34
- const wrappedTriggers = options.autoWrapImages === false ? [] : wrapImages(root, triggerClass);
37
+ const expandLabel = options.expandLabel ?? DEFAULT_EXPAND_LABEL;
38
+ const closeLabel = options.closeLabel ?? DEFAULT_CLOSE_LABEL;
39
+ const wrappedTriggers = options.autoWrapImages === false ? [] : wrapImages(root, triggerClass, expandLabel);
35
40
  const triggers = Array.from(
36
41
  root.querySelectorAll(`.${triggerClass}`)
37
42
  );
@@ -41,7 +46,7 @@ function initLightbox(root = document, options = {}) {
41
46
  };
42
47
  }
43
48
  let previouslyFocused = null;
44
- const dialog = createDialog();
49
+ const dialog = createDialog({ expandLabel, closeLabel });
45
50
  document.body.appendChild(dialog.element);
46
51
  const open = (trigger) => {
47
52
  const src = trigger.dataset.lightboxSrc ?? trigger.href;
@@ -73,7 +78,12 @@ function initLightbox(root = document, options = {}) {
73
78
  }
74
79
  };
75
80
  const handleKeyDown = (event) => {
76
- if (dialog.element.hidden || event.key !== "Escape") return;
81
+ if (dialog.element.hidden) return;
82
+ if (event.key === "Tab") {
83
+ trapFocus(event, dialog.element);
84
+ return;
85
+ }
86
+ if (event.key !== "Escape") return;
77
87
  event.preventDefault();
78
88
  close();
79
89
  };
@@ -93,7 +103,34 @@ function initLightbox(root = document, options = {}) {
93
103
  for (const trigger of wrappedTriggers) unwrapImage(trigger);
94
104
  };
95
105
  }
96
- function wrapImages(root, triggerClass) {
106
+ function trapFocus(event, container) {
107
+ const focusableElements = getFocusableElements(container);
108
+ if (focusableElements.length === 0) {
109
+ event.preventDefault();
110
+ container.focus();
111
+ return;
112
+ }
113
+ const first = focusableElements[0];
114
+ const last = focusableElements[focusableElements.length - 1];
115
+ const active = document.activeElement;
116
+ if (event.shiftKey && active === first) {
117
+ event.preventDefault();
118
+ last.focus();
119
+ return;
120
+ }
121
+ if (!event.shiftKey && active === last) {
122
+ event.preventDefault();
123
+ first.focus();
124
+ }
125
+ }
126
+ function getFocusableElements(container) {
127
+ return Array.from(
128
+ container.querySelectorAll(
129
+ 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
130
+ )
131
+ ).filter((element) => !element.hasAttribute("disabled"));
132
+ }
133
+ function wrapImages(root, triggerClass, expandLabel) {
97
134
  const images = Array.from(
98
135
  root.querySelectorAll("img[src]")
99
136
  );
@@ -112,7 +149,7 @@ function wrapImages(root, triggerClass) {
112
149
  trigger.dataset.lightboxAlt = image.alt;
113
150
  trigger.setAttribute(
114
151
  "aria-label",
115
- image.alt ? `画像を拡大表示: ${image.alt}` : "画像を拡大表示"
152
+ image.alt ? `${expandLabel}: ${image.alt}` : expandLabel
116
153
  );
117
154
  image.parentNode?.insertBefore(trigger, image);
118
155
  trigger.appendChild(image);
@@ -127,6 +164,12 @@ function unwrapImage(trigger) {
127
164
  trigger.parentNode.insertBefore(image, trigger);
128
165
  trigger.remove();
129
166
  }
167
+
168
+ // client.ts
169
+ function initLightboxFromOptions(options = {}) {
170
+ return initLightbox(document, options);
171
+ }
130
172
  export {
131
- initLightbox
173
+ initLightbox,
174
+ initLightboxFromOptions
132
175
  };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { LightboxOptions } from "./src/types.js";
1
+ import { type LightboxOptions } from "./src/types.js";
2
+ export { initLightboxFromOptions } from "./client.js";
2
3
  export { initLightbox } from "./src/init.js";
3
4
  export { rehypeLightbox } from "./src/rehype.js";
4
5
  export type { LightboxInitOptions, LightboxOptions } from "./src/types.js";
package/dist/index.js CHANGED
@@ -38,10 +38,13 @@ function getChildren(node) {
38
38
 
39
39
  // src/types.ts
40
40
  var DEFAULT_TRIGGER_CLASS = "rr-lightbox-trigger";
41
+ var DEFAULT_EXPAND_LABEL = "Expand image";
42
+ var DEFAULT_CLOSE_LABEL = "Close";
41
43
 
42
44
  // src/rehype.ts
43
45
  function rehypeLightbox(options = {}) {
44
46
  const triggerClass = options.selectorClass ?? DEFAULT_TRIGGER_CLASS;
47
+ const expandLabel = options.expandLabel ?? DEFAULT_EXPAND_LABEL;
45
48
  return (tree) => {
46
49
  visitElements(tree, (node, parent, index) => {
47
50
  if (!parent || index === void 0 || node.tagName !== "img") return;
@@ -63,7 +66,7 @@ function rehypeLightbox(options = {}) {
63
66
  href: src,
64
67
  dataLightboxSrc: src,
65
68
  dataLightboxAlt: alt,
66
- ariaLabel: alt ? `画像を拡大表示: ${alt}` : "画像を拡大表示"
69
+ ariaLabel: alt ? `${expandLabel}: ${alt}` : expandLabel
67
70
  },
68
71
  children: [
69
72
  {
@@ -80,12 +83,13 @@ function rehypeLightbox(options = {}) {
80
83
  }
81
84
 
82
85
  // src/dialog.ts
83
- function createDialog() {
86
+ function createDialog(options) {
84
87
  const element = document.createElement("div");
85
88
  element.className = "rr-lightbox";
86
89
  element.setAttribute("role", "dialog");
87
90
  element.setAttribute("aria-modal", "true");
88
- element.setAttribute("aria-label", "画像の拡大表示");
91
+ element.setAttribute("aria-label", options.expandLabel);
92
+ element.tabIndex = -1;
89
93
  element.setAttribute("hidden", "");
90
94
  const frame = document.createElement("figure");
91
95
  frame.className = "rr-lightbox__frame";
@@ -97,7 +101,7 @@ function createDialog() {
97
101
  const closeButton = document.createElement("button");
98
102
  closeButton.className = "rr-lightbox__close";
99
103
  closeButton.type = "button";
100
- closeButton.setAttribute("aria-label", "閉じる");
104
+ closeButton.setAttribute("aria-label", options.closeLabel);
101
105
  closeButton.textContent = "×";
102
106
  frame.appendChild(image);
103
107
  frame.appendChild(caption);
@@ -109,7 +113,9 @@ function createDialog() {
109
113
  // src/init.ts
110
114
  function initLightbox(root = document, options = {}) {
111
115
  const triggerClass = options.selectorClass ?? DEFAULT_TRIGGER_CLASS;
112
- const wrappedTriggers = options.autoWrapImages === false ? [] : wrapImages(root, triggerClass);
116
+ const expandLabel = options.expandLabel ?? DEFAULT_EXPAND_LABEL;
117
+ const closeLabel = options.closeLabel ?? DEFAULT_CLOSE_LABEL;
118
+ const wrappedTriggers = options.autoWrapImages === false ? [] : wrapImages(root, triggerClass, expandLabel);
113
119
  const triggers = Array.from(
114
120
  root.querySelectorAll(`.${triggerClass}`)
115
121
  );
@@ -119,7 +125,7 @@ function initLightbox(root = document, options = {}) {
119
125
  };
120
126
  }
121
127
  let previouslyFocused = null;
122
- const dialog = createDialog();
128
+ const dialog = createDialog({ expandLabel, closeLabel });
123
129
  document.body.appendChild(dialog.element);
124
130
  const open = (trigger) => {
125
131
  const src = trigger.dataset.lightboxSrc ?? trigger.href;
@@ -151,7 +157,12 @@ function initLightbox(root = document, options = {}) {
151
157
  }
152
158
  };
153
159
  const handleKeyDown = (event) => {
154
- if (dialog.element.hidden || event.key !== "Escape") return;
160
+ if (dialog.element.hidden) return;
161
+ if (event.key === "Tab") {
162
+ trapFocus(event, dialog.element);
163
+ return;
164
+ }
165
+ if (event.key !== "Escape") return;
155
166
  event.preventDefault();
156
167
  close();
157
168
  };
@@ -171,7 +182,34 @@ function initLightbox(root = document, options = {}) {
171
182
  for (const trigger of wrappedTriggers) unwrapImage(trigger);
172
183
  };
173
184
  }
174
- function wrapImages(root, triggerClass) {
185
+ function trapFocus(event, container) {
186
+ const focusableElements = getFocusableElements(container);
187
+ if (focusableElements.length === 0) {
188
+ event.preventDefault();
189
+ container.focus();
190
+ return;
191
+ }
192
+ const first = focusableElements[0];
193
+ const last = focusableElements[focusableElements.length - 1];
194
+ const active = document.activeElement;
195
+ if (event.shiftKey && active === first) {
196
+ event.preventDefault();
197
+ last.focus();
198
+ return;
199
+ }
200
+ if (!event.shiftKey && active === last) {
201
+ event.preventDefault();
202
+ first.focus();
203
+ }
204
+ }
205
+ function getFocusableElements(container) {
206
+ return Array.from(
207
+ container.querySelectorAll(
208
+ 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
209
+ )
210
+ ).filter((element) => !element.hasAttribute("disabled"));
211
+ }
212
+ function wrapImages(root, triggerClass, expandLabel) {
175
213
  const images = Array.from(
176
214
  root.querySelectorAll("img[src]")
177
215
  );
@@ -190,7 +228,7 @@ function wrapImages(root, triggerClass) {
190
228
  trigger.dataset.lightboxAlt = image.alt;
191
229
  trigger.setAttribute(
192
230
  "aria-label",
193
- image.alt ? `画像を拡大表示: ${image.alt}` : "画像を拡大表示"
231
+ image.alt ? `${expandLabel}: ${image.alt}` : expandLabel
194
232
  );
195
233
  image.parentNode?.insertBefore(trigger, image);
196
234
  trigger.appendChild(image);
@@ -206,20 +244,35 @@ function unwrapImage(trigger) {
206
244
  trigger.remove();
207
245
  }
208
246
 
247
+ // client.ts
248
+ function initLightboxFromOptions(options = {}) {
249
+ return initLightbox(document, options);
250
+ }
251
+
209
252
  // index.ts
210
253
  function lightboxPlugin(options = {}) {
211
254
  return definePlugin({
212
255
  name: "lightbox",
256
+ processedContentCache: {
257
+ version: "lightbox-v2",
258
+ dependencyMode: "none"
259
+ },
213
260
  options,
214
261
  extendHtmlPipeline: (pipeline) => {
215
262
  pipeline.use(rehypeLightbox, options);
216
263
  },
217
264
  assets: [createStyleAsset("lightbox")],
218
- clientEntries: [createClientEntry("lightbox", "initLightbox")]
265
+ clientEntries: [
266
+ createClientEntry("lightbox", "initLightboxFromOptions", {
267
+ expandLabel: options.expandLabel ?? DEFAULT_EXPAND_LABEL,
268
+ closeLabel: options.closeLabel ?? DEFAULT_CLOSE_LABEL
269
+ })
270
+ ]
219
271
  });
220
272
  }
221
273
  export {
222
274
  initLightbox,
275
+ initLightboxFromOptions,
223
276
  lightboxPlugin,
224
277
  rehypeLightbox
225
278
  };
@@ -1,4 +1,7 @@
1
- export declare function createDialog(): {
1
+ export declare function createDialog(options: {
2
+ expandLabel: string;
3
+ closeLabel: string;
4
+ }): {
2
5
  element: HTMLDivElement;
3
6
  image: HTMLImageElement;
4
7
  caption: HTMLElement;
@@ -10,8 +10,12 @@ export type HastNode = ElementNode | {
10
10
  };
11
11
  export type LightboxOptions = {
12
12
  selectorClass?: string;
13
+ expandLabel?: string;
14
+ closeLabel?: string;
13
15
  };
14
16
  export type LightboxInitOptions = LightboxOptions & {
15
17
  autoWrapImages?: boolean;
16
18
  };
17
19
  export declare const DEFAULT_TRIGGER_CLASS = "rr-lightbox-trigger";
20
+ export declare const DEFAULT_EXPAND_LABEL = "Expand image";
21
+ export declare const DEFAULT_CLOSE_LABEL = "Close";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riebeckite/plugin-lightbox",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -21,7 +21,7 @@
21
21
  "./style.css": "./style.css"
22
22
  },
23
23
  "dependencies": {
24
- "@riebeckite/core": "0.0.13"
24
+ "@riebeckite/core": "0.0.15"
25
25
  },
26
26
  "publishConfig": {
27
27
  "access": "public"
@@ -56,6 +56,7 @@
56
56
  "./style.css"
57
57
  ],
58
58
  "scripts": {
59
- "build": "node ../../../scripts/build_package.mjs"
59
+ "build": "node ../../../scripts/build_package.mjs",
60
+ "test": "node --import tsx --test \"test/*.test.ts\""
60
61
  }
61
62
  }