@ng-native/components 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -25,8 +25,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImpor
25
25
  type: Directive,
26
26
  args: [{ selector: 'ng-template[virtualListSeparator]' }]
27
27
  }] });
28
- /** A parked slot: out of layout, out of sight, and out of the accessibility tree. */
29
- const PARKED = Object.freeze({ display: 'none' });
28
+ /**
29
+ * A parked slot: out of the flow, out of sight and out of reach, with its view kept to be used
30
+ * again. Not `display: none`, which is an element with no view. A screen reader passes over a
31
+ * view with no opacity, and `VirtualListRow` says so outright where a row uses it. Moved off the
32
+ * list as well, since `pointer-events: none` is CSS's: something inside a row with a
33
+ * `pointer-events` of its own would still take a touch where the row was left.
34
+ */
35
+ const PARKED = Object.freeze({
36
+ position: 'absolute',
37
+ opacity: 0,
38
+ pointerEvents: 'none',
39
+ transform: 'translateX(-100000px)',
40
+ });
30
41
  /** How many hidden slots of each row type are kept ready for reuse. */
31
42
  const PARK_LIMIT = 6;
32
43
  /** The style of a row that sizes itself and needs nothing else: in flow, where it falls. */
@@ -178,6 +189,8 @@ export class VirtualList extends ScrollViewProps {
178
189
  ...(ngDevMode ? [{ debugName: "headerHeight" }] : /* istanbul ignore next */ []));
179
190
  /** What native reported the content measures along the axis, once it has. */
180
191
  contentSize = 0;
192
+ /** The rows' own extent when native reported that, so a report for other rows is not used. */
193
+ contentSizeFor = -1;
181
194
  /** The item count `endReached` last fired for, cleared once the list is away from the end. */
182
195
  endReachedFor = -1;
183
196
  /** Slot styles are cached so a scroll does not hand Fabric new prop objects for stable rows. */
