@@ -16,10 +16,11 @@ const posts = import.meta.glob<GlossaryEntry>(
1616 eager: true ,
1717 }
1818);
19-
20- const regex = new RegExp (` ${id }\\ .md$|${id }\\ .mdx$ ` );
21- const [path, post] =
22- Object .entries (posts ).find (([path ]) => regex .test (path )) ?? [];
19+ // Escape user-provided id to avoid unintended regex behavior
20+ const escapeRegExp = (s : string ) => s .replace (/ [. *+?^${}()|[\]\\ ] / g , " \\ $&" );
21+ const safeId = escapeRegExp (String (id ));
22+ const regex = new RegExp (` (?:${safeId })\\ .(?:md|mdx)$ ` );
23+ const [, post] = Object .entries (posts ).find (([p ]) => regex .test (p )) ?? [];
2324if (! post ) throw new Error (` Glossary entry "${id }" not found ` );
2425
2526// Extract content safely with type checking
@@ -28,14 +29,20 @@ try {
2829 const content = await post .compiledContent ();
2930 htmlContent = String (content );
3031} catch (e ) {
31- throw new Error (` Failed to extract content for "${id }": ${e .message } ` );
32+ const msg = e instanceof Error ? e .message : String (e );
33+ throw new Error (` Failed to extract content for "${id }": ${msg } ` );
3234}
3335
3436if (! htmlContent ) throw new Error (` Empty content for "${id }" ` );
3537
36- const htmlSplit = htmlContent .split (" <hr>" );
38+ // Split at any <hr> tag variant (e.g., <hr>, <hr/>, <hr ...>) case-insensitively
39+ const htmlSplit = htmlContent .split (/ <hr[\s\S ] *? >/ i );
3740if (! htmlSplit .length ) throw new Error (` Invalid content format for "${id }" ` );
3841
42+ const primaryHtml = htmlSplit [0 ] ?? " " ;
43+ const moreHtml = htmlSplit [1 ] ?? " " ;
44+ const hasMore = Boolean (moreHtml && moreHtml .trim ().length > 0 );
45+
3946const basePath = ` /${Astro .url .pathname .split (" /" )[1 ]}/ ` ;
4047const glossaryUrl = ` ${basePath }glossary/${id } ` ;
4148const uniqueId = ` modal-${id }-${crypto .randomUUID ().slice (0 , 6 )} ` ;
@@ -46,10 +53,19 @@ const uniqueId = `modal-${id}-${crypto.randomUUID().slice(0, 6)}`;
4653 tabindex =" 0"
4754 data-tippy-maxWidth =" 100"
4855 data-unique-id ={ uniqueId }
49- data-html ={ ` <div id="${uniqueId }-primary">${htmlSplit [0 ]}<hr color="#923458" class="glossary-separator"><div class="glossary-url-div" id="${uniqueId }-glossary-url">🟩 <a class="glossary-url" href=${glossaryUrl } target="_blank">Click here for the full Glossary entry</a></div></div> ` }
50- data-html-more ={ htmlSplit [1 ]}
56+ data-dialog-label ={ label }
57+ data-has-more ={ String (hasMore )}
58+ data-html ={ ` <div id="${uniqueId }-primary">${primaryHtml }<hr color="#923458" class="glossary-separator"><div class="glossary-url-div" id="${uniqueId }-glossary-url">🟩 <a class="glossary-url" href=${glossaryUrl } target="_blank" rel="noopener noreferrer">Click here for the full Glossary entry</a></div></div> ` }
59+ data-html-more ={ moreHtml }
5160>
52- <a class =" glossary-url dialog-button-open" id ={ ` ${uniqueId }-open ` } >{ label } </a >
61+ <a
62+ class =" glossary-url dialog-button-open"
63+ id ={ ` ${uniqueId }-open ` }
64+ role =" button"
65+ tabindex =" 0"
66+ aria-haspopup =" dialog"
67+ aria-controls ={ ` ${uniqueId }-dialog ` }
68+ >{ label } </a >
5369</my-modal>
5470
5571<script >
@@ -67,6 +83,8 @@ const uniqueId = `modal-${id}-${crypto.randomUUID().slice(0, 6)}`;
6783 const glossaryUrl = `${uniqueId}-glossary-url`;
6884 const html = this.dataset.html ?? "";
6985 const htmlMore = this.dataset.htmlMore ?? "";
86+ const hasMore = (this.dataset.hasMore ?? "false") === "true";
87+ const dialogLabel = this.dataset.dialogLabel ?? "Glossary entry";
7088
7189 const body = document.querySelector("body");
7290 if (!body) {
@@ -75,12 +93,12 @@ const uniqueId = `modal-${id}-${crypto.randomUUID().slice(0, 6)}`;
7593
7694 body.insertAdjacentHTML(
7795 "beforeend",
78- `<dialog class="modal-dialog" id="${idDialog}">
96+ `<dialog class="modal-dialog" id="${idDialog}" role="dialog" aria-modal="true" aria-label="${dialogLabel}" >
7997 <div class="dialog-content" id="${idContent}"></div>
8098 <div class="dialog-button">
8199 <div class="dialog-show-more" id="${showMore}" style="display: none"></div>
82- <button class="dialog-button-close" id="${buttonClose}" type="reset ">Close this Dialog</button>
83- <button class="dialog-button-more" id="${buttonMore}" type="reset ">Show more</button>
100+ <button class="dialog-button-close" id="${buttonClose}" type="button ">Close this Dialog</button>
101+ <button class="dialog-button-more" id="${buttonMore}" type="button" aria-expanded="false ">Show more</button>
84102 </div>
85103 </dialog>`
86104 );
@@ -109,9 +127,29 @@ const uniqueId = `modal-${id}-${crypto.randomUUID().slice(0, 6)}`;
109127 return;
110128 }
111129
130+ // Hide the "Show more" button if there's no extra content
131+ if (!hasMore && toggleMore) {
132+ toggleMore.style.display = "none";
133+ }
134+
112135 // Update button opens a modal dialog
113136 openDialog.addEventListener("click", () => {
114137 (dialog as HTMLDialogElement).showModal();
138+ // Move focus into the dialog
139+ const closeBtn = closeDialog as HTMLButtonElement;
140+ // Save the element to restore focus on close
141+ (dialog as any)._lastActiveElement = document.activeElement || null;
142+ closeBtn?.focus();
143+ });
144+
145+ // Open with keyboard (Enter/Space) for accessibility
146+ openDialog.addEventListener("keydown", (event: KeyboardEvent) => {
147+ if (event.key === "Enter" || event.key === " ") {
148+ event.preventDefault();
149+ (dialog as HTMLDialogElement).showModal();
150+ (dialog as any)._lastActiveElement = document.activeElement || null;
151+ (closeDialog as HTMLButtonElement)?.focus();
152+ }
115153 });
116154
117155 // Form cancel button closes the dialog box
@@ -132,19 +170,8 @@ const uniqueId = `modal-${id}-${crypto.randomUUID().slice(0, 6)}`;
132170 toggleDisplay(moreDialog);
133171 toggleMore.innerHTML =
134172 toggleMore.innerHTML === "Show less" ? "Show more" : "Show less";
135- });
136-
137- // Close the dialog when clicked outside
138- window.addEventListener("click", (event) => {
139- if (event.target === dialog) {
140- (dialog as HTMLDialogElement).close();
141- if (moreDialog) {
142- moreDialog.style.display = "none";
143- }
144- if (dialogUrl) {
145- dialogUrl.style.display = "block";
146- }
147- }
173+ const expanded = toggleMore.getAttribute("aria-expanded") === "true";
174+ toggleMore.setAttribute("aria-expanded", expanded ? "false" : "true");
148175 });
149176
150177 const resetContent = () => {
@@ -162,28 +189,42 @@ const uniqueId = `modal-${id}-${crypto.randomUUID().slice(0, 6)}`;
162189 }
163190 if (toggleMore) {
164191 toggleMore.innerHTML = "Show more";
192+ toggleMore.setAttribute("aria-expanded", "false");
165193 }
166194 };
167195
168196 const handleClose = () => {
169197 resetContent();
170198 (dialog as HTMLDialogElement).close();
199+ // Restore focus to the trigger, if possible
200+ const last = (dialog as any)._lastActiveElement as Element | null;
201+ if (last && "focus" in last) {
202+ (last as HTMLElement).focus();
203+ } else {
204+ (openDialog as HTMLElement)?.focus();
205+ }
171206 };
172207
173208 // Update all close handlers
174209 closeDialog.addEventListener("click", handleClose);
175210
211+ // Close the dialog when clicked on the backdrop
176212 window.addEventListener("click", (event) => {
177213 if (event.target === dialog) {
178214 handleClose();
179215 }
180216 });
181217
182218 document.addEventListener("keydown", (event) => {
183- if (event.key === "Escape" && dialog.open) {
219+ if (event.key === "Escape" && ( dialog as HTMLDialogElement) .open) {
184220 handleClose();
185221 }
186222 });
223+
224+ // Keep dialog state consistent if closed by other means
225+ (dialog as HTMLDialogElement).addEventListener("close", () => {
226+ resetContent();
227+ });
187228 }
188229 }
189230 customElements.define("my-modal", MyModal);
0 commit comments