Skip to content
Draft
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
17 changes: 0 additions & 17 deletions src/Elastic.Documentation.Site/Assets/image-carousel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,23 +70,6 @@ class ImageCarousel {
indicator.addEventListener('click', () => this.goToSlide(index))
})

// Handle image clicks for modal
this.slides.forEach((slide) => {
const imageLink = slide.querySelector('.carousel-image-reference')
if (imageLink) {
imageLink.addEventListener('click', (e) => {
e.preventDefault()
const modalId = imageLink.getAttribute('data-modal-id')
if (modalId) {
const modal = document.getElementById(modalId)
if (modal) {
modal.style.display = 'flex'
}
}
})
}
})

// Keyboard navigation
document.addEventListener('keydown', (e) => {
if (!this.isInViewport()) return
Expand Down
77 changes: 77 additions & 0 deletions src/Elastic.Documentation.Site/Assets/image-dialog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
const triggers = new WeakMap<HTMLDialogElement, HTMLButtonElement>()
let delegatedListenersInitialized = false

function closeDialog(dialog: HTMLDialogElement) {
if (dialog.open) dialog.close()
}

function initializeDelegatedListeners() {
if (delegatedListenersInitialized) return
delegatedListenersInitialized = true

document.addEventListener(
'keydown',
(event) => {
if (event.key !== 'Escape') return

const openDialog = document.querySelector(
'dialog[data-image-dialog][open]'
)
if (openDialog instanceof HTMLDialogElement) {
closeDialog(openDialog)
event.preventDefault()
}
},
{ capture: true }
)

document.addEventListener('click', (event) => {
if (!(event.target instanceof Element)) return

const openButton = event.target.closest('[data-image-dialog-open]')
if (openButton instanceof HTMLButtonElement) {
const dialogId = openButton.getAttribute('aria-controls')
if (!dialogId) return

const dialog = document.getElementById(dialogId)
if (!(dialog instanceof HTMLDialogElement)) return

triggers.set(dialog, openButton)
dialog.showModal()
const closeButton = dialog.querySelector(
'[data-image-dialog-close]'
)
if (closeButton instanceof HTMLButtonElement) closeButton.focus()
return
}

const closeButton = event.target.closest('[data-image-dialog-close]')
if (closeButton instanceof HTMLButtonElement) {
const dialog = closeButton.closest('dialog')
if (dialog instanceof HTMLDialogElement) closeDialog(dialog)
return
}

if (
event.target instanceof HTMLDialogElement &&
event.target.matches('[data-image-dialog]')
) {
closeDialog(event.target)
}
})
}

export function initImageDialogs() {
initializeDelegatedListeners()

document
.querySelectorAll<HTMLDialogElement>('dialog[data-image-dialog]')
.forEach((dialog) => {
if (dialog.dataset.initialized === 'true') return
dialog.dataset.initialized = 'true'
dialog.addEventListener('close', () => {
triggers.get(dialog)?.focus()
triggers.delete(dialog)
})
})
}
2 changes: 2 additions & 0 deletions src/Elastic.Documentation.Site/Assets/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { config } from './config'
import { initCopyButton } from './copybutton'
import { initHighlight } from './hljs'
import { initImageCarousel } from './image-carousel'
import { initImageDialogs } from './image-dialog'
import { initMermaid } from './mermaid'
import { openDetailsWithAnchor } from './open-details-with-anchor'
import { initNav } from './pages-nav'
Expand Down Expand Up @@ -215,6 +216,7 @@ document.addEventListener('htmx:load', function () {
['initSmoothScroll', initSmoothScroll],
['openDetailsWithAnchor', openDetailsWithAnchor],
['initImageCarousel', initImageCarousel],
['initImageDialogs', initImageDialogs],
['initTable', initTable],
['initApiDocs', initApiDocs],
['applyEditParam', applyEditParam],
Expand Down
26 changes: 21 additions & 5 deletions src/Elastic.Documentation.Site/Assets/modal.css
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
.modal {
@apply fixed inset-0 z-50 flex hidden items-center justify-center bg-black/50;
@apply fixed inset-0 z-50 m-auto max-h-none max-w-none items-center justify-center border-0 bg-transparent p-0;
width: 100%;
height: 100%;
}

.modal[open] {
display: flex;
}

.modal::backdrop {
background: rgb(0 0 0 / 50%);
}

.modal-content {
@apply relative w-full max-w-3xl items-center rounded-lg bg-white p-6 shadow-lg;
max-height: calc(100vh - 2rem);
overflow: auto;
}

.modal-content figure {
Expand All @@ -23,11 +35,15 @@
}

.modal-close {
@apply absolute top-2 right-4 cursor-pointer transition-colors;
@apply text-ink-dark hover:text-ink absolute top-2 right-2 z-10 flex min-h-10 min-w-10 cursor-pointer items-center justify-center rounded-sm bg-white text-2xl font-bold transition-colors;
}

& > a {
@apply text-ink-dark hover:text-ink text-2xl font-bold no-underline;
}
.image-dialog-trigger {
@apply cursor-zoom-in border-0 bg-transparent p-0 text-left;
}

body:has(dialog.modal[open]) {
overflow: hidden;
}

/* Search or Ask AI animation for secondary buttons */
Expand Down
180 changes: 143 additions & 37 deletions src/Elastic.Documentation.Site/Assets/pages-nav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,50 @@ import { throttle } from 'lodash'
import { $optional, $$optional } from 'select-dom'

const NAV_STATE_KEY = 'nav-expanded'
let controlsInitialized = false
let pagesNavTrigger: HTMLButtonElement | null = null

function isDevMode() {
return !!document.querySelector('diagnostics-panel')
}

function saveNavState(nav: HTMLElement) {
const expanded = $$optional('input[type="checkbox"]:checked', nav)
.map((el) => el.id)
const expanded = $$optional<HTMLButtonElement>(
'button[data-nav-toggle][aria-expanded="true"]',
nav
)
.map((el) => el.getAttribute('aria-controls'))
.filter(Boolean)
sessionStorage.setItem(NAV_STATE_KEY, JSON.stringify(expanded))
}

function setExpanded(button: HTMLButtonElement, expanded: boolean) {
const controlledId = button.getAttribute('aria-controls')
if (!controlledId) return

const controlled = document.getElementById(controlledId)
if (!controlled) return

button.setAttribute('aria-expanded', expanded.toString())
controlled.hidden = !expanded
}

function restoreNavState(nav: HTMLElement) {
const raw = sessionStorage.getItem(NAV_STATE_KEY)
if (!raw) return
try {
const ids: string[] = JSON.parse(raw)
for (const id of ids) {
const input = $optional(`#${CSS.escape(id)}`, nav)
if (input instanceof HTMLInputElement) {
input.checked = true
}
const button =
$optional(
`button[data-nav-toggle][aria-controls="${CSS.escape(id)}"]`,
nav
) ??
$optional(
`button[data-nav-toggle][aria-controls="${CSS.escape(`nav-subtree-${id}`)}"]`,
nav
)
if (button instanceof HTMLButtonElement) setExpanded(button, true)
}
} catch {
/* ignore corrupt storage */
Expand All @@ -33,14 +55,123 @@ function restoreNavState(nav: HTMLElement) {
function expandAllParents(navItem: HTMLElement) {
let parent: HTMLLIElement | null | undefined = navItem?.closest('li')
while (parent) {
const input = parent.querySelector('input')
if (input instanceof HTMLInputElement) {
input.checked = true
}
const button = parent.querySelector(
':scope > div > button[data-nav-toggle]'
)
if (button instanceof HTMLButtonElement) setExpanded(button, true)
parent = parent.parentElement?.closest('li')
}
}

function setPagesNavOpen(open: boolean, restoreFocus = false) {
const panel = document.querySelector('[data-pages-nav-panel]')
const backdrop = document.querySelector('[data-pages-nav-backdrop]')
if (!(panel instanceof HTMLElement)) return

panel.dataset.open = open.toString()
if (backdrop instanceof HTMLButtonElement) backdrop.hidden = !open
document.body.classList.toggle('overflow-hidden', open)
$$optional<HTMLButtonElement>('[data-pages-nav-open]').forEach((button) =>
button.setAttribute('aria-expanded', open.toString())
)

if (open) {
const closeButton = panel.querySelector('[data-pages-nav-close]')
if (closeButton instanceof HTMLButtonElement) closeButton.focus()
} else if (restoreFocus) {
pagesNavTrigger?.focus()
}
}

function initializeControls() {
if (controlsInitialized) return
controlsInitialized = true

document.addEventListener('click', (event) => {
if (!(event.target instanceof Element)) return

const openDropdown = document.querySelector(
'[data-pages-dropdown-toggle][aria-expanded="true"]'
)
if (
openDropdown instanceof HTMLButtonElement &&
!event.target.closest('#pages-dropdown')
) {
setExpanded(openDropdown, false)
}

const openButton = event.target.closest('[data-pages-nav-open]')
if (openButton instanceof HTMLButtonElement) {
pagesNavTrigger = openButton
setPagesNavOpen(true)
return
}

if (
event.target.closest('[data-pages-nav-close]') ||
event.target.closest('[data-pages-nav-backdrop]')
) {
setPagesNavOpen(false, true)
return
}

const navToggle = event.target.closest('[data-nav-toggle]')
if (navToggle instanceof HTMLButtonElement) {
setExpanded(
navToggle,
navToggle.getAttribute('aria-expanded') !== 'true'
)
const pagesNav = $optional('#pages-nav')
if (isDevMode() && pagesNav) saveNavState(pagesNav)
return
}

const dropdownToggle = event.target.closest(
'[data-pages-dropdown-toggle]'
)
if (dropdownToggle instanceof HTMLButtonElement) {
setExpanded(
dropdownToggle,
dropdownToggle.getAttribute('aria-expanded') !== 'true'
)
}
})

document.addEventListener('focusin', (event) => {
if (
event.target instanceof Element &&
!event.target.closest('#pages-dropdown')
) {
const openDropdown = document.querySelector(
'[data-pages-dropdown-toggle][aria-expanded="true"]'
)
if (openDropdown instanceof HTMLButtonElement) {
setExpanded(openDropdown, false)
}
}
})

document.addEventListener('keydown', (event) => {
if (event.key !== 'Escape') return

if (document.querySelector('dialog[data-image-dialog][open]')) return

const openDropdown = document.querySelector(
'[data-pages-dropdown-toggle][aria-expanded="true"]'
)
if (openDropdown instanceof HTMLButtonElement) {
setExpanded(openDropdown, false)
openDropdown.focus()
return
}

const panel = document.querySelector('[data-pages-nav-panel]')
if (panel instanceof HTMLElement && panel.dataset.open === 'true') {
setPagesNavOpen(false, true)
}
})
}

function scrollCurrentNaviItemIntoViewImpl(nav: HTMLElement) {
const currentNavItem = $optional('.current', nav)

Expand Down Expand Up @@ -94,36 +225,14 @@ export const scrollCurrentNaviItemIntoView = throttle(
{ leading: false, trailing: true }
)

/**
* Prevents focus-based dropdowns from closing before link navigation completes.
* Without this, clicking a link inside the dropdown would transfer focus away,
* causing the dropdown to close via CSS :focus-within before navigation happens.
*/
function preventFocusLossOnLinkClick(anchor: HTMLAnchorElement) {
anchor.addEventListener('mousedown', (e) => {
e.preventDefault()
})
// Close dropdown after click completes
anchor.addEventListener('mouseup', () => {
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur()
}
})
}

export function initNav() {
initializeControls()

const pagesNav = $optional('#pages-nav')
if (!pagesNav) {
return
}

const dropdownActiveAnchor = $optional(
'#pages-dropdown a.pages-dropdown_active'
)
if (dropdownActiveAnchor) {
preventFocusLossOnLinkClick(dropdownActiveAnchor)
}

if (isDevMode()) {
restoreNavState(pagesNav)
}
Expand Down Expand Up @@ -155,8 +264,5 @@ export function initNav() {

if (isDevMode()) {
saveNavState(pagesNav)
for (const cb of $$optional('input[type="checkbox"]', pagesNav)) {
cb.addEventListener('change', () => saveNavState(pagesNav))
}
}
}
Loading
Loading