diff --git a/frontend/code/components/revealer.ts b/frontend/code/components/revealer.ts index 4ad75d85..73574e39 100644 --- a/frontend/code/components/revealer.ts +++ b/frontend/code/components/revealer.ts @@ -18,21 +18,11 @@ export type RevealerState = ComponentState & { export class RevealerComponent extends ComponentBase { state: Required; - // Tracks the progress of the animation. Zero means fully collapsed, one - // means fully expanded. - private animationIsRunning: boolean = false; - private lastAnimationTick: number; - private openFractionBeforeEase: number = -1; // Initialized on first state update - private headerElement: HTMLElement; private labelElement: HTMLElement; private arrowElement: HTMLElement; - private contentInnerElement: HTMLElement; private contentOuterElement: HTMLElement; - - private headerScale: number; - private labelWidth: number; - private labelHeight: number; + private contentInnerElement: HTMLElement; private rippleInstance: RippleEffect; @@ -64,14 +54,14 @@ export class RevealerComponent extends ComponentBase { '.rio-revealer-arrow' ) as HTMLElement; - this.contentInnerElement = element.querySelector( - '.rio-revealer-content-inner' - ) as HTMLElement; - this.contentOuterElement = element.querySelector( '.rio-revealer-content-outer' ) as HTMLElement; + this.contentInnerElement = element.querySelector( + '.rio-revealer-content-inner' + ) as HTMLElement; + // Initialize them applyIcon(this.arrowElement, 'material/expand-more', 'currentColor'); @@ -85,22 +75,9 @@ export class RevealerComponent extends ComponentBase { this.rippleInstance.trigger(event); // Toggle the open state - this.state.is_open = !this.state.is_open; - - // Notify the backend this.setStateAndNotifyBackend({ - is_open: this.state.is_open, + is_open: !this.state.is_open, }); - - // Update the CSS - if (this.state.is_open) { - element.classList.add('rio-revealer-open'); - } else { - element.classList.remove('rio-revealer-open'); - } - - // Update the UI - this.startAnimationIfNotRunning(); }; // Color change on hover/leave @@ -145,24 +122,25 @@ export class RevealerComponent extends ComponentBase { ); // The text style defines the overall scale of the header + let headerScale: number; if (deltaState.header_style === 'heading1') { - this.headerScale = 2; + headerScale = 2; } else if (deltaState.header_style === 'heading2') { - this.headerScale = 1.5; + headerScale = 1.5; } else if (deltaState.header_style === 'heading3') { - this.headerScale = 1.2; + headerScale = 1.2; } else if (deltaState.header_style === 'text') { - this.headerScale = 1; + headerScale = 1; } else { - this.headerScale = deltaState.header_style.fontSize; + headerScale = deltaState.header_style.fontSize; } // Adapt the header's padding - let cssPadding = `${HEADER_PADDING * this.headerScale}rem`; + let cssPadding = `${HEADER_PADDING * headerScale}rem`; this.headerElement.style.padding = cssPadding; // Make the arrow match - let arrowSize = this.headerScale * 1.0; + let arrowSize = headerScale * 1.0; this.arrowElement.style.width = `${arrowSize}rem`; this.arrowElement.style.height = `${arrowSize}rem`; this.arrowElement.style.color = this.labelElement.style.color; @@ -170,61 +148,46 @@ export class RevealerComponent extends ComponentBase { // Expand / collapse if (deltaState.is_open !== undefined) { - // If this is the first state update, initialize the open fraction - if (this.openFractionBeforeEase === -1) { - this.openFractionBeforeEase = deltaState.is_open ? 1 : 0; - } - // Otherwise animate - else { - this.state.is_open = deltaState.is_open; - this.startAnimationIfNotRunning(); - } - - // Update the CSS - if (this.state.is_open) { - this.element.classList.add('rio-revealer-open'); + if (deltaState.is_open) { + this.animateOpen(); } else { - this.element.classList.remove('rio-revealer-open'); + this.animateClose(); } } } - /// If the animation is not yet running, start it. Does nothing otherwise. - /// This does not modify the state in any way. - startAnimationIfNotRunning() { - // If the animation is already running, do nothing. - if (this.animationIsRunning) { + private animateOpen(): void { + // Do nothing if already expanded + if (this.element.classList.contains('rio-revealer-open')) { return; } - // Start the animation - this.animationIsRunning = true; - this.lastAnimationTick = Date.now(); - requestAnimationFrame(() => this.animationWorker()); + // Update the CSS to trigger the expand animation + this.element.classList.add('rio-revealer-open'); + + // The components may currently be in flux due to a pending re-layout. If that + // is the case, reading the `scrollHeight` would lead to an incorrect value. + // Wait for the resize to finish before fetching it. + requestAnimationFrame(() => { + let contentHeight = this.contentInnerElement.scrollHeight; + let selfHeight = this.element.scrollHeight; + let headerHeight = this.headerElement.scrollHeight; + let targetHeight = Math.max( + contentHeight, + selfHeight - headerHeight + ); + + this.contentOuterElement.style.maxHeight = `${targetHeight}px`; + }); } - animationWorker() { - // Update state - let now = Date.now(); - let timePassed = now - this.lastAnimationTick; - this.lastAnimationTick = now; - - let direction = this.state.is_open ? 1 : -1; - this.openFractionBeforeEase = - this.openFractionBeforeEase + (direction * timePassed) / 200; - - // Clamp the open fraction - this.openFractionBeforeEase = Math.max( - 0, - Math.min(1, this.openFractionBeforeEase) - ); - - // If the animation is not yet finished, continue it. - let target = this.state.is_open ? 1 : 0; - if (this.openFractionBeforeEase === target) { - this.animationIsRunning = false; - } else { - requestAnimationFrame(() => this.animationWorker()); + private animateClose(): void { + // Do nothing if already collapsed + if (!this.element.classList.contains('rio-revealer-open')) { + return; } + + this.element.classList.remove('rio-revealer-open'); + this.contentOuterElement.style.maxHeight = '0'; } } diff --git a/frontend/css/style.scss b/frontend/css/style.scss index 69366206..36678b33 100644 --- a/frontend/css/style.scss +++ b/frontend/css/style.scss @@ -968,8 +968,6 @@ textarea:not(:placeholder-shown) ~ .rio-input-box-label, border-radius: var(--rio-global-corner-radius-small); transition: background-color 0.15s ease-out; - - overflow: hidden; // Needed by the ripple effect } .rio-revealer-header { @@ -993,22 +991,18 @@ textarea:not(:placeholder-shown) ~ .rio-input-box-label, transition: transform 0.25s ease-in-out; } -.rio-revealer-open .rio-revealer-arrow { +.rio-revealer-open > * > .rio-revealer-arrow { transform: rotate(0deg); } .rio-revealer-content-outer { - @include single-container(); - overflow: hidden; - transition: height 0.25s ease-in-out; - height: 0; -} - -.rio-revealer-open .rio-revealer-content-outer { - height: initial; flex-grow: 1; + + max-height: 0; + + transition: max-height 0.25s ease-in-out; } .rio-revealer-content-inner { @@ -1022,7 +1016,7 @@ textarea:not(:placeholder-shown) ~ .rio-input-box-label, transform 0.35s ease; } -.rio-revealer-open .rio-revealer-content-inner { +.rio-revealer-open > * > .rio-revealer-content-inner { opacity: 1; transform: translateY(0%); } @@ -1845,11 +1839,7 @@ textarea:not(:placeholder-shown) ~ .rio-input-box-label, } .rio-drawer-content-inner { - order: 1; -} - -.rio-drawer-content-inner > * { - position: relative !important; + @include single-container(); } .rio-drawer-knob { @@ -2807,6 +2797,8 @@ textarea:not(:placeholder-shown) ~ .rio-input-box-label, width: 100%; height: 100%; + overflow: hidden; + @include single-container(); }