@evergis/uilib-gl 1.0.136 → 1.0.137

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.
@@ -1,6 +1,6 @@
1
1
  /// <reference types="lodash" />
2
2
  import { Component } from "react";
3
- import { Flip, PopoverPosition, PopoverProps, PopoverPropsDefault } from "./types";
3
+ import { ContentObserver, Flip, PopoverPosition, PopoverProps, PopoverPropsDefault } from "./types";
4
4
  export declare class Popover extends Component<PopoverProps> {
5
5
  static defaultProps: PopoverPropsDefault;
6
6
  anchorNode: HTMLElement | null;
@@ -8,9 +8,29 @@ export declare class Popover extends Component<PopoverProps> {
8
8
  popoverAnchorNode: HTMLElement | null;
9
9
  flip: Flip | null;
10
10
  childrenClicked: boolean;
11
+ contentObservers: ContentObserver[];
12
+ heightSyncFrame: number | null;
11
13
  saveAnchor: (ref: HTMLElement | null) => null;
12
14
  savePopoverAnchor: (ref: HTMLElement | null) => void;
13
15
  saveContainer: (ref: HTMLElement | null) => void;
16
+ /**
17
+ * Содержимое меняет размер и после открытия: форма раскрывает новые поля, список догружается.
18
+ * Обрезку по окну пересчитываем вслед за ним, а не только на открытии и ресайзе окна — иначе
19
+ * выросшее содержимое уезжало бы за край экрана.
20
+ *
21
+ * Размер контейнера ловит ResizeObserver, но под обрезкой он замирает: высота ребёнка задана
22
+ * явно, и рост или сжатие содержимого контейнер уже не меняют. Эти случаи ловит MutationObserver —
23
+ * по вставке и удалению узлов, то есть ровно когда форма меняет свой состав.
24
+ */
25
+ observeContent: (node: HTMLElement | null) => void;
26
+ disconnectContent: () => void;
27
+ /**
28
+ * Пересчёт — в следующем кадре, а не прямо в колбэке: правка высоты внутри колбэка
29
+ * ResizeObserver зациклила бы его («ResizeObserver loop completed…»), а пачка мутаций одного
30
+ * рендера схлопывается в один пересчёт.
31
+ */
32
+ scheduleHeightSync: () => void;
33
+ syncContainerHeight: () => void;
14
34
  getContainerPosition: (anchorNode: HTMLElement, containerNode: HTMLElement) => PopoverPosition;
15
35
  setPositions: import("lodash").DebouncedFunc<() => void>;
16
36
  closePopover: (e: Event | MouseEvent | TouchEvent) => void;
@@ -40,3 +40,14 @@ export declare type AnchorProps = {
40
40
  withTime?: boolean;
41
41
  width?: string | number;
42
42
  };
43
+ /** Наблюдатель за содержимым поповера — ResizeObserver либо MutationObserver. */
44
+ export interface ContentObserver {
45
+ disconnect: () => void;
46
+ }
47
+ /**
48
+ * Конструктор ResizeObserver структурно: сборка (tsdx) идёт своим TypeScript 3.9, в DOM-типах
49
+ * которого ResizeObserver ещё нет, и прямое имя класса валит сборку.
50
+ */
51
+ export declare type ResizeObserverConstructor = new (callback: () => void) => ContentObserver & {
52
+ observe: (target: Element) => void;
53
+ };
@@ -7,5 +7,13 @@ export declare const checkFit: (anchorNode: HTMLElement, containerNode: HTMLElem
7
7
  y: boolean;
8
8
  };
9
9
  export declare const syncPosition: (anchor: HTMLElement, node: HTMLElement) => void;
10
+ /**
11
+ * Урезает высоту содержимого до места в окне по направлению раскрытия.
12
+ *
13
+ * Прошлая обрезка перед замером снимается: иначе мерилась бы уже урезанная высота, и содержимое,
14
+ * которое с тех пор выросло или сжалось, так и жило бы с устаревшей — выросшее уезжало бы за край
15
+ * окна, сжавшееся оставляло бы пустоту. Своей прокрутки обрезка не даёт: её включает содержимое.
16
+ */
17
+ export declare const setPopupContainerHeight: (anchorNode: HTMLElement, node: HTMLElement, targetY: number) => void;
10
18
  export declare const getOriginPosition: (flip: Flip | null, origin: Origin) => Position;
11
19
  export declare const setPopupContainerPosition: (anchorNode: HTMLElement, node: HTMLElement, { anchorOriginPosition: [anchorX, anchorY], targetOriginPosition: [targetX, targetY], paddingSide }: PopoverPosition, animateAppearance: boolean, animateX: boolean, animateY: boolean) => void;
@@ -17667,11 +17667,43 @@ const syncPosition = (anchor, node) => {
17667
17667
  node.style.top = `${top}px`;
17668
17668
  node.style.left = `${newLeft}px`;
17669
17669
  };
17670
+ /**
17671
+ * Метки обрезки в data-атрибутах самого ребёнка: что высоту поставил Popover, и какой она была до
17672
+ * него. Обрезку нужно уметь снять, не затерев собственную инлайн-высоту содержимого.
17673
+ */
17674
+
17675
+ const CLAMPED_HEIGHT_KEY = "popoverClampedHeight";
17676
+ const OWN_HEIGHT_KEY = "popoverOwnHeight";
17677
+
17678
+ const releaseClampedHeight = childNode => {
17679
+ if (childNode.dataset[CLAMPED_HEIGHT_KEY] === undefined) return;
17680
+ childNode.style.height = childNode.dataset[OWN_HEIGHT_KEY] ?? "";
17681
+ delete childNode.dataset[CLAMPED_HEIGHT_KEY];
17682
+ delete childNode.dataset[OWN_HEIGHT_KEY];
17683
+ };
17684
+
17685
+ const clampHeight = (childNode, height) => {
17686
+ if (childNode.dataset[CLAMPED_HEIGHT_KEY] === undefined) {
17687
+ childNode.dataset[OWN_HEIGHT_KEY] = childNode.style.height;
17688
+ childNode.dataset[CLAMPED_HEIGHT_KEY] = "";
17689
+ }
17690
+
17691
+ childNode.style.height = `${height}px`;
17692
+ };
17693
+ /**
17694
+ * Урезает высоту содержимого до места в окне по направлению раскрытия.
17695
+ *
17696
+ * Прошлая обрезка перед замером снимается: иначе мерилась бы уже урезанная высота, и содержимое,
17697
+ * которое с тех пор выросло или сжалось, так и жило бы с устаревшей — выросшее уезжало бы за край
17698
+ * окна, сжавшееся оставляло бы пустоту. Своей прокрутки обрезка не даёт: её включает содержимое.
17699
+ */
17700
+
17670
17701
 
17671
17702
  const setPopupContainerHeight = (anchorNode, node, targetY) => {
17672
- if (!node.firstChild) return;
17703
+ if (!(node.firstChild instanceof HTMLElement)) return;
17673
17704
  const clientHeight = window.innerHeight;
17674
17705
  const childNode = node.firstChild;
17706
+ releaseClampedHeight(childNode);
17675
17707
  const childNodeHeight = childNode.offsetHeight;
17676
17708
  const anchorBounds = anchorNode.getBoundingClientRect();
17677
17709
  const anchorPosY = anchorBounds.top;
@@ -17680,16 +17712,15 @@ const setPopupContainerHeight = (anchorNode, node, targetY) => {
17680
17712
  const downDirection = 0;
17681
17713
 
17682
17714
  if (targetY === upDirection && childNodeHeight > anchorPosY) {
17683
- childNode.style.height = `${anchorPosY}px`;
17715
+ clampHeight(childNode, anchorPosY);
17684
17716
  }
17685
17717
 
17686
17718
  if (targetY === downDirection && childNodeHeight + anchorPosY > clientHeight) {
17687
17719
  const heightWithTopIndent = childNodeHeight + anchorPosY;
17688
17720
  const delta = heightWithTopIndent - clientHeight;
17689
- childNode.style.height = `${childNodeHeight - delta - anchorIndent}px`;
17721
+ clampHeight(childNode, childNodeHeight - delta - anchorIndent);
17690
17722
  }
17691
17723
  };
17692
-
17693
17724
  const getOriginPosition = (flip, origin) => flip ? flipPosition(flip, positions[origin]) : positions[origin];
17694
17725
  const setPopupContainerPosition = (anchorNode, node, _ref6, animateAppearance, animateX, animateY) => {
17695
17726
  let {
@@ -17726,6 +17757,8 @@ class Popover extends Component {
17726
17757
  this.flip = null; // Click inside react tree on case when children uses Portal
17727
17758
 
17728
17759
  this.childrenClicked = false;
17760
+ this.contentObservers = [];
17761
+ this.heightSyncFrame = null;
17729
17762
 
17730
17763
  this.saveAnchor = ref => {
17731
17764
  this.anchorNode = ref;
@@ -17738,6 +17771,73 @@ class Popover extends Component {
17738
17771
 
17739
17772
  this.saveContainer = ref => {
17740
17773
  this.containerNode = ref;
17774
+ this.observeContent(ref);
17775
+ };
17776
+ /**
17777
+ * Содержимое меняет размер и после открытия: форма раскрывает новые поля, список догружается.
17778
+ * Обрезку по окну пересчитываем вслед за ним, а не только на открытии и ресайзе окна — иначе
17779
+ * выросшее содержимое уезжало бы за край экрана.
17780
+ *
17781
+ * Размер контейнера ловит ResizeObserver, но под обрезкой он замирает: высота ребёнка задана
17782
+ * явно, и рост или сжатие содержимого контейнер уже не меняют. Эти случаи ловит MutationObserver —
17783
+ * по вставке и удалению узлов, то есть ровно когда форма меняет свой состав.
17784
+ */
17785
+
17786
+
17787
+ this.observeContent = node => {
17788
+ this.disconnectContent();
17789
+ if (!node) return;
17790
+ const {
17791
+ ResizeObserver: ResizeObserverClass
17792
+ } = window;
17793
+
17794
+ if (ResizeObserverClass) {
17795
+ const resizeObserver = new ResizeObserverClass(this.scheduleHeightSync);
17796
+ resizeObserver.observe(node);
17797
+ this.contentObservers.push(resizeObserver);
17798
+ }
17799
+
17800
+ if (typeof MutationObserver !== "undefined") {
17801
+ const mutationObserver = new MutationObserver(this.scheduleHeightSync);
17802
+ mutationObserver.observe(node, {
17803
+ childList: true,
17804
+ subtree: true
17805
+ });
17806
+ this.contentObservers.push(mutationObserver);
17807
+ }
17808
+ };
17809
+
17810
+ this.disconnectContent = () => {
17811
+ this.contentObservers.forEach(observer => observer.disconnect());
17812
+ this.contentObservers = [];
17813
+
17814
+ if (this.heightSyncFrame !== null) {
17815
+ cancelAnimationFrame(this.heightSyncFrame);
17816
+ this.heightSyncFrame = null;
17817
+ }
17818
+ };
17819
+ /**
17820
+ * Пересчёт — в следующем кадре, а не прямо в колбэке: правка высоты внутри колбэка
17821
+ * ResizeObserver зациклила бы его («ResizeObserver loop completed…»), а пачка мутаций одного
17822
+ * рендера схлопывается в один пересчёт.
17823
+ */
17824
+
17825
+
17826
+ this.scheduleHeightSync = () => {
17827
+ if (this.heightSyncFrame !== null) return;
17828
+ this.heightSyncFrame = requestAnimationFrame(() => {
17829
+ this.heightSyncFrame = null;
17830
+ this.syncContainerHeight();
17831
+ });
17832
+ }; // Только высота: полный `setPositions` заново проигрывал бы анимацию появления на каждом изменении.
17833
+
17834
+
17835
+ this.syncContainerHeight = () => {
17836
+ if (!this.anchorNode || !this.containerNode) return;
17837
+ const {
17838
+ targetOriginPosition: [, targetY]
17839
+ } = this.getContainerPosition(this.anchorNode, this.containerNode);
17840
+ setPopupContainerHeight(this.anchorNode, this.containerNode, targetY);
17741
17841
  };
17742
17842
 
17743
17843
  this.getContainerPosition = (anchorNode, containerNode) => {
@@ -17840,6 +17940,7 @@ class Popover extends Component {
17840
17940
 
17841
17941
  componentWillUnmount() {
17842
17942
  this.removeListeners();
17943
+ this.disconnectContent();
17843
17944
  }
17844
17945
 
17845
17946
  componentDidUpdate(prevProps) {