Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions docs/assets/javascripts/sidebar-toggle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
(function () {
"use strict";

const storageKey = "dg-sidebar-collapsed";
const desktopQuery = "(min-width: 1220px)";

function readStoredState(storage) {
if (!storage) return false;
try {
return storage.getItem(storageKey) === "true";
} catch {
return false;
}
}

function writeStoredState(storage, collapsed) {
if (!storage) return;
try {
storage.setItem(storageKey, collapsed ? "true" : "false");
} catch {
// Ignore storage failures (private mode / strict policies).
}
}

function getStorage(windowRef) {
try {
return windowRef.localStorage || null;
} catch {
return null;
}
}

function applyState(root, button, storage, collapsed) {
root.dataset.dgSidebarCollapsed = collapsed ? "true" : "false";
button.setAttribute("aria-expanded", collapsed ? "false" : "true");
button.setAttribute("aria-label", "왼쪽 메뉴");
writeStoredState(storage, collapsed);
}

function mountSidebarToggle(documentRef, windowRef) {
if (!documentRef || !windowRef) return null;
const root = documentRef.documentElement;
if (!root || root.dataset.dgSidebarToggleReady === "true") return null;
const headerInner = documentRef.querySelector(".md-header__inner");
if (!headerInner) return null;

root.dataset.dgSidebarToggleReady = "true";

const button = documentRef.createElement("button");
button.type = "button";
button.className = "md-header__button md-icon dg-sidebar-toggle";
button.setAttribute("aria-expanded", "true");
button.setAttribute("aria-label", "왼쪽 메뉴");
button.title = "왼쪽 메뉴 접기/펼치기";
button.innerHTML =
'<svg viewBox="0 0 24 24" role="presentation" focusable="false"><path d="M3 6h18v2H3V6zm0 5h18v2H3v-2zm0 5h18v2H3v-2z"/></svg>';

const mediaQuery = typeof windowRef.matchMedia === "function"
? windowRef.matchMedia(desktopQuery)
: null;
const storage = getStorage(windowRef);

const syncState = () => {
const collapsed = readStoredState(storage);
applyState(root, button, storage, collapsed);
};

button.addEventListener("click", () => {
const collapsed = root.dataset.dgSidebarCollapsed === "true";
applyState(root, button, storage, !collapsed);
});

if (mediaQuery) {
const onMediaChange = event => {
if (event.matches) syncState();
};
if (typeof mediaQuery.addEventListener === "function") {
mediaQuery.addEventListener("change", onMediaChange);
} else if (typeof mediaQuery.addListener === "function") {
mediaQuery.addListener(onMediaChange);
}
}

syncState();
headerInner.insertBefore(button, headerInner.firstChild);
return button;
}

if (typeof module !== "undefined" && module.exports) {
module.exports = {mountSidebarToggle, readStoredState, applyState, getStorage};
}

if (typeof document !== "undefined") {
const start = () => mountSidebarToggle(document, window);
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", start);
} else {
start();
}
}
})();
30 changes: 30 additions & 0 deletions docs/assets/stylesheets/extra.css
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,37 @@ body {
box-shadow: inset 2px 0 var(--dg-accent);
}

.dg-sidebar-toggle {
display: none;
}

@media (max-width: 1219px) {
.md-header .dg-sidebar-toggle {
display: none !important;
}
}

@media (min-width: 1220px) {
.md-header .dg-sidebar-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
}

.dg-sidebar-toggle svg {
width: 1.1rem;
height: 1.1rem;
fill: currentColor;
}

[data-dg-sidebar-collapsed="true"] .md-sidebar--primary {
display: none;
}

[data-dg-sidebar-collapsed="true"] .dg-sidebar-toggle {
color: var(--dg-accent);
}

