Skip to content

Commit 7fa87db

Browse files
committed
docs
1 parent a09ef07 commit 7fa87db

File tree

3 files changed

+120
-41
lines changed

3 files changed

+120
-41
lines changed

docs/src/components/advancedButton.astro

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11

22

3-
<!-- Early inline script to avoid flash: set the attribute before styles run -->
3+
<!-- Early inline script to avoid flash: set the attribute before styles run (default: basic/off) -->
44
<script is:inline>
55
try {
66
const stored = localStorage.getItem('advanced');
7-
const value = stored === 'false' ? 'false' : 'true';
7+
const value = (stored === 'true' || stored === 'false') ? stored : 'false';
88
document.documentElement.dataset.advanced = value;
99
} catch {}
10-
// Default is "true" (show advanced) when not set
10+
// Default is "false" (basic mode) when not set
1111
</script>
1212

1313
<!-- Hide the toggle until the state is applied to avoid visual flash -->
@@ -18,7 +18,7 @@
1818
class="advanced-toggle"
1919
id="advanced-switch"
2020
role="switch"
21-
aria-checked="true"
21+
aria-checked="false"
2222
aria-label="Toggle advanced content"
2323
/>
2424
<label for="advanced-switch" class="sr-only">Toggle advanced content</label>
@@ -28,10 +28,36 @@
2828
const __initAdvancedToggle = () => {
2929
const input = document.querySelector('[data-advanced-input]');
3030
const wrapper = document.getElementById('advanced-toggle-wrapper');
31-
const current = (() => {
32-
try { return localStorage.getItem('advanced'); } catch { return null; }
33-
})();
34-
const isOn = current !== 'false';
31+
// Initialize from dataset set above (default false)
32+
const isOn = (document.documentElement.dataset.advanced || 'false') !== 'false';
33+
34+
// Build a registry of advanced blocks and their removable wrapper targets
35+
/** @type {{ el: Element, target: Element, placeholder: Comment }[]} */
36+
const registry = [];
37+
const prepared = new WeakSet();
38+
const isWhitespaceOnly = (node) => (node?.textContent || '').trim() === '';
39+
const getWrapperCandidate = (el) => {
40+
const p = el.parentElement;
41+
if (!p) return null;
42+
const tag = p.tagName;
43+
// Collapse common markdown wrappers if they only contain Advanced
44+
if (tag !== 'P' && tag !== 'DIV') return null;
45+
const onlyChild = p.childElementCount === 1 && p.firstElementChild === el;
46+
if (!onlyChild) return null;
47+
if (!isWhitespaceOnly(p)) return null;
48+
return p;
49+
};
50+
const prepareRegistry = () => {
51+
const nodes = document.querySelectorAll('.advanced-title');
52+
for (const el of nodes) {
53+
if (prepared.has(el)) continue;
54+
const wrap = getWrapperCandidate(el);
55+
const target = wrap || el;
56+
const placeholder = document.createComment('advanced:placeholder');
57+
prepared.add(el);
58+
registry.push({ el, target, placeholder });
59+
}
60+
};
3561

3662
// Keep tooltip text in sync with state; uses both native title and data-tooltip
3763
const setTooltip = (on) => {
@@ -44,10 +70,16 @@
4470
};
4571

4672
const applyVisibility = (on) => {
47-
// Advanced content blocks
48-
const blocks = document.querySelectorAll('.advanced-title');
49-
for (const el of blocks) {
50-
el.style.display = on ? '' : 'none';
73+
// Advanced content blocks: remove or restore by swapping with placeholders
74+
prepareRegistry();
75+
if (on) {
76+
for (const { target, placeholder } of registry) {
77+
if (placeholder.parentNode) placeholder.replaceWith(target);
78+
}
79+
} else {
80+
for (const { target, placeholder } of registry) {
81+
if (target.parentNode) target.replaceWith(placeholder);
82+
}
5183
}
5284
// Advanced-only TOC items (supports Shadow DOM and mobile dropdown)
5385
const setLinks = (root) => {
@@ -163,7 +195,13 @@
163195
const handler = () => {
164196
const next = input.checked;
165197
try { localStorage.setItem('advanced', next ? 'true' : 'false'); } catch {}
166-
setState(next);
198+
if (next) {
199+
// Ensure dataset reflects the new mode, then reload to fully hydrate late content
200+
document.documentElement.dataset.advanced = 'true';
201+
window.location.reload();
202+
} else {
203+
setState(next);
204+
}
167205
};
168206
input.addEventListener('change', handler);
169207
input.addEventListener('click', handler, { passive: true });

docs/src/components/advancedMarkdown.astro

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
33
---
44

5-
<div class="Advanced-class advanced-title" style="display: none;" ;>
5+
<span class="Advanced-class advanced-title" style="display: contents;">
66
<slot />
7-
</div>
7+
</span>

docs/src/components/modal.astro

Lines changed: 67 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -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)) ?? [];
2324
if (!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
3436
if (!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);
3740
if (!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+
3946
const basePath = `/${Astro.url.pathname.split("/")[1]}/`;
4047
const glossaryUrl = `${basePath}glossary/${id}`;
4148
const 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

Comments
 (0)