From 4adb6bbf040d1cb503332c8177c235de72be2a46 Mon Sep 17 00:00:00 2001
From: Cory LaViska <cory@abeautifulsite.net>
Date: Thu, 29 Oct 2020 18:15:48 -0400
Subject: [PATCH] Add experimental resize observer

---
 docs/_sidebar.md                              |  1 +
 docs/components/resize-observer.md            | 46 +++++++++++++++++
 docs/getting-started/changelog.md             |  3 +-
 src/components.d.ts                           | 17 +++++++
 .../resize-observer/resize-observer.scss      |  5 ++
 .../resize-observer/resize-observer.tsx       | 50 +++++++++++++++++++
 6 files changed, 121 insertions(+), 1 deletion(-)
 create mode 100644 docs/components/resize-observer.md
 create mode 100644 src/components/resize-observer/resize-observer.scss
 create mode 100644 src/components/resize-observer/resize-observer.tsx

diff --git a/docs/_sidebar.md b/docs/_sidebar.md
index da14ee03..a4cfb1b1 100644
--- a/docs/_sidebar.md
+++ b/docs/_sidebar.md
@@ -51,6 +51,7 @@
   - [Animation](/components/animation.md)
   - [Format Bytes](/components/format-bytes.md)
   - [Include](/components/include.md)
+  - [Resize Observer](/components/resize-observer.md)
   - [Theme](/components/theme.md)
 
 - Design Tokens