@@ -309,6 +322,9 @@ export class VirtualList extends ScrollViewProps {
309
322
  stride() {
310
323
  return this.itemHeight() + this.rowGap();
311
324
  }
325
+ /** ScrollView.js's `contentContainerHorizontal`: the header, rows and footer side by side. */
326
+ content = computed(() => this.horizontal() ? { flexDirection: 'row' } : undefined, /* @ts-ignore */
327
+ ...(ngDevMode ? [{ debugName: "content" }] : /* istanbul ignore next */ []));
312
328
  canvas = computed(() => this.horizontal() ? { width: this.extent(), flexDirection: 'row' } : { height: this.extent() }, /* @ts-ignore */
313
329
  ...(ngDevMode ? [{ debugName: "canvas" }] : /* istanbul ignore next */ []));
314
330
  /**
@@ -521,6 +537,7 @@ export class VirtualList extends ScrollViewProps {
521
537
  this.flushScroll();
522
538
  if (this.nativeSticky)
523
539
  this.driveStickyRows();
540
+ this.recheckItems();
524
541
  });
525
542
  const stop = dismissKeyboardOnTap(this.engine, this.node, () => this.keyboardShouldPersistTaps());
526
543
  inject(DestroyRef).onDestroy(() => {
@@ -533,6 +550,21 @@ export class VirtualList extends ScrollViewProps {
533
550
  if (this.zeroSize)
534
551
  this.checkZeroSize(this.zeroSize);
535
552
  }
553
+ /** The items the end and viewability were last checked against after a render. */
554
+ checkedItems = null;
555
+ /**
556
+ * New items can bring the end within reach or change what is on screen with no scroll: a list
557
+ * shorter than its viewport, a page appended while at the end, a filter. Checked once the rows
558
+ * they make are rendered, and only when they changed, so any other render costs a comparison.
559
+ */
560
+ recheckItems() {
561
+ const items = this.items();
562
+ if (items === this.checkedItems)
563
+ return;
564
+ this.checkedItems = items;
565
+ this.checkEnd(this.headerHeight() + this.lastOffset);
566
+ this.announceViewable(this.lastOffset);
567
+ }
536
568
  /** Whether the viewport has been laid out yet, so a zero in it is a measurement. */
537
569
  viewportMeasured = false;
538
570
  /**
@@ -973,6 +1005,8 @@ export class VirtualList extends ScrollViewProps {
973
1005
  }
974
1006
  /** The last set announced, so the output fires on a change rather than on every frame. */
975
1007
  announced = [];
1008
+ /** The items the last set was announced from: new items are a change even at the same indices. */
1009
+ announcedFrom = null;
976
1010
  /**
977
1011
  * Announce a change in what is on screen.
978
1012
  *
@@ -985,10 +1019,12 @@ export class VirtualList extends ScrollViewProps {
985
1019
  const viewable = this.viewableAt(offset);
986
1020
  const entered = viewable.filter((index) => !this.announced.includes(index));
987
1021
  const left = this.announced.filter((index) => !viewable.includes(index));
988
- if (entered.length === 0 && left.length === 0)
1022
+ const items = this.items();
1023
+ const same = items === this.announcedFrom || viewable.length === 0;
1024
+ if (entered.length === 0 && left.length === 0 && same)
989
1025
  return;
990
1026
  this.announced = viewable;
991
- const items = this.items();
1027
+ this.announcedFrom = items;
992
1028
  this.viewableItemsChanged.emit({
993
1029
  viewable: viewable.map((index) => {
994
1030
  const key = this.keyAt(index);
@@ -1091,6 +1127,7 @@ export class VirtualList extends ScrollViewProps {
1091
1127
  this.viewport.set((this.horizontal() ? layout?.width : layout?.height) ?? 0);
1092
1128
  this.viewportMeasured = true;
1093
1129
  this.zeroSize?.laidOut(this.viewport(), this.items().length > 0);
1130
+ this.checkEnd(this.headerHeight() + this.lastOffset);
1094
1131
  this.announceViewable(this.lastOffset);
1095
1132
  }
1096
1133
  /** The header's extent along the axis, which every row sits after. */
@@ -1118,13 +1155,18 @@ export class VirtualList extends ScrollViewProps {
1118
1155
  onContentSize(event) {
1119
1156
  const size = event.nativeEvent;
1120
1157
  this.contentSize = (this.horizontal() ? size?.width : size?.height) ?? 0;
1158
+ this.contentSizeFor = this.extent();
1159
+ this.checkEnd(this.headerHeight() + this.lastOffset);
1121
1160
  }
1122
1161
  checkEnd(y) {
1123
1162
  const count = this.items().length;
1124
1163
  const viewport = this.viewport();
1125
1164
  if (!count || !viewport)
1126
1165
  return;
1127
- const total = this.contentSize || this.headerHeight() + this.extent();
1166
+ // Native's size is for the rows it last laid out; until it reports for these, their own extent.
1167
+ const extent = this.extent();
1168
+ const reported = this.contentSizeFor === extent ? this.contentSize : 0;
1169
+ const total = reported || this.headerHeight() + extent;
1128
1170
  const distanceFromEnd = total - (y + viewport);
1129
1171
  // Away from the end again re-arms it, as React Native does, so a load that failed is retried
1130
1172
  // the next time the user comes back down.
@@ -1140,23 +1182,27 @@ export class VirtualList extends ScrollViewProps {
1140
1182
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: VirtualList, deps: [], target: i0.ɵɵFactoryTarget.Component });
1141
1183
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.2.0", type: VirtualList, isStandalone: true, selector: "virtual-list", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: true, transformFunction: null }, itemHeight: { classPropertyName: "itemHeight", publicName: "itemHeight", isSignal: true, isRequired: false, transformFunction: null }, estimatedItemHeight: { classPropertyName: "estimatedItemHeight", publicName: "estimatedItemHeight", isSignal: true, isRequired: false, transformFunction: null }, contentPadding: { classPropertyName: "contentPadding", publicName: "contentPadding", isSignal: true, isRequired: false, transformFunction: null }, rowGap: { classPropertyName: "rowGap", publicName: "rowGap", isSignal: true, isRequired: false, transformFunction: null }, keyExtractor: { classPropertyName: "keyExtractor", publicName: "keyExtractor", isSignal: true, isRequired: false, transformFunction: null }, itemType: { classPropertyName: "itemType", publicName: "itemType", isSignal: true, isRequired: false, transformFunction: null }, maintainVisibleContentPosition: { classPropertyName: "maintainVisibleContentPosition", publicName: "maintainVisibleContentPosition", isSignal: true, isRequired: false, transformFunction: null }, overscan: { classPropertyName: "overscan", publicName: "overscan", isSignal: true, isRequired: false, transformFunction: null }, endReachedThreshold: { classPropertyName: "endReachedThreshold", publicName: "endReachedThreshold", isSignal: true, isRequired: false, transformFunction: null }, horizontal: { classPropertyName: "horizontal", publicName: "horizontal", isSignal: true, isRequired: false, transformFunction: null }, inverted: { classPropertyName: "inverted", publicName: "inverted", isSignal: true, isRequired: false, transformFunction: null }, stickyIndices: { classPropertyName: "stickyIndices", publicName: "stickyIndices", isSignal: true, isRequired: false, transformFunction: null }, stickyHeader: { classPropertyName: "stickyHeader", publicName: "stickyHeader", isSignal: true, isRequired: false, transformFunction: null }, keyboardShouldPersistTaps: { classPropertyName: "keyboardShouldPersistTaps", publicName: "keyboardShouldPersistTaps", isSignal: true, isRequired: false, transformFunction: null }, itemVisiblePercentThreshold: { classPropertyName: "itemVisiblePercentThreshold", publicName: "itemVisiblePercentThreshold", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { endReached: "endReached", viewableItemsChanged: "viewableItemsChanged" }, host: { listeners: { "scroll": "onScroll($event)", "layout": "onLayout($event)", "contentSizeChange": "onContentSize($event)" }, properties: { "flexDirection": "horizontal() ? 'row' : 'column'", "horizontal": "horizontal()", "nestedScrollEnabled": "nestedScroll()", "transform": "flip()", "scrollEventThrottle": "scrollEventThrottle()" } }, queries: [{ propertyName: "separator", first: true, predicate: VirtualListSeparator, descendants: true, isSignal: true }, { propertyName: "refreshControl", first: true, predicate: RefreshControl, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "canvasView", first: true, predicate: ["rows"], descendants: true, read: ElementRef, isSignal: true }], exportAs: ["virtualList"], usesInheritance: true, ngImport: i0, template: `
1142
1184
  <ng-content select="refresh-control" />
1143
- <view [style]="headerStyle()" (layout)="onHeaderLayout($event)"
1144
- ><ng-content select="[listHeader]"
1145
- /></view>
1146
- <view #rows [style]="canvas()" collapsable="false">
1147
- @if (measuring()) {
1148
- <view [style]="spacer()"></view>
1149
- }
1150
- <ng-content />
1151
- @if (separator(); as separator) {
1152
- @for (gap of gaps(); track gap.index) {
1153
- <view [style]="gap.style"
1154
- ><ng-container [templateSlot]="separator.template" [templateSlotContext]="gap.context"
1155
- /></view>
1185
+ <view [style]="content()" collapsable="false">
1186
+ <view [style]="headerStyle()" (layout)="onHeaderLayout($event)"
1187
+ ><ng-content select="[listHeader]"
1188
+ /></view>
1189
+ <view #rows [style]="canvas()" collapsable="false">
1190
+ @if (measuring()) {
1191
+ <view [style]="spacer()"></view>
1192
+ }
1193
+ <ng-content />
1194
+ @if (separator(); as separator) {
1195
+ @for (gap of gaps(); track gap.index) {
1196
+ <view [style]="gap.style"
1197
+ ><ng-container
1198
+ [templateSlot]="separator.template"
1199
+ [templateSlotContext]="gap.context"
1200
+ /></view>
1201
+ }
1156
1202
  }
1157
- }
1203
+ </view>
1204
+ <view [style]="footerStyle()"><ng-content select="[listFooter]" /></view>
1158
1205
  </view>
1159
- <view [style]="footerStyle()"><ng-content select="[listFooter]" /></view>
1160
1206
  `, isInline: true, dependencies: [{ kind: "directive", type: TemplateSlot, selector: "[templateSlot]", inputs: ["templateSlot", "templateSlotContext"] }, { kind: "directive", type: View, selector: "view" }] });
1161
1207
  }
1162
1208
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: VirtualList, decorators: [{
@@ -1165,29 +1211,36 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImpor
1165
1211
  selector: 'virtual-list',
1166
1212
  exportAs: 'virtualList',
1167
1213
  imports: [TemplateSlot, View],
1214
+ // One content view holds the header, the rows and the footer, as ScrollView.js's content
1215
+ // container does: Android's ScrollView takes one child and throws on a second. It is never
1216
+ // collapsed, or Fabric would flatten it away and hand the scroll view all three again.
1168
1217
  // See ScrollView: the canvas carries only a height, so Fabric would flatten it away and drags
1169
1218
  // over the gaps between rows would not scroll. Separators come after the rows so they draw over
1170
1219
  // the slot they share. The spacer is there only when rows are in flow.
1171
1220
  // A refresh control comes first, as the scroll view's own child; see ScrollView.
1172
1221
  template: `
1173
1222
  <ng-content select="refresh-control" />
1174
- <view [style]="headerStyle()" (layout)="onHeaderLayout($event)"
1175
- ><ng-content select="[listHeader]"
1176
- /></view>
1177
- <view #rows [style]="canvas()" collapsable="false">
1178
- @if (measuring()) {
1179
- <view [style]="spacer()"></view>
1180
- }
1181
- <ng-content />
1182
- @if (separator(); as separator) {
1183
- @for (gap of gaps(); track gap.index) {
1184
- <view [style]="gap.style"
1185
- ><ng-container [templateSlot]="separator.template" [templateSlotContext]="gap.context"
1186
- /></view>
1223
+ <view [style]="content()" collapsable="false">
1224
+ <view [style]="headerStyle()" (layout)="onHeaderLayout($event)"
1225
+ ><ng-content select="[listHeader]"
1226
+ /></view>
1227
+ <view #rows [style]="canvas()" collapsable="false">
1228
+ @if (measuring()) {
1229
+ <view [style]="spacer()"></view>
1230
+ }
1231
+ <ng-content />
1232
+ @if (separator(); as separator) {
1233
+ @for (gap of gaps(); track gap.index) {
1234
+ <view [style]="gap.style"
1235
+ ><ng-container
1236
+ [templateSlot]="separator.template"
1237
+ [templateSlotContext]="gap.context"
1238
+ /></view>
1239
+ }
1187
1240
  }
1188
- }
1241
+ </view>
1242
+ <view [style]="footerStyle()"><ng-content select="[listFooter]" /></view>
1189
1243
  </view>
1190
- <view [style]="footerStyle()"><ng-content select="[listFooter]" /></view>
1191
1244
  `,
1192
1245
  host: {
1193
1246
  // ScrollView.js's `baseHorizontal`: a column would stack the header, canvas and footer, and
@@ -1223,7 +1276,7 @@ export class VirtualListRow {
1223
1276
  this.list.measureRow(this.row(), event.nativeEvent?.layout);
1224
1277
  }
1225
1278
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: VirtualListRow, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1226
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.2.0", type: VirtualListRow, isStandalone: true, selector: "[virtualListRow]", inputs: { row: { classPropertyName: "row", publicName: "virtualListRow", isSignal: true, isRequired: true, transformFunction: null } }, host: { listeners: { "layout": "onLayout($event)" }, properties: { "style": "row().style" } }, ngImport: i0 });
1279
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.2.0", type: VirtualListRow, isStandalone: true, selector: "[virtualListRow]", inputs: { row: { classPropertyName: "row", publicName: "virtualListRow", isSignal: true, isRequired: true, transformFunction: null } }, host: { listeners: { "layout": "onLayout($event)" }, properties: { "style": "row().style", "attr.aria-hidden": "row().parked ? true : null" } }, ngImport: i0 });
1227
1280
  }
1228
1281
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: VirtualListRow, decorators: [{
1229
1282
  type: Directive,
@@ -1231,6 +1284,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImpor
1231
1284
  selector: '[virtualListRow]',
1232
1285
  host: {
1233
1286
  '[style]': 'row().style',
1287
+ // A parked row is not read out: see `PARKED`.
1288
+ '[attr.aria-hidden]': 'row().parked ? true : null',
1234
1289
  '(layout)': 'onLayout($event)',
1235
1290
  },
1236
1291
  }]