index.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. import React from 'react';
  2. import { findDOMNode } from 'react-dom';
  3. import PropTypes from 'prop-types';
  4. import BaseComponent, { BaseProps } from '../_base/baseComponent';
  5. /** A parallel type to `ResizeObserverEntry` (from resize-observer-polyfill). */
  6. export interface ResizeEntry {
  7. contentRect: DOMRectReadOnly;
  8. target: Element
  9. }
  10. export interface ReactResizeObserverProps extends BaseProps {
  11. onResize?: (entries: ResizeEntry[]) => void;
  12. observeParent?: boolean;
  13. observerProperty?: ObserverProperty;
  14. delayTick?: number
  15. }
  16. export enum ObserverProperty {
  17. Width='width',
  18. Height = "height",
  19. All = "all"
  20. }
  21. export default class ReactResizeObserver extends BaseComponent<ReactResizeObserverProps> {
  22. static propTypes = {
  23. onResize: PropTypes.func,
  24. observeParent: PropTypes.bool,
  25. observerProperty: PropTypes.string,
  26. delayTick: PropTypes.number
  27. };
  28. static defaultProps = {
  29. onResize: () => {}, // eslint-disable-line
  30. observeParent: false,
  31. observerProperty: "all",
  32. delayTick: 0
  33. };
  34. observer: ResizeObserver;
  35. childNode: any;
  36. element: Element;
  37. _parentNode: HTMLElement;
  38. formerPropertyValue: Map<Element, number> = new Map()
  39. constructor(props: ReactResizeObserverProps) {
  40. super(props);
  41. if (globalThis['ResizeObserver']) {
  42. this.observer = new ResizeObserver(this.handleResizeEventTriggered);
  43. }
  44. }
  45. componentDidMount() {
  46. this.observeElement?.();
  47. }
  48. componentDidUpdate(prevProps: ReactResizeObserverProps) {
  49. this.observeElement?.(this.props.observeParent !== prevProps.observeParent);
  50. }
  51. componentWillUnmount() {
  52. if (this.observer) {
  53. this.observer.disconnect();
  54. this.observer = null;
  55. this.element = null;
  56. }
  57. }
  58. getElement = () => {
  59. try {
  60. // using findDOMNode for two reasons:
  61. // 1. cloning to insert a ref is unwieldy and not performant.
  62. // 2. ensure that we resolve to an actual DOM node (instead of any JSX ref instance).
  63. return findDOMNode(this.childNode || this);
  64. } catch (error) {
  65. // swallow error if findDOMNode is run on unmounted component.
  66. return null;
  67. }
  68. };
  69. handleResizeEventTriggered = (entries: ResizeEntry[])=>{
  70. if (this.props.observerProperty === ObserverProperty.All) {
  71. this.props.onResize?.(entries);
  72. } else {
  73. const finalEntries: ResizeEntry[] = [];
  74. for (const entry of entries) {
  75. if (this.formerPropertyValue.has(entry.target)) {
  76. if (entry.contentRect[this.props.observerProperty]!==this.formerPropertyValue.get(entry.target)) {
  77. this.formerPropertyValue.set(entry.target, entry.contentRect[this.props.observerProperty]);
  78. finalEntries.push(entry);
  79. }
  80. } else {
  81. this.formerPropertyValue.set(entry.target, entry.contentRect[this.props.observerProperty]);
  82. finalEntries.push(entry);
  83. }
  84. }
  85. if (finalEntries.length>0) {
  86. this.props.onResize?.(finalEntries);
  87. }
  88. }
  89. }
  90. observeElement = (force = false)=>{
  91. const element = this.getElement();
  92. if (!this.observer) {
  93. this.observer = new ResizeObserver(this.handleResizeEventTriggered);
  94. }
  95. if (!(element && element instanceof Element)) {
  96. // stop everything if not defined
  97. this.observer.disconnect();
  98. return;
  99. }
  100. if (element === this.element && !force) {
  101. // abort if given same element -- nothing to update (unless forced)
  102. return;
  103. } else {
  104. // clear observer list if new element
  105. this.observer.disconnect();
  106. // remember element reference for next time
  107. this.element = element;
  108. }
  109. // observer callback is invoked immediately when observing new elements
  110. this.observer.observe(element);
  111. if (
  112. this.props.observeParent &&
  113. element.parentNode &&
  114. element.parentNode.ownerDocument &&
  115. element.parentNode.ownerDocument.defaultView &&
  116. element.parentNode instanceof element.parentNode.ownerDocument.defaultView.HTMLElement
  117. ) {
  118. this._parentNode = element.parentNode;
  119. this.observer.observe(this._parentNode);
  120. }
  121. }
  122. mergeRef = (ref: any, node: HTMLDivElement) => {
  123. this.childNode = node;
  124. if (typeof ref === 'function') {
  125. ref(node);
  126. } else if (typeof ref === 'object' && ref && 'current' in ref) {
  127. ref.current = node;
  128. }
  129. };
  130. render() {
  131. const child = React.Children.only(this.props.children);
  132. const { ref } = child as any;
  133. return React.cloneElement(child as React.ReactElement, {
  134. ref: (node: HTMLDivElement) => this.mergeRef(ref, node),
  135. });
  136. }
  137. }