diff --git a/src/CollapsableExtLink.ts b/src/CollapsableExtLink.ts index cc6ca18..36c5fd2 100644 --- a/src/CollapsableExtLink.ts +++ b/src/CollapsableExtLink.ts @@ -1,5 +1,6 @@ import { CollapsableItem } from './CollapsableItem' import { Collapsable } from './Collapsable' +import { AttributeSnapshot } from './utils' export class CollapsableExtLink { private readonly collapsable: Collapsable @@ -10,6 +11,8 @@ export class CollapsableExtLink { private listener: EventListener | undefined private ariaPerRole!: 'aria-expanded' | 'aria-selected' // always set by `this.prepareDOM()` called from constructor + private originalAttributes = new AttributeSnapshot() + public constructor( collapsable: Collapsable, link: HTMLAnchorElement | HTMLButtonElement, @@ -29,9 +32,11 @@ export class CollapsableExtLink { } private prepareDOM(): void { + this.originalAttributes.remember(this.extLink, 'aria-controls') this.extLink.setAttribute('aria-controls', this.collapsableItem.boxElements.map((box) => box.id).join(' ')) if (this.extLink instanceof HTMLAnchorElement && !this.extLink.role) { + this.originalAttributes.remember(this.extLink, 'role') this.extLink.role = 'button' } @@ -41,6 +46,7 @@ export class CollapsableExtLink { this.ariaPerRole = 'aria-selected' } + this.originalAttributes.remember(this.extLink, this.ariaPerRole) this.extLink.setAttribute(this.ariaPerRole, String(this.collapsableItem.isExpanded)) } @@ -72,9 +78,10 @@ export class CollapsableExtLink { public destroy(): void { if (this.listener) { this.extLink.removeEventListener('click', this.listener) - this.extLink.removeAttribute('aria-controls') - this.extLink.removeAttribute(this.ariaPerRole) - this.extLink.removeAttribute('role') + this.originalAttributes.restore(this.extLink, 'aria-controls') + this.originalAttributes.restore(this.extLink, this.ariaPerRole) + this.originalAttributes.restore(this.extLink, 'role') + this.originalAttributes.clear() } } } diff --git a/src/CollapsableItem.ts b/src/CollapsableItem.ts index 8538098..3df00e5 100644 --- a/src/CollapsableItem.ts +++ b/src/CollapsableItem.ts @@ -1,5 +1,5 @@ import { Collapsable, CollapsableEvent } from './Collapsable' -import { getUid } from './utils' +import { AttributeSnapshot, getUid } from './utils' type CollapsableItemAction = 'expand' | 'collapse' type CollapsableItemEvents = @@ -40,6 +40,8 @@ export class CollapsableItem { private listenersMap: ListenersMapItem[] = [] + private originalAttributes = new AttributeSnapshot() + public constructor(collapsable: Collapsable, element: HTMLElement) { this.collapsable = collapsable @@ -85,6 +87,10 @@ export class CollapsableItem { box.id = boxItemId ariaControlsAttr.push(boxItemId) + + // Snapshot before any modification (hidden/aria-hidden are set later in handleExpandCollapse). + this.originalAttributes.remember(box, 'hidden') + this.originalAttributes.remember(box, 'aria-hidden') }) this.controlElements.forEach((control) => { @@ -105,9 +111,15 @@ export class CollapsableItem { } interactiveElement.classList.add(options.classNames.interactiveElement) + + this.originalAttributes.remember(interactiveElement, 'aria-controls') interactiveElement.setAttribute('aria-controls', ariaControlsAttr.join(' ')) + // aria-expanded is set later in handleExpandCollapse, snapshot it now. + this.originalAttributes.remember(interactiveElement, 'aria-expanded') + if (interactiveElement.tagName.toLowerCase() === 'a') { + this.originalAttributes.remember(interactiveElement, 'role') interactiveElement.setAttribute('role', 'button') } @@ -423,18 +435,21 @@ export class CollapsableItem { interactiveElement.parentElement.innerHTML = interactiveElement.innerHTML } else { interactiveElement.classList.remove(options.classNames.interactiveElement) - interactiveElement.removeAttribute('aria-controls') - interactiveElement.removeAttribute('aria-expanded') + this.originalAttributes.restore(interactiveElement, 'aria-controls') + this.originalAttributes.restore(interactiveElement, 'aria-expanded') + this.originalAttributes.restore(interactiveElement, 'role') } }) this.boxElements.forEach((box) => { - box.removeAttribute('aria-hidden') - box.removeAttribute('hidden') + this.originalAttributes.restore(box, 'aria-hidden') + this.originalAttributes.restore(box, 'hidden') delete box.dataset.collapsableState }) this.element.dispatchEvent(new CustomEvent('destroy.collapsable', { bubbles: true })) + + this.originalAttributes.clear() } } diff --git a/src/index.esm.ts b/src/index.esm.ts index 840e239..bdcc1e9 100644 --- a/src/index.esm.ts +++ b/src/index.esm.ts @@ -1,2 +1,2 @@ -export { deepMerge } from './utils' +export { deepMerge, AttributeSnapshot } from './utils' export { Collapsable as default } from './Collapsable' diff --git a/src/utils.ts b/src/utils.ts index 83be099..a8bf10b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -36,3 +36,37 @@ export let caUid = 0 export function getUid() { return 'ca-uid-' + caUid++ } + +// Remembers original attribute values before collapsable modifies them, so they can be restored on destroy. +export class AttributeSnapshot { + private store = new Map>() + + public remember(element: HTMLElement, name: string): void { + let attrs = this.store.get(element) + if (!attrs) { + attrs = {} + this.store.set(element, attrs) + } + // Only the first (original) value is stored, repeated calls won't overwrite it. + if (!(name in attrs)) { + attrs[name] = element.getAttribute(name) + } + } + + public restore(element: HTMLElement, name: string): void { + const attrs = this.store.get(element) + if (!attrs || !(name in attrs)) { + return + } + const value = attrs[name] + if (value === null) { + element.removeAttribute(name) + } else { + element.setAttribute(name, value) + } + } + + public clear(): void { + this.store.clear() + } +}