diff --git a/docs/components/resize-observer.md b/docs/components/resize-observer.md
new file mode 100644
index 00000000..738df98d
--- /dev/null
+++ b/docs/components/resize-observer.md
@@ -0,0 +1,46 @@
+# Resize Observer
+
+[component-header:sl-resize-observer]
+
+Resize observers offer a thin, declarative interface to the [`ResizeObserver API`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver).
+
+The resize observer will report changes to the dimensions of the elements it wraps through the `sl-resize` event. When emitted, a collection of [`ResizeObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry) objects will be attached to `event.detail`, containing the target element and information about its dimensions.
+
+```html preview
+<div class="resize-observer-overview">
+  <sl-resize-observer>
+    <div>
+      Resize me and watch the console
+    </div>
+  </sl-resize-observer>
+</div>
+
+<script>
+  const container = document.querySelector('.resize-observer-overview');
+  const resizeObserver = container.querySelector('sl-resize-observer');
+
+  resizeObserver.addEventListener('sl-resize', event => {
+    console.log(event);
+  });
+</script>
+
+<style>
+  .resize-observer-overview div {
+    display: flex; 
+    width: 12rem;
+    min-height: 12rem;
+    min-width: 12rem;
+    max-width: 100%; 
+    max-height: 50vh;
+    border: solid 2px var(--sl-input-border-color); 
+    resize: both; 
+    overflow: auto; 
+    align-items: center; 
+    justify-content: center;
+    text-align: center;
+    padding: 1rem;
+  }
+</style>
+```
+
+[component-metadata:sl-resize-observer]
diff --git a/docs/getting-started/changelog.md b/docs/getting-started/changelog.md
index 47bcfd8a..981975bb 100644
--- a/docs/getting-started/changelog.md
+++ b/docs/getting-started/changelog.md
@@ -8,7 +8,8 @@ _During the beta period, these restrictions may be relaxed in the event of a mis
 
 ## Next
 
-- Added `sl-theme` utility and updated theming documentation
+- Added experimental `sl-resize-observer` utility
+- Added experimental `sl-theme` utility and updated theming documentation
 - Fixed a bug where `sl-menu-item` wouldn't render properly in the dark theme
 - Improved placeholder contrast in dark theme
 - Updated Boostrap Icons to 1.1.0
diff --git a/src/components.d.ts b/src/components.d.ts
index 4dff527d..632f36c4 100644
--- a/src/components.d.ts
+++ b/src/components.d.ts
@@ -851,6 +851,8 @@ export namespace Components {
          */
         "value": number;
     }
+    interface SlResizeObserver {
+    }
     interface SlResponsiveEmbed {
         /**
           * The aspect ratio of the embedded media in the format of `width:height`, e.g. `16:9`, `4:3`, or `1:1`. Ratios not in this format will be ignored.
@@ -1376,6 +1378,12 @@ declare global {
         prototype: HTMLSlRatingElement;
         new (): HTMLSlRatingElement;
     };
+    interface HTMLSlResizeObserverElement extends Components.SlResizeObserver, HTMLStencilElement {
+    }
+    var HTMLSlResizeObserverElement: {
+        prototype: HTMLSlResizeObserverElement;
+        new (): HTMLSlResizeObserverElement;
+    };
     interface HTMLSlResponsiveEmbedElement extends Components.SlResponsiveEmbed, HTMLStencilElement {
     }
     var HTMLSlResponsiveEmbedElement: {
@@ -1479,6 +1487,7 @@ declare global {
         "sl-radio": HTMLSlRadioElement;
         "sl-range": HTMLSlRangeElement;
         "sl-rating": HTMLSlRatingElement;
+        "sl-resize-observer": HTMLSlResizeObserverElement;
         "sl-responsive-embed": HTMLSlResponsiveEmbedElement;
         "sl-select": HTMLSlSelectElement;
         "sl-skeleton": HTMLSlSkeletonElement;
@@ -2365,6 +2374,12 @@ declare namespace LocalJSX {
          */
         "value"?: number;
     }
+    interface SlResizeObserver {
+        /**
+          * Emitted when the element is resized.
+         */
+        "onSl-resize"?: (event: CustomEvent<ResizeObserverEntry[]>) => void;
+    }
     interface SlResponsiveEmbed {
         /**
           * The aspect ratio of the embedded media in the format of `width:height`, e.g. `16:9`, `4:3`, or `1:1`. Ratios not in this format will be ignored.
@@ -2735,6 +2750,7 @@ declare namespace LocalJSX {
         "sl-radio": SlRadio;
         "sl-range": SlRange;
         "sl-rating": SlRating;
+        "sl-resize-observer": SlResizeObserver;
         "sl-responsive-embed": SlResponsiveEmbed;
         "sl-select": SlSelect;
         "sl-skeleton": SlSkeleton;
@@ -2783,6 +2799,7 @@ declare module "@stencil/core" {
             "sl-radio": LocalJSX.SlRadio & JSXBase.HTMLAttributes<HTMLSlRadioElement>;
             "sl-range": LocalJSX.SlRange & JSXBase.HTMLAttributes<HTMLSlRangeElement>;
             "sl-rating": LocalJSX.SlRating & JSXBase.HTMLAttributes<HTMLSlRatingElement>;
+            "sl-resize-observer": LocalJSX.SlResizeObserver & JSXBase.HTMLAttributes<HTMLSlResizeObserverElement>;
             "sl-responsive-embed": LocalJSX.SlResponsiveEmbed & JSXBase.HTMLAttributes<HTMLSlResponsiveEmbedElement>;
             "sl-select": LocalJSX.SlSelect & JSXBase.HTMLAttributes<HTMLSlSelectElement>;
             "sl-skeleton": LocalJSX.SlSkeleton & JSXBase.HTMLAttributes<HTMLSlSkeletonElement>;
diff --git a/src/components/resize-observer/resize-observer.scss b/src/components/resize-observer/resize-observer.scss
new file mode 100644
index 00000000..03be999b
--- /dev/null
+++ b/src/components/resize-observer/resize-observer.scss
@@ -0,0 +1,5 @@
+@import 'component';
+
+:host {
+  display: contents;
+}
diff --git a/src/components/resize-observer/resize-observer.tsx b/src/components/resize-observer/resize-observer.tsx
new file mode 100644
index 00000000..1c70d916
--- /dev/null
+++ b/src/components/resize-observer/resize-observer.tsx
@@ -0,0 +1,50 @@
+import { Component, Element, Event, EventEmitter, h } from '@stencil/core';
+import ResizeObserver from 'resize-observer-polyfill';
+
+/**
+ * @since 2.0
+ * @status experimental
+ */
+
+@Component({
+  tag: 'sl-resize-observer',
+  styleUrl: 'resize-observer.scss',
+  shadow: true
+})
+export class ResizeObserverUtility {
+  resizeObserver: ResizeObserver;
+  observedElements: HTMLElement[] = [];
+
+  @Element() host: HTMLSlResizeObserverElement;
+
+  /** Emitted when the element is resized. */
+  @Event({ eventName: 'sl-resize' }) slResize: EventEmitter<ResizeObserverEntry[]>;
+
+  connectedCallback() {
+    this.resizeObserver = new ResizeObserver(event => this.slResize.emit(event));
+    this.handleSlotChange = this.handleSlotChange.bind(this);
+  }
+
+  disconnectedCallback() {
+    this.resizeObserver.disconnect();
+  }
+
+  handleSlotChange() {
+    const slot = this.host.shadowRoot.querySelector('slot');
+    const elements = slot.assignedElements({ flatten: true }) as HTMLElement[];
+
+    // Unwatch previous elements
+    this.observedElements.map(el => this.resizeObserver.unobserve(el));
+    this.observedElements = [];
+
+    // Watch new elements
+    elements.map(el => {
+      this.resizeObserver.observe(el);
+      this.observedElements.push(el);
+    });
+  }
+
+  render() {
+    return <slot onSlotchange={this.handleSlotChange} />;
+  }
+}