@ionic/core
Version:
Base components for Ionic
200 lines (199 loc) • 8.37 kB
JavaScript
/*!
* (C) Ionic http://ionicframework.com - MIT License
*/
import { readTask, writeTask } from "@stencil/core";
import { clamp } from "../../utils/helpers";
const TRANSITION = 'all 0.2s ease-in-out';
export const cloneElement = (tagName) => {
const getCachedEl = document.querySelector(`${tagName}.ion-cloned-element`);
if (getCachedEl !== null) {
return getCachedEl;
}
const clonedEl = document.createElement(tagName);
clonedEl.classList.add('ion-cloned-element');
clonedEl.style.setProperty('display', 'none');
document.body.appendChild(clonedEl);
return clonedEl;
};
export const createHeaderIndex = (headerEl) => {
if (!headerEl) {
return;
}
const toolbars = headerEl.querySelectorAll('ion-toolbar');
return {
el: headerEl,
toolbars: Array.from(toolbars).map((toolbar) => {
const ionTitleEl = toolbar.querySelector('ion-title');
return {
el: toolbar,
background: toolbar.shadowRoot.querySelector('.toolbar-background'),
ionTitleEl,
innerTitleEl: ionTitleEl ? ionTitleEl.shadowRoot.querySelector('.toolbar-title') : null,
ionButtonsEl: Array.from(toolbar.querySelectorAll('ion-buttons')),
};
}),
};
};
export const handleContentScroll = (scrollEl, scrollHeaderIndex, contentEl) => {
readTask(() => {
const scrollTop = scrollEl.scrollTop;
const scale = clamp(1, 1 + -scrollTop / 500, 1.1);
// Native refresher should not cause titles to scale
const nativeRefresher = contentEl.querySelector('ion-refresher.refresher-native');
if (nativeRefresher === null) {
writeTask(() => {
scaleLargeTitles(scrollHeaderIndex.toolbars, scale);
});
}
});
};
export const setToolbarBackgroundOpacity = (headerEl, opacity) => {
/**
* Fading in the backdrop opacity
* should happen after the large title
* has collapsed, so it is handled
* by handleHeaderFade()
*/
if (headerEl.collapse === 'fade') {
return;
}
if (opacity === undefined) {
headerEl.style.removeProperty('--opacity-scale');
}
else {
headerEl.style.setProperty('--opacity-scale', opacity.toString());
}
};
const handleToolbarBorderIntersection = (ev, mainHeaderIndex, scrollTop) => {
if (!ev[0].isIntersecting) {
return;
}
/**
* There is a bug in Safari where overflow scrolling on a non-body element
* does not always reset the scrollTop position to 0 when letting go. It will
* set to 1 once the rubber band effect has ended. This causes the background to
* appear slightly on certain app setups.
*
* Additionally, we check if user is rubber banding (scrolling is negative)
* as this can mean they are using pull to refresh. Once the refresher starts,
* the content is transformed which can cause the intersection observer to erroneously
* fire here as well.
*/
const scale = ev[0].intersectionRatio > 0.9 || scrollTop <= 0 ? 0 : ((1 - ev[0].intersectionRatio) * 100) / 75;
setToolbarBackgroundOpacity(mainHeaderIndex.el, scale === 1 ? undefined : scale);
};
/**
* If toolbars are intersecting, hide the scrollable toolbar content
* and show the primary toolbar content. If the toolbars are not intersecting,
* hide the primary toolbar content and show the scrollable toolbar content
*/
export const handleToolbarIntersection = (ev, // TODO(FW-2832): type (IntersectionObserverEntry[] triggers errors which should be sorted)
mainHeaderIndex, scrollHeaderIndex, scrollEl) => {
writeTask(() => {
const scrollTop = scrollEl.scrollTop;
handleToolbarBorderIntersection(ev, mainHeaderIndex, scrollTop);
const event = ev[0];
const intersection = event.intersectionRect;
const intersectionArea = intersection.width * intersection.height;
const rootArea = event.rootBounds.width * event.rootBounds.height;
const isPageHidden = intersectionArea === 0 && rootArea === 0;
const leftDiff = Math.abs(intersection.left - event.boundingClientRect.left);
const rightDiff = Math.abs(intersection.right - event.boundingClientRect.right);
const isPageTransitioning = intersectionArea > 0 && (leftDiff >= 5 || rightDiff >= 5);
if (isPageHidden || isPageTransitioning) {
return;
}
if (event.isIntersecting) {
setHeaderActive(mainHeaderIndex, false);
setHeaderActive(scrollHeaderIndex);
}
else {
/**
* There is a bug with IntersectionObserver on Safari
* where `event.isIntersecting === false` when cancelling
* a swipe to go back gesture. Checking the intersection
* x, y, width, and height provides a workaround. This bug
* does not happen when using Safari + Web Animations,
* only Safari + CSS Animations.
*/
const hasValidIntersection = (intersection.x === 0 && intersection.y === 0) || (intersection.width !== 0 && intersection.height !== 0);
if (hasValidIntersection && scrollTop > 0) {
setHeaderActive(mainHeaderIndex);
setHeaderActive(scrollHeaderIndex, false);
setToolbarBackgroundOpacity(mainHeaderIndex.el);
}
}
});
};
export const setHeaderActive = (headerIndex, active = true) => {
const headerEl = headerIndex.el;
const toolbars = headerIndex.toolbars;
const ionTitles = toolbars.map((toolbar) => toolbar.ionTitleEl);
if (active) {
headerEl.classList.remove('header-collapse-condense-inactive');
ionTitles.forEach((ionTitle) => {
if (ionTitle) {
ionTitle.removeAttribute('aria-hidden');
}
});
}
else {
headerEl.classList.add('header-collapse-condense-inactive');
/**
* The small title should only be accessed by screen readers
* when the large title collapses into the small title due
* to scrolling.
*
* Originally, the header was given `aria-hidden="true"`
* but this caused issues with screen readers not being
* able to access any focusable elements within the header.
*/
ionTitles.forEach((ionTitle) => {
if (ionTitle) {
ionTitle.setAttribute('aria-hidden', 'true');
}
});
}
};
export const scaleLargeTitles = (toolbars = [], scale = 1, transition = false) => {
toolbars.forEach((toolbar) => {
const ionTitle = toolbar.ionTitleEl;
const titleDiv = toolbar.innerTitleEl;
if (!ionTitle || ionTitle.size !== 'large') {
return;
}
titleDiv.style.transition = transition ? TRANSITION : '';
titleDiv.style.transform = `scale3d(${scale}, ${scale}, 1)`;
});
};
export const handleHeaderFade = (scrollEl, baseEl, condenseHeader) => {
readTask(() => {
const scrollTop = scrollEl.scrollTop;
const baseElHeight = baseEl.clientHeight;
const fadeStart = condenseHeader ? condenseHeader.clientHeight : 0;
/**
* If we are using fade header with a condense
* header, then the toolbar backgrounds should
* not begin to fade in until the condense
* header has fully collapsed.
*
* Additionally, the main content should not
* overflow out of the container until the
* condense header has fully collapsed. When
* using just the condense header the content
* should overflow out of the container.
*/
if (condenseHeader !== null && scrollTop < fadeStart) {
baseEl.style.setProperty('--opacity-scale', '0');
scrollEl.style.setProperty('clip-path', `inset(${baseElHeight}px 0px 0px 0px)`);
return;
}
const distanceToStart = scrollTop - fadeStart;
const fadeDuration = 10;
const scale = clamp(0, distanceToStart / fadeDuration, 1);
writeTask(() => {
scrollEl.style.removeProperty('clip-path');
baseEl.style.setProperty('--opacity-scale', scale.toString());
});
});
};