.md-nav--primary .md-nav[data-md-level="1"],
.md-nav--primary .md-nav[data-md-level="2"] {
border-left: 1px solid var(--dg-border);
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,5 +82,6 @@ extra_css:

extra_javascript:
- assets/javascripts/explore.js
- assets/javascripts/sidebar-toggle.js

copyright: Copyright &copy; Azure Engineering Notes contributors
114 changes: 114 additions & 0 deletions tests/docs/sidebar-toggle.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
const test = require("node:test");
const assert = require("node:assert/strict");
const toggle = require("../../docs/assets/javascripts/sidebar-toggle.js");

function createFixture(stored = "false") {
const root = {dataset: {}};
const buttonListeners = {};
const inserted = [];

const button = {
attributes: {},
type: "",
className: "",
title: "",
innerHTML: "",
setAttribute(name, value) {
this.attributes[name] = value;
},
addEventListener(type, callback) {
buttonListeners[type] = callback;
},
};

const headerInner = {
firstChild: null,
insertBefore(node) {
inserted.push(node);
},
};

const localStorage = {
value: stored,
getItem() {
return this.value;
},
setItem(_key, value) {
this.value = value;
},
};

const media = {
matches: true,
addEventListener() {},
};

const documentRef = {
documentElement: root,
querySelector(selector) {
return selector === ".md-header__inner" ? headerInner : null;
},
createElement(tag) {
assert.equal(tag, "button");
return button;
},
};

const windowRef = {
localStorage,
matchMedia() {
return media;
},
};

return {
root,
button,
buttonListeners,
inserted,
documentRef,
windowRef,
localStorage,
media,
};
}

test("mountSidebarToggle inserts a button and applies stored collapsed state", () => {
const fixture = createFixture("true");
const mounted = toggle.mountSidebarToggle(fixture.documentRef, fixture.windowRef);
assert.equal(mounted, fixture.button);
assert.equal(fixture.inserted.length, 1);
assert.equal(fixture.root.dataset.dgSidebarCollapsed, "true");
assert.equal(fixture.button.attributes["aria-expanded"], "false");
assert.equal(fixture.button.attributes["aria-label"], "왼쪽 메뉴");
assert.equal(fixture.button.attributes["aria-pressed"], undefined);
assert.equal(fixture.root.dataset.dgSidebarToggleReady, "true");
});

test("button click toggles collapsed state and persists to storage", () => {
const fixture = createFixture("false");
toggle.mountSidebarToggle(fixture.documentRef, fixture.windowRef);
fixture.buttonListeners.click();
assert.equal(fixture.root.dataset.dgSidebarCollapsed, "true");
assert.equal(fixture.button.attributes["aria-expanded"], "false");
assert.equal(fixture.localStorage.value, "true");
fixture.buttonListeners.click();
assert.equal(fixture.root.dataset.dgSidebarCollapsed, "false");
assert.equal(fixture.button.attributes["aria-expanded"], "true");
assert.equal(fixture.localStorage.value, "false");
});

test("blocked localStorage still mounts without persistence", () => {
const fixture = createFixture("true");
Object.defineProperty(fixture.windowRef, "localStorage", {
get() {
throw new Error("storage blocked");
},
});
const mounted = toggle.mountSidebarToggle(fixture.documentRef, fixture.windowRef);
assert.equal(mounted, fixture.button);
assert.equal(fixture.inserted.length, 1);
assert.equal(fixture.root.dataset.dgSidebarCollapsed, "false");
fixture.buttonListeners.click();
assert.equal(fixture.root.dataset.dgSidebarCollapsed, "true");
});
5 changes: 3 additions & 2 deletions tests/docs/test_faceted_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,11 +261,12 @@ def test_service_discovery_reports_topics_and_documents(taxonomy, catalog):
assert "개 주제 · " in pages[PurePosixPath(path)]


def test_javascript_behavior_with_available_node():
@pytest.mark.parametrize("script_name", ("explore.test.cjs", "sidebar-toggle.test.cjs"))
def test_javascript_behavior_with_available_node(script_name):
node = shutil.which("node")
if not node:
pytest.skip("Node unavailable: parent browser verification is required")
result = subprocess.run([node, "--test", str(ROOT / "tests/docs/explore.test.cjs")],
result = subprocess.run([node, "--test", str(ROOT / "tests/docs" / script_name)],
cwd=ROOT, capture_output=True, text=True)
assert result.returncode == 0, result.stdout + result.stderr

Expand Down
Loading