refactor(scroll): merge into ion-content

This commit is contained in:
Manu Mtz.-Almeida
2018-08-10 00:00:18 +02:00
parent a4571bfa4f
commit 997cda0667
31 changed files with 548 additions and 912 deletions

View File

@@ -44,6 +44,7 @@ A list of the breaking changes introduced to each component in Ionic Angular v4.
- [Radio](#radio)
- [Range](#range)
- [Refresher](#refresher)
- [Scroll](#scroll)
- [Segment](#segment)
- [Select](#select)
- [Spinner](#spinner)
@@ -1113,6 +1114,23 @@ The `enabled` property (with a default value of `true`) has been renamed to `dis
</ion-refresher>
```
## Scroll
`ion-scroll` has been removed, fortunatelly `ion-content` can work as a drop-in replacement:
```diff
- <ion-scroll scrollX="true">
+ <ion-content scrollX="true">
```
Another very good option is to style a `div` to become scrollable using CSS:
```css
div.scrollable {
overflow: scroll
}
```
## Segment

View File

@@ -619,15 +619,35 @@ declare global {
* If true, the content will scroll behind the headers and footers. This effect can easily be seen by setting the toolbar to transparent.
*/
'fullscreen': boolean;
'getScrollElement': () => HTMLIonScrollElement;
'getScrollElement': () => HTMLElement;
/**
* By default `ion-content` uses an `ion-scroll` under the hood to implement scrolling, if you want to disable the content scrolling for a given page, set this property to `false`.
* Scroll by a specified X/Y distance in the component
*/
'scrollEnabled': boolean;
'scrollByPoint': (x: number, y: number, duration: number) => Promise<any>;
/**
* Because of performance reasons, ionScroll events are disabled by default, in order to enable them and start listening from (ionScroll), set this property to `true`.
*/
'scrollEvents': boolean;
/**
* Scroll to the bottom of the component
*/
'scrollToBottom': (duration?: number) => Promise<void>;
/**
* Scroll to a specified X/Y location in the component
*/
'scrollToPoint': (x: number | undefined, y: number | undefined, duration?: number) => Promise<void>;
/**
* Scroll to the top of the component
*/
'scrollToTop': (duration?: number) => Promise<void>;
/**
* If you want to enable the content scrolling in the X axis, set this property to `true`.
*/
'scrollX': boolean;
/**
* If you want to disable the content scrolling in the Y axis, set this property to `false`.
*/
'scrollY': boolean;
}
interface IonDatetime {
@@ -1946,37 +1966,6 @@ declare global {
}
interface IonScroll {
/**
* If true and the content does not cause an overflow scroll, the scroll interaction will cause a bounce. If the content exceeds the bounds of ionScroll, nothing will change. Note, the does not disable the system bounce on iOS. That is an OS level setting.
*/
'forceOverscroll': boolean;
/**
* The mode for component.
*/
'mode': Mode;
/**
* Scroll by a specified X/Y distance in the component
*/
'scrollByPoint': (x: number, y: number, duration: number) => Promise<any>;
/**
* If true, the component will emit scroll events.
*/
'scrollEvents': boolean;
/**
* Scroll to the bottom of the component
*/
'scrollToBottom': (duration: number) => Promise<void>;
/**
* Scroll to a specified X/Y location in the component
*/
'scrollToPoint': (x: number, y: number, duration: number) => Promise<void>;
/**
* Scroll to the top of the component
*/
'scrollToTop': (duration: number) => Promise<void>;
}
interface IonSearchbar {
/**
* If true, enable searchbar animation. Default `false`.
@@ -2398,10 +2387,6 @@ declare global {
* Set the position of the tabbar, relative to the content. Available options: `"top"`, `"bottom"`.
*/
'placement': TabbarPlacement;
/**
* If true, the tabs will be scrollable when there are enough tabs to overflow the width of the screen.
*/
'scrollable': boolean;
/**
* The selected tab component
*/
@@ -2434,10 +2419,6 @@ declare global {
* A unique name for the tabs.
*/
'name': string;
/**
* If true, the tabs will be scrollable when there are enough tabs to overflow the width of the screen.
*/
'scrollable': boolean;
/**
* Index or the Tab instance, of the tab to select.
*/
@@ -3342,14 +3323,6 @@ declare global {
};
interface HTMLIonScrollElement extends StencilComponents.IonScroll, HTMLStencilElement {}
var HTMLIonScrollElement: {
prototype: HTMLIonScrollElement;
new (): HTMLIonScrollElement;
};
interface HTMLIonSearchbarElement extends StencilComponents.IonSearchbar, HTMLStencilElement {}
var HTMLIonSearchbarElement: {
@@ -3630,7 +3603,6 @@ declare global {
'ion-router-outlet': JSXElements.IonRouterOutletAttributes;
'ion-router': JSXElements.IonRouterAttributes;
'ion-row': JSXElements.IonRowAttributes;
'ion-scroll': JSXElements.IonScrollAttributes;
'ion-searchbar': JSXElements.IonSearchbarAttributes;
'ion-segment-button': JSXElements.IonSegmentButtonAttributes;
'ion-segment': JSXElements.IonSegmentAttributes;
@@ -4218,13 +4190,29 @@ declare global {
*/
'fullscreen'?: boolean;
/**
* By default `ion-content` uses an `ion-scroll` under the hood to implement scrolling, if you want to disable the content scrolling for a given page, set this property to `false`.
* Emitted while scrolling. This event is disabled by default. Look at the property: `scrollEvents`
*/
'scrollEnabled'?: boolean;
'onIonScroll'?: (event: CustomEvent<ScrollDetail>) => void;
/**
* Emitted when the scroll has ended.
*/
'onIonScrollEnd'?: (event: CustomEvent<ScrollBaseDetail>) => void;
/**
* Emitted when the scroll has started.
*/
'onIonScrollStart'?: (event: CustomEvent<ScrollBaseDetail>) => void;
/**
* Because of performance reasons, ionScroll events are disabled by default, in order to enable them and start listening from (ionScroll), set this property to `true`.
*/
'scrollEvents'?: boolean;
/**
* If you want to enable the content scrolling in the X axis, set this property to `true`.
*/
'scrollX'?: boolean;
/**
* If you want to disable the content scrolling in the Y axis, set this property to `false`.
*/
'scrollY'?: boolean;
}
export interface IonDatetimeAttributes extends HTMLAttributes {
@@ -5510,33 +5498,6 @@ declare global {
}
export interface IonScrollAttributes extends HTMLAttributes {
/**
* If true and the content does not cause an overflow scroll, the scroll interaction will cause a bounce. If the content exceeds the bounds of ionScroll, nothing will change. Note, the does not disable the system bounce on iOS. That is an OS level setting.
*/
'forceOverscroll'?: boolean;
/**
* The mode for component.
*/
'mode'?: Mode;
/**
* Emitted while scrolling. This event is disabled by default. Look at the property: `scrollEvents`
*/
'onIonScroll'?: (event: CustomEvent<ScrollDetail>) => void;
/**
* Emitted when the scroll has ended.
*/
'onIonScrollEnd'?: (event: CustomEvent<ScrollBaseDetail>) => void;
/**
* Emitted when the scroll has started.
*/
'onIonScrollStart'?: (event: CustomEvent<ScrollBaseDetail>) => void;
/**
* If true, the component will emit scroll events.
*/
'scrollEvents'?: boolean;
}
export interface IonSearchbarAttributes extends HTMLAttributes {
/**
* If true, enable searchbar animation. Default `false`.
@@ -6033,10 +5994,6 @@ declare global {
* Set the position of the tabbar, relative to the content. Available options: `"top"`, `"bottom"`.
*/
'placement'?: TabbarPlacement;
/**
* If true, the tabs will be scrollable when there are enough tabs to overflow the width of the screen.
*/
'scrollable'?: boolean;
/**
* The selected tab component
*/
@@ -6076,10 +6033,6 @@ declare global {
* Emitted when the navigation will load a component.
*/
'onIonNavWillLoad'?: (event: CustomEvent<void>) => void;
/**
* If true, the tabs will be scrollable when there are enough tabs to overflow the width of the screen.
*/
'scrollable'?: boolean;
/**
* If true, the tabbar will be hidden. Defaults to `false`.
*/
@@ -6470,7 +6423,6 @@ declare global {
'ion-router-outlet': HTMLIonRouterOutletElement
'ion-router': HTMLIonRouterElement
'ion-row': HTMLIonRowElement
'ion-scroll': HTMLIonScrollElement
'ion-searchbar': HTMLIonSearchbarElement
'ion-segment-button': HTMLIonSegmentButtonElement
'ion-segment': HTMLIonSegmentElement
@@ -6576,7 +6528,6 @@ declare global {
'ion-router-outlet': HTMLIonRouterOutletElement;
'ion-router': HTMLIonRouterElement;
'ion-row': HTMLIonRowElement;
'ion-scroll': HTMLIonScrollElement;
'ion-searchbar': HTMLIonSearchbarElement;
'ion-segment-button': HTMLIonSegmentButtonElement;
'ion-segment': HTMLIonSegmentElement;

View File

@@ -19,14 +19,9 @@
flex: 1;
width: 100%;
height: 100%;
/* stylelint-disable */
/* TODO: find a better solution in padding.css, that does not require !important, */
margin: 0 !important;
padding: 0 !important;
margin: 0 !important;
/* stylelint-enable */
background-color: #{current-color(base)};
@@ -35,23 +30,64 @@
contain: layout size style;
}
:host(.scroll-disabled),
ion-scroll {
:host(.ion-color-outer),
:host(.outer-content) {
--ion-color-base: #{$background-color-step-50};
}
.inner-scroll {
@include position(
calc(var(--offset-top) * -1), 0,
calc(var(--offset-bottom) * -1), 0
);
@include padding(
calc(var(--padding-top) + var(--offset-top)),
var(--padding-end),
calc(var(--padding-bottom) + var(--keyboard-offset) + var(--offset-bottom)),
var(--padding-start)
);
position: absolute;
box-sizing: border-box;
overflow: hidden;
}
:host(.ion-color-outer),
:host(.outer-content) {
--ion-color-base: #{$background-color-step-50};
.scroll-x.scroll.y {
-webkit-overflow-scrolling: touch;
will-change: scroll-position;
}
:host(.content-size) ion-scroll {
position: relative;
contain: none;
.scroll-y {
overflow-y: auto;
}
.scroll-x {
overflow-x: auto;
}
.scroll-enabled {
overflow-x: hidden;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
will-change: scroll-position;
}
.overscroll::before,
.overscroll::after {
position: absolute;
width: 1px;
height: 1px;
content: "";
}
.overscroll::before {
bottom: -1px;
}
.overscroll::after {
top: -1px;
}

View File

@@ -1,7 +1,7 @@
import { Component, Element, Listen, Method, Prop, QueueApi } from '@stencil/core';
import { Component, Element, Event, EventEmitter, Listen, Method, Prop, QueueApi } from '@stencil/core';
import { Color, Config, Mode } from '../../interface';
import { createColorClasses, hostContext } from '../../utils/theme';
import { Color, Config, Mode, ScrollBaseDetail, ScrollDetail } from '../../interface';
import { createColorClasses } from '../../utils/theme';
@Component({
tag: 'ion-content',
@@ -13,9 +13,35 @@ import { createColorClasses, hostContext } from '../../utils/theme';
})
export class Content {
private watchDog: any;
private isScrolling = false;
private lastScroll = 0;
private queued = false;
private cTop = -1;
private cBottom = -1;
private scrollEl?: HTMLIonScrollElement;
private scrollEl!: HTMLElement;
// Detail is used in a hot loop in the scroll event, by allocating it here
// V8 will be able to inline any read/write to it since it's a monomorphic class.
// https://mrale.ph/blog/2015/01/11/whats-up-with-monomorphism.html
private detail: ScrollDetail = {
scrollTop: 0,
scrollLeft: 0,
type: 'scroll',
event: undefined!,
startX: 0,
startY: 0,
startTimeStamp: 0,
currentX: 0,
currentY: 0,
velocityX: 0,
velocityY: 0,
deltaX: 0,
deltaY: 0,
timeStamp: 0,
data: undefined,
isScrolling: true,
};
mode!: Mode;
@Prop() color?: Color;
@@ -24,6 +50,7 @@ export class Content {
@Prop({ context: 'config' }) config!: Config;
@Prop({ context: 'queue' }) queue!: QueueApi;
@Prop({ context: 'window' }) win!: Window;
/**
* If true, the content will scroll behind the headers
@@ -37,13 +64,17 @@ export class Content {
* If the content exceeds the bounds of ionContent, nothing will change.
* Note, the does not disable the system bounce on iOS. That is an OS level setting.
*/
@Prop() forceOverscroll?: boolean;
@Prop({ mutable: true }) forceOverscroll?: boolean;
/**
* By default `ion-content` uses an `ion-scroll` under the hood to implement scrolling,
* if you want to disable the content scrolling for a given page, set this property to `false`.
* If you want to enable the content scrolling in the X axis, set this property to `true`.
*/
@Prop() scrollEnabled = true;
@Prop() scrollX = false;
/**
* If you want to disable the content scrolling in the Y axis, set this property to `false`.
*/
@Prop() scrollY = true;
/**
* Because of performance reasons, ionScroll events are disabled by default, in order to enable them
@@ -51,24 +82,44 @@ export class Content {
*/
@Prop() scrollEvents = false;
/**
* Emitted when the scroll has started.
*/
@Event() ionScrollStart!: EventEmitter<ScrollBaseDetail>;
/**
* Emitted while scrolling. This event is disabled by default.
* Look at the property: `scrollEvents`
*/
@Event() ionScroll!: EventEmitter<ScrollDetail>;
/**
* Emitted when the scroll has ended.
*/
@Event() ionScrollEnd!: EventEmitter<ScrollBaseDetail>;
@Listen('body:ionNavDidChange')
onNavChanged() {
this.resize();
}
componentWillLoad() {
if (this.forceOverscroll === undefined) {
this.forceOverscroll = this.mode === 'ios' && ('ontouchstart' in this.win);
}
}
componentDidLoad() {
this.resize();
}
@Method()
getScrollElement(): HTMLIonScrollElement {
return this.scrollEl!;
componentDidUnload() {
if (this.watchDog) {
clearInterval(this.watchDog);
}
}
private resize() {
if (!this.scrollEl) {
return;
}
if (this.fullscreen) {
this.queue.read(this.readDimensions.bind(this));
} else if (this.cTop !== 0 || this.cBottom !== 0) {
@@ -89,35 +140,173 @@ export class Content {
}
}
private onScroll(ev: UIEvent) {
const timeStamp = Date.now();
const didStart = !this.isScrolling;
this.lastScroll = timeStamp;
if (didStart) {
this.onScrollStart();
}
if (!this.queued && this.scrollEvents) {
this.queued = true;
this.queue.read(ts => {
this.queued = false;
this.detail.event = ev;
updateScrollDetail(this.detail, this.el, ts, didStart);
this.ionScroll.emit(this.detail);
});
}
}
@Method()
getScrollElement(): HTMLElement {
return this.scrollEl;
}
/**
* Scroll to the top of the component
*/
@Method()
scrollToTop(duration = 100): Promise<void> {
return this.scrollToPoint(0, undefined, duration);
}
/**
* Scroll to the bottom of the component
*/
@Method()
scrollToBottom(duration = 100): Promise<void> {
const y = this.scrollEl.scrollHeight - this.scrollEl.clientHeight;
return this.scrollToPoint(undefined, y, duration);
}
/**
* Scroll by a specified X/Y distance in the component
*/
@Method()
scrollByPoint(x: number, y: number, duration: number): Promise<any> {
return this.scrollToPoint(x + this.scrollEl.scrollLeft, y + this.scrollEl.scrollTop, duration);
}
/**
* Scroll to a specified X/Y location in the component
*/
@Method()
async scrollToPoint(x: number | undefined, y: number | undefined, duration = 0): Promise<void> {
const el = this.scrollEl;
if (duration < 32) {
if (y != null) {
el.scrollTop = y;
}
if (x != null) {
el.scrollLeft = x;
}
return;
}
let resolve!: () => void;
let startTime: number;
const promise = new Promise<void>(r => resolve = r);
const fromY = el.scrollTop;
const fromX = el.scrollLeft;
const deltaY = y != null ? y - fromY : 0;
const deltaX = x != null ? x - fromX : 0;
// scroll loop
const step = (timeStamp: number) => {
let linearTime = Math.min(1, ((timeStamp - startTime) / duration));
const easedT = (--linearTime) * linearTime * linearTime + 1;
if (deltaY !== 0) {
el.scrollTop = (easedT * deltaY) + fromY;
}
if (deltaX !== 0) {
el.scrollLeft = Math.floor((easedT * deltaX) + fromX);
}
if (easedT < 1) {
// do not use DomController here
// must use nativeRaf in order to fire in the next frame
// TODO: remove as any
requestAnimationFrame(step);
} else {
this.isScrolling = false;
resolve();
}
};
// start scroll loop
this.isScrolling = true;
// chill out for a frame first
requestAnimationFrame(ts => {
startTime = ts;
step(ts);
});
return promise;
}
private onScrollStart() {
this.isScrolling = true;
this.ionScrollStart.emit({
isScrolling: true
});
if (this.watchDog) {
clearInterval(this.watchDog);
}
// watchdog
this.watchDog = setInterval(() => {
if (this.lastScroll < Date.now() - 120) {
this.onScrollEnd();
}
}, 100);
}
private onScrollEnd() {
clearInterval(this.watchDog);
this.watchDog = null;
this.isScrolling = false;
this.ionScrollEnd.emit({
isScrolling: false
});
}
hostData() {
return {
class: {
...createColorClasses(this.color),
'content-size': hostContext('ion-popover', this.el),
'scroll-disabled': !this.scrollEnabled,
'overscroll': this.forceOverscroll,
},
style: {
'--offset-top': `${this.cTop}px`,
'--offset-bottom': `${this.cBottom}px`,
}
};
}
render() {
const { scrollX, scrollY, forceOverscroll } = this;
const scrollEnabled = scrollX || scrollY;
this.resize();
return [
this.scrollEnabled ? (
<ion-scroll
ref={el => this.scrollEl = el as any}
mode={this.mode}
scrollEvents={this.scrollEvents}
forceOverscroll={this.forceOverscroll}
style={{
'top': `${-this.cTop}px`,
'bottom': `${-this.cBottom}px`,
'--offset-top': `${this.cTop}px`,
'--offset-bottom': `${this.cBottom}px`,
}}>
<slot></slot>
</ion-scroll>
) : <slot></slot>,
<div
class={{
'inner-scroll': true,
'scroll-x': scrollX,
'scroll-y': scrollY,
'scroll-enabled': scrollEnabled,
'overscroll': scrollEnabled && !!forceOverscroll
}}
ref={el => this.scrollEl = el!}
onScroll={ev => this.onScroll(ev)}>
<slot></slot>
</div>,
<slot name="fixed"></slot>
];
}
@@ -146,3 +335,37 @@ function getPageElement(el: HTMLElement) {
}
return getParentElement(el);
}
// ******** DOM READ ****************
function updateScrollDetail(
detail: ScrollDetail,
el: HTMLElement,
timestamp: number,
didStart: boolean
) {
const prevX = detail.currentX;
const prevY = detail.currentY;
const prevT = detail.timeStamp;
const currentX = el.scrollLeft;
const currentY = el.scrollTop;
if (didStart) {
// remember the start positions
detail.startTimeStamp = timestamp;
detail.startX = currentX;
detail.startY = currentY;
detail.velocityX = detail.velocityY = 0;
}
detail.timeStamp = timestamp;
detail.currentX = detail.scrollLeft = currentX;
detail.currentY = detail.scrollTop = currentY;
detail.deltaX = currentX - detail.startX;
detail.deltaY = currentY - detail.startY;
const timeDelta = timestamp - prevT;
if (timeDelta > 0 && timeDelta < 100) {
const velocityX = (currentX - prevX) / timeDelta;
const velocityY = (currentY - prevY) / timeDelta;
detail.velocityX = velocityX * 0.7 + detail.velocityX * 0.3;
detail.velocityY = velocityY * 0.7 + detail.velocityY * 0.3;
}
}

View File

@@ -32,14 +32,6 @@ and footers. This effect can easily be seen by setting the toolbar
to transparent.
#### scrollEnabled
boolean
By default `ion-content` uses an `ion-scroll` under the hood to implement scrolling,
if you want to disable the content scrolling for a given page, set this property to `false`.
#### scrollEvents
boolean
@@ -48,6 +40,20 @@ Because of performance reasons, ionScroll events are disabled by default, in ord
and start listening from (ionScroll), set this property to `true`.
#### scrollX
boolean
If you want to enable the content scrolling in the X axis, set this property to `true`.
#### scrollY
boolean
If you want to disable the content scrolling in the Y axis, set this property to `false`.
## Attributes
#### color
@@ -73,14 +79,6 @@ and footers. This effect can easily be seen by setting the toolbar
to transparent.
#### scroll-enabled
boolean
By default `ion-content` uses an `ion-scroll` under the hood to implement scrolling,
if you want to disable the content scrolling for a given page, set this property to `false`.
#### scroll-events
boolean
@@ -89,11 +87,63 @@ Because of performance reasons, ionScroll events are disabled by default, in ord
and start listening from (ionScroll), set this property to `true`.
#### scroll-x
boolean
If you want to enable the content scrolling in the X axis, set this property to `true`.
#### scroll-y
boolean
If you want to disable the content scrolling in the Y axis, set this property to `false`.
## Events
#### ionScroll
Emitted while scrolling. This event is disabled by default.
Look at the property: `scrollEvents`
#### ionScrollEnd
Emitted when the scroll has ended.
#### ionScrollStart
Emitted when the scroll has started.
## Methods
#### getScrollElement()
#### scrollByPoint()
Scroll by a specified X/Y distance in the component
#### scrollToBottom()
Scroll to the bottom of the component
#### scrollToPoint()
Scroll to a specified X/Y location in the component
#### scrollToTop()
Scroll to the top of the component
----------------------------------------------

View File

@@ -23,7 +23,7 @@
<div style="display: flex; flex: 1">
<ion-content padding style="text-align: center; flex: 2" id="content">
<f></f>
<f style="height: 50%"></f>
<ion-button onclick="toggleFullscreen()">Toggle content.fullscreen</ion-button>
<p>
<ion-button onclick="toggleDisplay('header')" color="secondary">Toggle header</ion-button>
@@ -104,10 +104,6 @@
animation: ani1 5s infinite;
}
#content2 ion-scroll {
background: black;
}
@keyframes ani1 {
0% {
transform: translateX(0)

View File

@@ -8,7 +8,7 @@ export class InfiniteScroll {
private thrPx = 0;
private thrPc = 0;
private scrollEl?: HTMLIonScrollElement;
private scrollEl?: HTMLElement;
private didFire = false;
private isBusy = false;
@@ -81,9 +81,13 @@ export class InfiniteScroll {
}
this.thresholdChanged(this.threshold);
this.enableScrollEvents(!this.disabled);
if (this.position === 'top') {
this.queue.write(() => this.scrollEl && this.scrollEl.scrollToBottom(0));
}
// if (this.position === 'top') {
// this.queue.write(() => {
// if (this.scrollEl) {
// this.scrollEl.scrollTop = this.scrollEl.scrollHeight - this.scrollEl.clientHeight;
// }
// });
// }
}
componentDidUnload() {
@@ -143,45 +147,45 @@ export class InfiniteScroll {
}
this.isLoading = false;
if (this.position === 'top') {
/**
* New content is being added at the top, but the scrollTop position stays the same,
* which causes a scroll jump visually. This algorithm makes sure to prevent this.
* (Frame 1)
* - complete() is called, but the UI hasn't had time to update yet.
* - Save the current content dimensions.
* - Wait for the next frame using _dom.read, so the UI will be updated.
* (Frame 2)
* - Read the new content dimensions.
* - Calculate the height difference and the new scroll position.
* - Delay the scroll position change until other possible dom reads are done using _dom.write to be performant.
* (Still frame 2, if I'm correct)
* - Change the scroll position (= visually maintain the scroll position).
* - Change the state to re-enable the InfiniteScroll.
* - This should be after changing the scroll position, or it could
* cause the InfiniteScroll to be triggered again immediately.
* (Frame 3)
* Done.
*/
this.isBusy = true;
// ******** DOM READ ****************
// Save the current content dimensions before the UI updates
const prev = scrollEl.scrollHeight - scrollEl.scrollTop;
// if (this.position === 'top') {
// /**
// * New content is being added at the top, but the scrollTop position stays the same,
// * which causes a scroll jump visually. This algorithm makes sure to prevent this.
// * (Frame 1)
// * - complete() is called, but the UI hasn't had time to update yet.
// * - Save the current content dimensions.
// * - Wait for the next frame using _dom.read, so the UI will be updated.
// * (Frame 2)
// * - Read the new content dimensions.
// * - Calculate the height difference and the new scroll position.
// * - Delay the scroll position change until other possible dom reads are done using _dom.write to be performant.
// * (Still frame 2, if I'm correct)
// * - Change the scroll position (= visually maintain the scroll position).
// * - Change the state to re-enable the InfiniteScroll.
// * - This should be after changing the scroll position, or it could
// * cause the InfiniteScroll to be triggered again immediately.
// * (Frame 3)
// * Done.
// */
// this.isBusy = true;
// // ******** DOM READ ****************
// // Save the current content dimensions before the UI updates
// const prev = scrollEl.scrollHeight - scrollEl.scrollTop;
// ******** DOM READ ****************
this.queue.read(() => {
// UI has updated, save the new content dimensions
const scrollHeight = scrollEl.scrollHeight;
// New content was added on top, so the scroll position should be changed immediately to prevent it from jumping around
const newScrollTop = scrollHeight - prev;
// // ******** DOM READ ****************
// this.queue.read(() => {
// // UI has updated, save the new content dimensions
// const scrollHeight = scrollEl.scrollHeight;
// // New content was added on top, so the scroll position should be changed immediately to prevent it from jumping around
// const newScrollTop = scrollHeight - prev;
// ******** DOM WRITE ****************
this.queue.write(() => {
scrollEl.scrollTop = newScrollTop;
this.isBusy = false;
});
});
}
// // ******** DOM WRITE ****************
// this.queue.write(() => {
// scrollEl.scrollTop = newScrollTop;
// this.isBusy = false;
// });
// });
// }
}
/**

View File

@@ -38,10 +38,6 @@
</ion-app>
<script>
let items = [];
for (var i = 0; i < 30; i++) {
items.push(i + 1);
}
const list = document.getElementById('list');
const infiniteScroll = document.getElementById('infinite-scroll');
@@ -51,35 +47,30 @@
infiniteScroll.addEventListener('ionInfinite', async function () {
console.log('Loading data...');
const data = await getAsyncData();
items = items.concat(data);
await wait(500);
infiniteScroll.complete();
render();
appendItems();
console.log('Done');
});
function render() {
let html = '';
for (let item of items) {
html += `<ion-item>${item}</ion-item>`;
function appendItems() {
for (var i = 0; i < 30; i++) {
const el = document.createElement('ion-item');
el.textContent = `${1 + i}`;
list.appendChild(el);
}
list.innerHTML = html;
}
function getAsyncData() {
// async return mock data
function wait(time) {
return new Promise(resolve => {
setTimeout(() => {
let data = [];
for (var i = 0; i < 30; i++) {
data.push(i);
}
resolve(data);
}, 500);
resolve();
}, time);
});
}
render();
appendItems();
</script>
</body>

View File

@@ -30,13 +30,10 @@ ion-popover {
z-index: $z-index-overlay-wrapper;
}
.popover-content ion-content,
.popover-content ion-scroll {
contain: none;
}
.popover-content ion-scroll {
.popover-content ion-content {
position: relative;
contain: none;
}
ion-popover-controller {

View File

@@ -6,7 +6,6 @@ ion-refresher {
display: none;
position: absolute;
top: 0;
width: 100%;
height: $refresher-height;

View File

@@ -15,7 +15,7 @@ export class Refresher {
private appliedStyles = false;
private didStart = false;
private progress = 0;
private scrollEl?: HTMLIonScrollElement;
private scrollEl?: HTMLElement;
private gesture?: Gesture;
mode!: Mode;
@@ -333,7 +333,7 @@ export class Refresher {
this.queue.write(() => {
if (this.scrollEl) {
const style = this.scrollEl.style;
style.transform = ((y > 0) ? 'translateY(' + y + 'px) translateZ(0px)' : 'translateZ(0px)');
style.transform = ((y > 0) ? `translateY(${y}px) translateZ(0px)` : 'translateZ(0px)');
style.transitionDuration = duration;
style.transitionDelay = delay;
style.overflow = (overflowVisible ? 'hidden' : '');

View File

@@ -8,6 +8,10 @@
will-change: transform;
}
.reorder-enabled {
user-select: none;
}
.reorder-enabled ion-reorder {
display: block;

View File

@@ -14,7 +14,7 @@ export class ReorderGroup {
private selectedItemHeight!: number;
private lastToIndex!: number;
private cachedHeights: number[] = [];
private scrollEl?: HTMLIonScrollElement;
private scrollEl?: HTMLElement;
private gesture?: Gesture;
private scrollElTop = 0;

View File

@@ -1,102 +0,0 @@
# ion-scroll
Scroll is a low-level component for arbitrary scrolling areas. It's used internally by Content.
<!-- Auto Generated Below -->
## Properties
#### forceOverscroll
boolean
If true and the content does not cause an overflow scroll, the scroll interaction will cause a bounce.
If the content exceeds the bounds of ionScroll, nothing will change.
Note, the does not disable the system bounce on iOS. That is an OS level setting.
#### mode
string
The mode for component.
#### scrollEvents
boolean
If true, the component will emit scroll events.
## Attributes
#### force-overscroll
boolean
If true and the content does not cause an overflow scroll, the scroll interaction will cause a bounce.
If the content exceeds the bounds of ionScroll, nothing will change.
Note, the does not disable the system bounce on iOS. That is an OS level setting.
#### mode
string
The mode for component.
#### scroll-events
boolean
If true, the component will emit scroll events.
## Events
#### ionScroll
Emitted while scrolling. This event is disabled by default.
Look at the property: `scrollEvents`
#### ionScrollEnd
Emitted when the scroll has ended.
#### ionScrollStart
Emitted when the scroll has started.
## Methods
#### scrollByPoint()
Scroll by a specified X/Y distance in the component
#### scrollToBottom()
Scroll to the bottom of the component
#### scrollToPoint()
Scroll to a specified X/Y location in the component
#### scrollToTop()
Scroll to the top of the component
----------------------------------------------
*Built with [StencilJS](https://stenciljs.com/)*

View File

@@ -1,35 +0,0 @@
@import "../../themes/ionic.globals";
:host {
@include position(0, 0, 0, 0);
display: block;
position: absolute;
contain: size style layout;
z-index: $z-index-scroll-content;
overflow-x: hidden;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
will-change: scroll-position;
box-sizing: border-box;
}
:host(.overscroll)::before,
:host(.overscroll)::after {
position: absolute;
width: 1px;
height: 1px;
content: "";
}
:host(.overscroll)::before {
bottom: -1px;
}
:host(.overscroll)::after {
top: -1px;
}

View File

@@ -1,286 +0,0 @@
import { Component, Element, Event, EventEmitter, Listen, Method, Prop, QueueApi } from '@stencil/core';
import { Config, Mode, ScrollBaseDetail, ScrollDetail } from '../../interface';
import { createThemedClasses } from '../../utils/theme';
@Component({
tag: 'ion-scroll',
styleUrl: 'scroll.scss',
shadow: true
})
export class Scroll {
private watchDog: any;
private isScrolling = false;
private lastScroll = 0;
private queued = false;
// Detail is used in a hot loop in the scroll event, by allocating it here
// V8 will be able to inline any read/write to it since it's a monomorphic class.
// https://mrale.ph/blog/2015/01/11/whats-up-with-monomorphism.html
private detail: ScrollDetail = {
scrollTop: 0,
scrollLeft: 0,
type: 'scroll',
event: undefined!,
startX: 0,
startY: 0,
startTimeStamp: 0,
currentX: 0,
currentY: 0,
velocityX: 0,
velocityY: 0,
deltaX: 0,
deltaY: 0,
timeStamp: 0,
data: undefined,
isScrolling: true,
};
@Element() el!: HTMLElement;
@Prop({ context: 'config' }) config!: Config;
@Prop({ context: 'queue' }) queue!: QueueApi;
@Prop({ context: 'window' }) win!: Window;
/** The mode for component. */
@Prop() mode!: Mode;
/**
* If true and the content does not cause an overflow scroll, the scroll interaction will cause a bounce.
* If the content exceeds the bounds of ionScroll, nothing will change.
* Note, the does not disable the system bounce on iOS. That is an OS level setting.
*/
@Prop({ mutable: true }) forceOverscroll?: boolean;
/** If true, the component will emit scroll events. */
@Prop() scrollEvents = false;
/**
* Emitted when the scroll has started.
*/
@Event() ionScrollStart!: EventEmitter<ScrollBaseDetail>;
/**
* Emitted while scrolling. This event is disabled by default.
* Look at the property: `scrollEvents`
*/
@Event() ionScroll!: EventEmitter<ScrollDetail>;
/**
* Emitted when the scroll has ended.
*/
@Event() ionScrollEnd!: EventEmitter<ScrollBaseDetail>;
componentWillLoad() {
if (this.forceOverscroll === undefined) {
this.forceOverscroll = this.mode === 'ios' && ('ontouchstart' in this.win);
}
}
componentDidUnload() {
if (this.watchDog) {
clearInterval(this.watchDog);
}
}
@Listen('scroll', { passive: true })
onScroll(ev: UIEvent) {
const timeStamp = Date.now();
const didStart = !this.isScrolling;
this.lastScroll = timeStamp;
if (didStart) {
this.onScrollStart();
}
if (!this.queued && this.scrollEvents) {
this.queued = true;
this.queue.read(ts => {
this.queued = false;
this.detail.event = ev;
updateScrollDetail(this.detail, this.el, ts, didStart);
this.ionScroll.emit(this.detail);
});
}
}
/** Scroll to the top of the component */
@Method()
scrollToTop(duration: number): Promise<void> {
return this.scrollToPoint(0, 0, duration);
}
/** Scroll to the bottom of the component */
@Method()
scrollToBottom(duration: number): Promise<void> {
const y = this.el.scrollHeight - this.el.clientHeight;
return this.scrollToPoint(0, y, duration);
}
/** Scroll by a specified X/Y distance in the component */
@Method()
scrollByPoint(x: number, y: number, duration: number): Promise<any> {
return this.scrollToPoint(x + this.el.scrollLeft, y + this.el.scrollTop, duration);
}
/** Scroll to a specified X/Y location in the component */
@Method()
scrollToPoint(x: number, y: number, duration: number): Promise<void> {
// scroll animation loop w/ easing
// credit https://gist.github.com/dezinezync/5487119
let resolve!: () => void;
const promise = new Promise<void>(r => {
resolve = r;
});
const self = this;
const el = self.el;
if (!el) {
// invalid element
resolve();
return promise;
}
if (duration < 32) {
el.scrollTop = y;
el.scrollLeft = x;
resolve();
return promise;
}
const fromY = el.scrollTop;
const fromX = el.scrollLeft;
const maxAttempts = (duration / 16) + 100;
let startTime: number;
let attempts = 0;
let stopScroll = false;
// scroll loop
function step(timeStamp: number) {
attempts++;
if (!self.el || stopScroll || attempts > maxAttempts) {
self.isScrolling = false;
el.style.transform = el.style.webkitTransform = '';
resolve();
return;
}
let time = Math.min(1, ((timeStamp - startTime) / duration));
// where .5 would be 50% of time on a linear scale easedT gives a
// fraction based on the easing method
const easedT = (--time) * time * time + 1;
if (fromY !== y) {
el.scrollTop = (easedT * (y - fromY)) + fromY;
}
if (fromX !== x) {
el.scrollLeft = Math.floor((easedT * (x - fromX)) + fromX);
}
if (easedT < 1) {
// do not use DomController here
// must use nativeRaf in order to fire in the next frame
// TODO: remove as any
self.queue.read(step as any);
} else {
stopScroll = true;
self.isScrolling = false;
el.style.transform = el.style.webkitTransform = '';
resolve();
}
}
// start scroll loop
self.isScrolling = true;
// chill out for a frame first
this.queue.write(() => {
this.queue.write(timeStamp => {
startTime = timeStamp;
step(timeStamp);
});
});
return promise;
}
private onScrollStart() {
this.isScrolling = true;
this.ionScrollStart.emit({
isScrolling: true
});
if (this.watchDog) {
clearInterval(this.watchDog);
}
// watchdog
this.watchDog = setInterval(() => {
if (this.lastScroll < Date.now() - 120) {
this.onScrollEnd();
}
}, 100);
}
private onScrollEnd() {
clearInterval(this.watchDog);
this.watchDog = null;
this.isScrolling = false;
this.ionScrollEnd.emit({
isScrolling: false
});
}
hostData() {
return {
class: {
...createThemedClasses(this.mode, 'scroll'),
overscroll: this.forceOverscroll
}
};
}
render() {
return <slot></slot>;
}
}
// ******** DOM READ ****************
function updateScrollDetail(
detail: ScrollDetail,
el: HTMLElement,
timestamp: number,
didStart: boolean
) {
const prevX = detail.currentX;
const prevY = detail.currentY;
const prevT = detail.timeStamp;
const currentX = el.scrollLeft;
const currentY = el.scrollTop;
if (didStart) {
// remember the start positions
detail.startTimeStamp = timestamp;
detail.startX = currentX;
detail.startY = currentY;
detail.velocityX = detail.velocityY = 0;
}
detail.timeStamp = timestamp;
detail.currentX = detail.scrollLeft = currentX;
detail.currentY = detail.scrollTop = currentY;
detail.deltaX = currentX - detail.startX;
detail.deltaY = currentY - detail.startY;
const timeDelta = timestamp - prevT;
if (timeDelta > 0 && timeDelta < 100) {
const velocityX = (currentX - prevX) / timeDelta;
const velocityY = (currentY - prevY) / timeDelta;
detail.velocityX = velocityX * 0.7 + detail.velocityX * 0.3;
detail.velocityY = velocityY * 0.7 + detail.velocityY * 0.3;
}
}

View File

@@ -39,13 +39,6 @@ string
Set the position of the tabbar, relative to the content. Available options: `"top"`, `"bottom"`.
#### scrollable
boolean
If true, the tabs will be scrollable when there are enough tabs to overflow the width of the screen.
#### selectedTab
HTMLIonTabElement
@@ -100,13 +93,6 @@ string
Set the position of the tabbar, relative to the content. Available options: `"top"`, `"bottom"`.
#### scrollable
boolean
If true, the tabs will be scrollable when there are enough tabs to overflow the width of the screen.
#### selected-tab

View File

@@ -13,20 +13,9 @@
contain: strict;
}
ion-scroll {
@include margin(0, 8px);
max-width: 650px;
}
// TODO: REVIEW
:host(.placement-top) .tab-button.tab-selected .tab-button-icon,
:host(.placement-top) .tab-button.tab-selected .tab-button-text {
transform: inherit;
}
.scrollable .tab-button {
flex: 1 0 auto;
overflow: hidden;
}

View File

@@ -72,22 +72,3 @@
:host(.placement-bottom) .tabbar-highlight {
top: 0;
}
// Overflow Scrolling
// --------------------------------------------------
ion-scroll {
overflow: hidden;
}
.scroll-inner {
display: flex;
position: relative;
flex-direction: row;
}
ion-button.inactive {
visibility: hidden;
}

View File

@@ -13,8 +13,6 @@ import { createColorClasses } from '../../utils/theme';
})
export class Tabbar {
private scrollEl?: HTMLIonScrollElement;
@Prop() mode!: Mode;
@Prop() color?: Color;
@@ -40,17 +38,11 @@ export class Tabbar {
/** The selected tab component */
@Prop() selectedTab?: HTMLIonTabElement;
/**
* If true, the tabs will be scrollable when there are enough tabs to overflow the width of the screen.
*/
@Prop() scrollable = false;
/** The tabs to render */
@Prop() tabs: HTMLIonTabElement[] = [];
@Watch('selectedTab')
selectedTabChanged() {
this.scrollToSelectedButton();
this.updateHighlight();
}
@@ -85,92 +77,14 @@ export class Tabbar {
}
componentDidLoad() {
this.updateBoundaries();
this.updateHighlight();
}
protected analyzeTabs() {
const tabs: HTMLIonTabButtonElement[] = Array.from(this.doc.querySelectorAll('ion-tab-button'));
const scrollLeft = this.scrollEl!.scrollLeft;
const tabsWidth = this.scrollEl!.clientWidth;
let previous: {tab: HTMLIonTabButtonElement, amount: number} | undefined;
let next: {tab: HTMLIonTabButtonElement, amount: number} | undefined;
for (const tab of tabs) {
const left = tab.offsetLeft;
const right = left + tab.offsetWidth;
if (left < scrollLeft) {
previous = { tab, amount: left };
}
if (!next && right > (tabsWidth + scrollLeft)) {
const amount = right - tabsWidth;
next = { tab, amount };
}
}
return { previous, next };
}
private getSelectedButton(): HTMLIonTabButtonElement | undefined {
return Array.from(this.el.querySelectorAll('ion-tab-button'))
.find(btn => btn.selected);
}
protected scrollToSelectedButton() {
if (!this.scrollEl || !this.scrollable) {
return;
}
this.queue.read(() => {
const activeTabButton = this.getSelectedButton();
if (activeTabButton) {
const scrollLeft = this.scrollEl!.scrollLeft;
const tabsWidth = this.scrollEl!.clientWidth;
const left = activeTabButton.offsetLeft;
const right = left + activeTabButton.offsetWidth;
let amount = 0;
if (right > (tabsWidth + scrollLeft)) {
amount = right - tabsWidth;
} else if (left < scrollLeft) {
amount = left;
}
if (amount !== 0) {
this.queue.write(() => {
this.scrollEl!.scrollToPoint(amount, 0, 250).then(() => {
this.updateBoundaries();
});
});
}
}
});
}
private scrollByTab(direction: 'left' | 'right') {
this.queue.read(() => {
const { previous, next } = this.analyzeTabs();
const info = direction === 'right' ? next : previous;
const amount = info && info.amount;
if (info && amount) {
this.scrollEl!.scrollToPoint(amount, 0, 250).then(() => {
this.updateBoundaries();
});
}
});
}
private updateBoundaries() {
if (this.scrollEl && this.scrollable) {
this.canScrollLeft = this.scrollEl.scrollLeft !== 0;
this.canScrollRight = this.scrollEl.scrollLeft < (this.scrollEl.scrollWidth - this.scrollEl.offsetWidth);
}
}
private updateHighlight() {
if (!this.highlight) {
return;
@@ -185,7 +99,7 @@ export class Tabbar {
}
hostData() {
const { color, translucent, layout, placement, keyboardVisible, scrollable } = this;
const { color, translucent, layout, placement, keyboardVisible } = this;
return {
role: 'tablist',
'aria-hidden': keyboardVisible ? 'true' : null,
@@ -195,56 +109,38 @@ export class Tabbar {
[`layout-${layout}`]: true,
[`placement-${placement}`]: true,
'tabbar-hidden': keyboardVisible,
'scrollable': scrollable
}
};
}
render() {
const selectedTab = this.selectedTab;
const ionTabbarHighlight = this.highlight ? <div class="animated tabbar-highlight" /> as HTMLElement : null;
const tabButtons = this.tabs.map(tab => <ion-tab-button
id={tab.btnId}
label={tab.label}
icon={tab.icon}
badge={tab.badge}
disabled={tab.disabled}
badgeColor={tab.badgeColor}
href={tab.href}
selected={selectedTab === tab}
mode={this.mode}
color={this.color}
aria-hidden={ !tab.show ? 'true' : null }
class={{ 'tab-hidden': !tab.show }}
onClick={ev => {
if (!tab.disabled) {
this.ionTabbarClick.emit(tab);
}
ev.stopPropagation();
ev.preventDefault();
}}
/>);
if (this.scrollable) {
return [
<ion-button onClick={() => this.scrollByTab('left')} fill="clear" class={{ inactive: !this.canScrollLeft }}>
<ion-icon name="arrow-dropleft" lazy={false}/>
</ion-button>,
<ion-scroll forceOverscroll={false} ref={scrollEl => this.scrollEl = scrollEl as HTMLIonScrollElement}>
{tabButtons}
{ionTabbarHighlight}
</ion-scroll>,
<ion-button onClick={() => this.scrollByTab('right')} fill="clear" class={{ inactive: !this.canScrollRight }}>
<ion-icon name="arrow-dropright" lazy={false}/>
</ion-button>
];
} else {
return [
...tabButtons,
ionTabbarHighlight
];
}
return [
this.tabs.map(tab => (
<ion-tab-button
id={tab.btnId}
label={tab.label}
icon={tab.icon}
badge={tab.badge}
disabled={tab.disabled}
badgeColor={tab.badgeColor}
href={tab.href}
selected={selectedTab === tab}
mode={this.mode}
color={this.color}
aria-hidden={ !tab.show ? 'true' : null }
class={{ 'tab-hidden': !tab.show }}
onClick={ev => {
if (!tab.disabled) {
this.ionTabbarClick.emit(tab);
}
ev.stopPropagation();
ev.preventDefault();
}}
/>
)),
this.highlight && <div class="animated tabbar-highlight" />
];
}
}

View File

@@ -25,13 +25,6 @@ string
A unique name for the tabs.
#### scrollable
boolean
If true, the tabs will be scrollable when there are enough tabs to overflow the width of the screen.
#### tabbarHidden
boolean
@@ -95,13 +88,6 @@ string
A unique name for the tabs.
#### scrollable
boolean
If true, the tabs will be scrollable when there are enough tabs to overflow the width of the screen.
#### tabbar-hidden
boolean

View File

@@ -21,7 +21,6 @@ export class Tabs implements NavOutlet {
@State() selectedTab?: HTMLIonTabElement;
@Prop({ context: 'config' }) config!: Config;
@Prop({ context: 'document' }) doc!: Document;
/**
@@ -64,11 +63,6 @@ export class Tabs implements NavOutlet {
*/
@Prop() translucent = false;
/**
* If true, the tabs will be scrollable when there are enough tabs to overflow the width of the screen.
*/
@Prop() scrollable = false;
/**
* If true, the tabs will use the router and `selectedTab` will not do anything.
*/
@@ -307,14 +301,12 @@ export class Tabs implements NavOutlet {
}
render() {
const dom = [
return [
<div class="tabs-inner">
<slot></slot>
</div>
];
</div>,
if (!this.tabbarHidden) {
dom.push(
!this.tabbarHidden && (
<ion-tabbar
tabs={this.tabs.slice()}
color={this.color}
@@ -322,12 +314,10 @@ export class Tabs implements NavOutlet {
highlight={this.tabbarHighlight}
placement={this.tabbarPlacement}
layout={this.tabbarLayout}
translucent={this.translucent}
scrollable={this.scrollable}>
translucent={this.translucent}>
</ion-tabbar>
);
}
return dom;
)
];
}
}

View File

@@ -1,37 +0,0 @@
<!DOCTYPE html>
<html dir="ltr">
<head>
<meta charset="UTF-8">
<title>Tab - Scroll</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<script src="/dist/ionic.js"></script>
<link rel="stylesheet" type="text/css" href="/css/ionic.min.css">
</head>
<body>
<ion-app>
<ion-tabs scrollable="true" tabbar-placement="top" tabbar-highlight="true">
<ion-tab label="Plain List" icon="star"></ion-tab>
<ion-tab label="Schedule Long Text" icon="globe"></ion-tab>
<ion-tab label="Stopwatch" icon="logo-facebook"></ion-tab>
<ion-tab label="Messages" icon="chatboxes"></ion-tab>
<ion-tab label="Messages 2" icon="chatboxes"></ion-tab>
<ion-tab label="Messages 3" icon="chatboxes"></ion-tab>
<ion-tab label="Messages 4" icon="chatboxes"></ion-tab>
<ion-tab label="Messages 5" icon="chatboxes"></ion-tab>
<ion-tab label="Messages 6" icon="chatboxes"></ion-tab>
<ion-tab label="Messages 7" icon="chatboxes"></ion-tab>
<ion-tab label="Messages 8" icon="chatboxes"></ion-tab>
<ion-tab label="Messages 9" icon="chatboxes"></ion-tab>
<ion-tab label="Messages 10" icon="chatboxes"></ion-tab>
<ion-tab label="Messages 11" icon="chatboxes"></ion-tab>
<ion-tab label="Messages 12" icon="chatboxes"></ion-tab>
</ion-tabs>
<ion-nav-controller></ion-nav-controller>
</ion-app>
</body>
</html>

View File

@@ -11,7 +11,7 @@ import { Range, calcCells, calcHeightIndex, doRender, findCellIndex, getRange, g
})
export class VirtualScroll {
private scrollEl?: HTMLIonScrollElement;
private scrollEl?: HTMLElement;
private range: Range = { offset: 0, length: 0 };
private timerUpdate: any;
private heightIndex?: Uint32Array;
@@ -113,17 +113,17 @@ export class VirtualScroll {
this.calcCells();
}
componentDidLoad() {
async componentDidLoad() {
const contentEl = this.el.closest('ion-content');
if (!contentEl) {
console.error('virtual-scroll must be used inside ion-scroll/ion-content');
console.error('virtual-scroll must be used inside ion-content');
return;
}
contentEl.componentOnReady().then(() => {
this.scrollEl = contentEl.getScrollElement();
this.calcCells();
this.updateState();
});
await contentEl.componentOnReady();
this.scrollEl = contentEl.getScrollElement();
this.calcCells();
this.updateState();
}
componentDidUpdate() {

View File

@@ -11,7 +11,7 @@ export * from './components/popover/popover-interface';
export * from './components/nav/nav-interface';
export * from './components/router/utils/interface';
export * from './components/range/range-interface';
export * from './components/scroll/scroll-interface';
export * from './components/content/content-interface';
export * from './components/select/select-interface';
export * from './components/select-popover/select-popover-interface';
export * from './components/tabbar/tabbar-interface';

View File

@@ -1,6 +1,6 @@
import { isFocused, relocateInput } from './common';
export function enableHideCaretOnScroll(componentEl: HTMLElement, inputEl: HTMLInputElement | undefined, scrollEl: HTMLIonScrollElement | undefined) {
export function enableHideCaretOnScroll(componentEl: HTMLElement, inputEl: HTMLInputElement | undefined, scrollEl: HTMLIonContentElement | undefined) {
if (!scrollEl || !inputEl) {
return () => { return; };
}

View File

@@ -64,7 +64,7 @@ function jsSetFocus(
inputEl.focus();
// scroll the input into place
contentEl.getScrollElement().scrollByPoint(0, scrollData.scrollAmount, scrollData.scrollDuration).then(() => {
contentEl.scrollByPoint(0, scrollData.scrollAmount, scrollData.scrollDuration).then(() => {
// the scroll view is in the correct position now
// give the native text input focus
relocateInput(componentEl, inputEl, false, scrollData.inputSafeY);

View File

@@ -26,8 +26,7 @@ export function startInputShims(
function registerInput(componentEl: HTMLElement) {
const inputEl = (componentEl.shadowRoot || componentEl).querySelector('input');
const contentEl = componentEl.closest('ion-content');
const scrollEl = contentEl && contentEl.getScrollElement();
const scrollEl = componentEl.closest('ion-content');
if (!inputEl) {
return;
@@ -38,8 +37,8 @@ export function startInputShims(
hideCaretMap.set(componentEl, rmFn);
}
if (SCROLL_ASSIST && contentEl && scrollAssist && !scrollAssistMap.has(componentEl)) {
const rmFn = enableScrollAssist(componentEl, inputEl, contentEl, keyboardHeight);
if (SCROLL_ASSIST && scrollEl && scrollAssist && !scrollAssistMap.has(componentEl)) {
const rmFn = enableScrollAssist(componentEl, inputEl, scrollEl, keyboardHeight);
scrollAssistMap.set(componentEl, rmFn);
}
}

View File

@@ -12,7 +12,7 @@ export function startStatusTap(win: Window, queue: QueueApi) {
if (contentEl) {
await contentEl.componentOnReady();
queue.write(() => {
contentEl.getScrollElement().scrollToTop(300);
contentEl.scrollToTop(300);
});
}
});

View File

@@ -7,7 +7,7 @@ exports.config = {
{ components: ['ion-alert', 'ion-alert-controller'] },
{ components: ['ion-anchor', 'ion-back-button'] },
{ components: ['ion-animation-controller'] },
{ components: ['ion-app', 'ion-buttons', 'ion-content', 'ion-footer', 'ion-header', 'ion-scroll', 'ion-title', 'ion-toolbar'] },
{ components: ['ion-app', 'ion-buttons', 'ion-content', 'ion-footer', 'ion-header', 'ion-title', 'ion-toolbar'] },
{ components: ['ion-avatar', 'ion-badge', 'ion-thumbnail'] },
{ components: ['ion-backdrop'] },
{ components: ['ion-button', 'ion-icon'] },