diff --git a/src/components/compress/custom-els/MultiPanel/index.ts b/src/components/compress/custom-els/MultiPanel/index.ts index 10922bad..27f91a01 100644 --- a/src/components/compress/custom-els/MultiPanel/index.ts +++ b/src/components/compress/custom-els/MultiPanel/index.ts @@ -1,4 +1,5 @@ import * as style from './styles.css'; +import { transitionHeight } from '../../../../lib/util'; interface CloseAllOptions { exceptFirst?: boolean; @@ -6,15 +7,44 @@ interface CloseAllOptions { const openOneOnlyAttr = 'open-one-only'; -function getClosestHeading(el: Element) { +function getClosestHeading(el: Element): HTMLElement | undefined { // Look for the child of multi-panel, but stop at interactive elements like links & buttons const closestEl = el.closest('multi-panel > *, a, button'); if (closestEl && closestEl.classList.contains(style.panelHeading)) { - return closestEl; + return closestEl as HTMLElement; } return undefined; } +async function close(content: HTMLElement) { + const from = content.getBoundingClientRect().height; + + content.removeAttribute('expanded'); + content.setAttribute('aria-expanded', 'false'); + + await transitionHeight(content, { + from, + to: 0, + duration: 300, + }); + + content.style.height = ''; +} + +async function open(content: HTMLElement) { + const from = content.getBoundingClientRect().height; + + content.setAttribute('expanded', ''); + content.setAttribute('aria-expanded', 'true'); + + await transitionHeight(content, { + from, + duration: 300, + }); + + content.style.height = ''; +} + /** * A multi-panel view that the user can add any number of 'panels'. * 'a panel' consists of two elements. Even index element becomes heading, @@ -47,7 +77,7 @@ export default class MultiPanel extends HTMLElement { // Click event handler private _onClick(event: MouseEvent) { - const el = event.target as Element; + const el = event.target as HTMLElement; const heading = getClosestHeading(el); if (!heading) return; this._toggle(heading); @@ -108,41 +138,36 @@ export default class MultiPanel extends HTMLElement { } } - private _toggle(heading: Element) { + private _toggle(heading: HTMLElement) { if (!heading) return; - const content = heading.nextElementSibling; + const content = heading.nextElementSibling as HTMLElement; // if there is no content, nothing to expand if (!content) return; // toggle expanded and aria-expanded attributes if (content.hasAttribute('expanded')) { - content.removeAttribute('expanded'); - content.setAttribute('aria-expanded', 'false'); + close(content); } else { if (this.openOneOnly) this._closeAll(); - content.setAttribute('expanded', ''); - content.setAttribute('aria-expanded', 'true'); + open(content); } } private _closeAll(options: CloseAllOptions = {}): void { const { exceptFirst = false } = options; - let els = [...this.children].filter(el => el.matches('[expanded]')); + let els = [...this.children].filter(el => el.matches('[expanded]')) as HTMLElement[]; if (exceptFirst) { els = els.slice(1); } - for (const el of els) { - el.removeAttribute('expanded'); - el.setAttribute('aria-expanded', 'false'); - } + for (const el of els) close(el); } // children of multi-panel should always be even number (heading/content pair) private _childrenChange() { - let preserveTabIndex : boolean = false; + let preserveTabIndex = false; let heading = this.firstElementChild; while (heading) { diff --git a/src/components/compress/custom-els/MultiPanel/styles.css b/src/components/compress/custom-els/MultiPanel/styles.css index 286c5fa7..546a2a05 100644 --- a/src/components/compress/custom-els/MultiPanel/styles.css +++ b/src/components/compress/custom-els/MultiPanel/styles.css @@ -1,11 +1,10 @@ .panel-heading { - background:gray; + background: gray; } .panel-content { - height:0px; - overflow:scroll; - transition: height 1s; + height: 0px; + overflow: auto; } .panel-content[expanded] { - height:auto; + height: auto; } diff --git a/src/components/compress/style.scss b/src/components/compress/style.scss index 2232720e..f4a5c4c1 100644 --- a/src/components/compress/style.scss +++ b/src/components/compress/style.scss @@ -9,10 +9,6 @@ @media (min-width: 600px) { grid-template-columns: 1fr auto; - grid-template-rows: calc(100% - 75px); - } - - @media (min-width: 860px) { grid-template-rows: 100%; } } @@ -21,19 +17,23 @@ color: #fff; opacity: 0.9; font-size: 1.2rem; - max-height: 100%; display: flex; flex-flow: column; max-width: 400px; margin: 0 auto; width: calc(100% - 60px); max-height: calc(100% - 143px); + overflow: hidden; @media (min-width: 600px) { - max-height: none; + max-height: calc(100% - 75px); width: 300px; margin: 0; } + + @media (min-width: 860px) { + max-height: none; + } } .multi-panel { diff --git a/src/components/expander/index.tsx b/src/components/expander/index.tsx index 49099cc2..8099937a 100644 --- a/src/components/expander/index.tsx +++ b/src/components/expander/index.tsx @@ -1,6 +1,6 @@ import { h, Component, ComponentChild, ComponentChildren } from 'preact'; import * as style from './style.scss'; -import { linkRef } from '../../lib/initial-util'; +import { transitionHeight } from '../../lib/util'; interface Props { children: ComponentChildren; @@ -13,7 +13,6 @@ export default class Expander extends Component { state: State = { outgoingChildren: [], }; - private el?: HTMLDivElement; private lastElHeight: number = 0; componentWillReceiveProps(nextProps: Props) { @@ -32,10 +31,10 @@ export default class Expander extends Component { // Only interested if going from empty to not-empty, or not-empty to empty. if ((children[0] && nextChildren[0]) || (!children[0] && !nextChildren[0])) return; - this.lastElHeight = this.el!.getBoundingClientRect().height; + this.lastElHeight = this.base!.getBoundingClientRect().height; } - componentDidUpdate(previousProps: Props) { + async componentDidUpdate(previousProps: Props) { const children = this.props.children as ComponentChild[]; const previousChildren = previousProps.children as ComponentChild[]; @@ -43,37 +42,20 @@ export default class Expander extends Component { if ((children[0] && previousChildren[0]) || (!children[0] && !previousChildren[0])) return; // What height do we need to transition to? - this.el!.style.transition = 'none'; - this.el!.style.height = ''; - const newHeight = children[0] ? this.el!.getBoundingClientRect().height : 0; + this.base!.style.height = ''; + this.base!.style.overflow = 'hidden'; + const newHeight = children[0] ? this.base!.getBoundingClientRect().height : 0; - if (this.lastElHeight === newHeight) { - this.el!.style.transition = ''; - return; - } + await transitionHeight(this.base!, { + duration: 300, + from: this.lastElHeight, + to: newHeight, + }); - // Set the currently rendered height absolutely. - this.el!.style.height = this.lastElHeight + 'px'; - this.el!.style.transition = ''; - this.el!.style.overflow = 'hidden'; - // Force a style calc so the browser picks up the start value. - getComputedStyle(this.el!).height; - // Animate to the new height. - this.el!.style.height = newHeight + 'px'; - - const listener = () => { - // Unset the height & overflow, so element changes do the right thing. - this.el!.style.height = ''; - this.el!.style.overflow = ''; - this.el!.removeEventListener('transitionend', listener); - this.el!.removeEventListener('transitioncancel', listener); - if (this.state.outgoingChildren[0]) { - this.setState({ outgoingChildren: [] }); - } - }; - - this.el!.addEventListener('transitionend', listener); - this.el!.addEventListener('transitioncancel', listener); + // Unset the height & overflow, so element changes do the right thing. + this.base!.style.height = ''; + this.base!.style.overflow = ''; + if (this.state.outgoingChildren[0]) this.setState({ outgoingChildren: [] }); } render(props: Props, { outgoingChildren }: State) { @@ -81,10 +63,7 @@ export default class Expander extends Component { const childrenExiting = !children[0] && outgoingChildren[0]; return ( -
+
{children[0] ? children : outgoingChildren}
); diff --git a/src/components/expander/style.scss b/src/components/expander/style.scss index f72985b7..5eeaeeb4 100644 --- a/src/components/expander/style.scss +++ b/src/components/expander/style.scss @@ -1,7 +1,3 @@ -.expander { - transition: height 200ms ease-in-out; -} - .children-exiting { & > * { pointer-events: none; diff --git a/src/lib/util.ts b/src/lib/util.ts index cecf38fc..65c69be2 100644 --- a/src/lib/util.ts +++ b/src/lib/util.ts @@ -258,3 +258,43 @@ export function konami(): Promise { window.addEventListener('keydown', listener); }); } + +interface TransitionOptions { + from?: number; + to?: number; + duration?: number; + easing?: string; +} + +export async function transitionHeight(el: HTMLElement, opts: TransitionOptions): Promise { + const { + from = el.getBoundingClientRect().height, + to = el.getBoundingClientRect().height, + duration = 1000, + easing = 'ease-in-out', + } = opts; + + if (from === to || duration === 0) { + el.style.height = to + 'px'; + return; + } + + el.style.height = from + 'px'; + el.style.transition = `height ${duration}ms ${easing}`; + // Force a style calc so the browser picks up the start value. + getComputedStyle(el).height; + el.style.height = to + 'px'; + + return new Promise((resolve) => { + const listener = (event: Event) => { + if (event.target !== el) return; + el.style.transition = ''; + el.removeEventListener('transitionend', listener); + el.removeEventListener('transitioncancel', listener); + resolve(); + }; + + el.addEventListener('transitionend', listener); + el.addEventListener('transitioncancel', listener); + }); +}