mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-14 09:39:15 +00:00
Adding height animation to multi-panel
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import * as style from './styles.css';
|
import * as style from './styles.css';
|
||||||
|
import { transitionHeight } from '../../../../lib/util';
|
||||||
|
|
||||||
interface CloseAllOptions {
|
interface CloseAllOptions {
|
||||||
exceptFirst?: boolean;
|
exceptFirst?: boolean;
|
||||||
@@ -6,15 +7,44 @@ interface CloseAllOptions {
|
|||||||
|
|
||||||
const openOneOnlyAttr = 'open-one-only';
|
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
|
// Look for the child of multi-panel, but stop at interactive elements like links & buttons
|
||||||
const closestEl = el.closest('multi-panel > *, a, button');
|
const closestEl = el.closest('multi-panel > *, a, button');
|
||||||
if (closestEl && closestEl.classList.contains(style.panelHeading)) {
|
if (closestEl && closestEl.classList.contains(style.panelHeading)) {
|
||||||
return closestEl;
|
return closestEl as HTMLElement;
|
||||||
}
|
}
|
||||||
return undefined;
|
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 multi-panel view that the user can add any number of 'panels'.
|
||||||
* 'a panel' consists of two elements. Even index element becomes heading,
|
* 'a panel' consists of two elements. Even index element becomes heading,
|
||||||
@@ -47,7 +77,7 @@ export default class MultiPanel extends HTMLElement {
|
|||||||
|
|
||||||
// Click event handler
|
// Click event handler
|
||||||
private _onClick(event: MouseEvent) {
|
private _onClick(event: MouseEvent) {
|
||||||
const el = event.target as Element;
|
const el = event.target as HTMLElement;
|
||||||
const heading = getClosestHeading(el);
|
const heading = getClosestHeading(el);
|
||||||
if (!heading) return;
|
if (!heading) return;
|
||||||
this._toggle(heading);
|
this._toggle(heading);
|
||||||
@@ -108,41 +138,36 @@ export default class MultiPanel extends HTMLElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _toggle(heading: Element) {
|
private _toggle(heading: HTMLElement) {
|
||||||
if (!heading) return;
|
if (!heading) return;
|
||||||
const content = heading.nextElementSibling;
|
const content = heading.nextElementSibling as HTMLElement;
|
||||||
|
|
||||||
// if there is no content, nothing to expand
|
// if there is no content, nothing to expand
|
||||||
if (!content) return;
|
if (!content) return;
|
||||||
|
|
||||||
// toggle expanded and aria-expanded attributes
|
// toggle expanded and aria-expanded attributes
|
||||||
if (content.hasAttribute('expanded')) {
|
if (content.hasAttribute('expanded')) {
|
||||||
content.removeAttribute('expanded');
|
close(content);
|
||||||
content.setAttribute('aria-expanded', 'false');
|
|
||||||
} else {
|
} else {
|
||||||
if (this.openOneOnly) this._closeAll();
|
if (this.openOneOnly) this._closeAll();
|
||||||
content.setAttribute('expanded', '');
|
open(content);
|
||||||
content.setAttribute('aria-expanded', 'true');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _closeAll(options: CloseAllOptions = {}): void {
|
private _closeAll(options: CloseAllOptions = {}): void {
|
||||||
const { exceptFirst = false } = options;
|
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) {
|
if (exceptFirst) {
|
||||||
els = els.slice(1);
|
els = els.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const el of els) {
|
for (const el of els) close(el);
|
||||||
el.removeAttribute('expanded');
|
|
||||||
el.setAttribute('aria-expanded', 'false');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// children of multi-panel should always be even number (heading/content pair)
|
// children of multi-panel should always be even number (heading/content pair)
|
||||||
private _childrenChange() {
|
private _childrenChange() {
|
||||||
let preserveTabIndex : boolean = false;
|
let preserveTabIndex = false;
|
||||||
let heading = this.firstElementChild;
|
let heading = this.firstElementChild;
|
||||||
|
|
||||||
while (heading) {
|
while (heading) {
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
.panel-heading {
|
.panel-heading {
|
||||||
background:gray;
|
background: gray;
|
||||||
}
|
}
|
||||||
.panel-content {
|
.panel-content {
|
||||||
height:0px;
|
height: 0px;
|
||||||
overflow:scroll;
|
overflow: auto;
|
||||||
transition: height 1s;
|
|
||||||
}
|
}
|
||||||
.panel-content[expanded] {
|
.panel-content[expanded] {
|
||||||
height:auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,6 @@
|
|||||||
|
|
||||||
@media (min-width: 600px) {
|
@media (min-width: 600px) {
|
||||||
grid-template-columns: 1fr auto;
|
grid-template-columns: 1fr auto;
|
||||||
grid-template-rows: calc(100% - 75px);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 860px) {
|
|
||||||
grid-template-rows: 100%;
|
grid-template-rows: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,19 +17,23 @@
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
max-height: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
width: calc(100% - 60px);
|
width: calc(100% - 60px);
|
||||||
max-height: calc(100% - 143px);
|
max-height: calc(100% - 143px);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
@media (min-width: 600px) {
|
@media (min-width: 600px) {
|
||||||
max-height: none;
|
max-height: calc(100% - 75px);
|
||||||
width: 300px;
|
width: 300px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 860px) {
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.multi-panel {
|
.multi-panel {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { h, Component, ComponentChild, ComponentChildren } from 'preact';
|
import { h, Component, ComponentChild, ComponentChildren } from 'preact';
|
||||||
import * as style from './style.scss';
|
import * as style from './style.scss';
|
||||||
import { linkRef } from '../../lib/initial-util';
|
import { transitionHeight } from '../../lib/util';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: ComponentChildren;
|
children: ComponentChildren;
|
||||||
@@ -13,7 +13,6 @@ export default class Expander extends Component<Props, State> {
|
|||||||
state: State = {
|
state: State = {
|
||||||
outgoingChildren: [],
|
outgoingChildren: [],
|
||||||
};
|
};
|
||||||
private el?: HTMLDivElement;
|
|
||||||
private lastElHeight: number = 0;
|
private lastElHeight: number = 0;
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps: Props) {
|
componentWillReceiveProps(nextProps: Props) {
|
||||||
@@ -32,10 +31,10 @@ export default class Expander extends Component<Props, State> {
|
|||||||
|
|
||||||
// Only interested if going from empty to not-empty, or not-empty to empty.
|
// Only interested if going from empty to not-empty, or not-empty to empty.
|
||||||
if ((children[0] && nextChildren[0]) || (!children[0] && !nextChildren[0])) return;
|
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 children = this.props.children as ComponentChild[];
|
||||||
const previousChildren = previousProps.children as ComponentChild[];
|
const previousChildren = previousProps.children as ComponentChild[];
|
||||||
|
|
||||||
@@ -43,37 +42,20 @@ export default class Expander extends Component<Props, State> {
|
|||||||
if ((children[0] && previousChildren[0]) || (!children[0] && !previousChildren[0])) return;
|
if ((children[0] && previousChildren[0]) || (!children[0] && !previousChildren[0])) return;
|
||||||
|
|
||||||
// What height do we need to transition to?
|
// What height do we need to transition to?
|
||||||
this.el!.style.transition = 'none';
|
this.base!.style.height = '';
|
||||||
this.el!.style.height = '';
|
this.base!.style.overflow = 'hidden';
|
||||||
const newHeight = children[0] ? this.el!.getBoundingClientRect().height : 0;
|
const newHeight = children[0] ? this.base!.getBoundingClientRect().height : 0;
|
||||||
|
|
||||||
if (this.lastElHeight === newHeight) {
|
await transitionHeight(this.base!, {
|
||||||
this.el!.style.transition = '';
|
duration: 300,
|
||||||
return;
|
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.
|
// Unset the height & overflow, so element changes do the right thing.
|
||||||
this.el!.style.height = '';
|
this.base!.style.height = '';
|
||||||
this.el!.style.overflow = '';
|
this.base!.style.overflow = '';
|
||||||
this.el!.removeEventListener('transitionend', listener);
|
if (this.state.outgoingChildren[0]) this.setState({ outgoingChildren: [] });
|
||||||
this.el!.removeEventListener('transitioncancel', listener);
|
|
||||||
if (this.state.outgoingChildren[0]) {
|
|
||||||
this.setState({ outgoingChildren: [] });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.el!.addEventListener('transitionend', listener);
|
|
||||||
this.el!.addEventListener('transitioncancel', listener);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render(props: Props, { outgoingChildren }: State) {
|
render(props: Props, { outgoingChildren }: State) {
|
||||||
@@ -81,10 +63,7 @@ export default class Expander extends Component<Props, State> {
|
|||||||
const childrenExiting = !children[0] && outgoingChildren[0];
|
const childrenExiting = !children[0] && outgoingChildren[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div class={childrenExiting ? style.childrenExiting : ''}>
|
||||||
ref={linkRef(this, 'el')}
|
|
||||||
class={`${style.expander} ${childrenExiting ? style.childrenExiting : ''}`}
|
|
||||||
>
|
|
||||||
{children[0] ? children : outgoingChildren}
|
{children[0] ? children : outgoingChildren}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
.expander {
|
|
||||||
transition: height 200ms ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.children-exiting {
|
.children-exiting {
|
||||||
& > * {
|
& > * {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|||||||
@@ -258,3 +258,43 @@ export function konami(): Promise<void> {
|
|||||||
window.addEventListener('keydown', listener);
|
window.addEventListener('keydown', listener);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TransitionOptions {
|
||||||
|
from?: number;
|
||||||
|
to?: number;
|
||||||
|
duration?: number;
|
||||||
|
easing?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function transitionHeight(el: HTMLElement, opts: TransitionOptions): Promise<void> {
|
||||||
|
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<void>((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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user