From 93928420b59d78b9e7db4dd08bb5bbc107047d01 Mon Sep 17 00:00:00 2001 From: Ofer Shaal Date: Mon, 22 May 2023 05:07:53 +0000 Subject: [PATCH 1/4] feat: initial resize-controller as package --- .../controllers/resize-controller/README.md | 94 +++++++++ .../controllers/resize-controller/index.ts | 1 + .../resize-controller/package.json | 41 ++++ .../src/resize-controller.ts | 186 ++++++++++++++++++ .../resize-controller/tsconfig.build.json | 10 + 5 files changed, 332 insertions(+) create mode 100644 packages/controllers/resize-controller/README.md create mode 100644 packages/controllers/resize-controller/index.ts create mode 100644 packages/controllers/resize-controller/package.json create mode 100644 packages/controllers/resize-controller/src/resize-controller.ts create mode 100644 packages/controllers/resize-controller/tsconfig.build.json diff --git a/packages/controllers/resize-controller/README.md b/packages/controllers/resize-controller/README.md new file mode 100644 index 000000000..7e895f6cf --- /dev/null +++ b/packages/controllers/resize-controller/README.md @@ -0,0 +1,94 @@ +# Resize Controller + +The `resize-controller` is a utility module that allows you to observe and react to changes in the size of a web component. It provides a simple way to handle resize events and perform actions based on the new size of the component. + +## Installation + +You can install the `resize-controller` package using yarn: + +``` +yarn add -D @phase2/resize-controller +``` + +## Usage + +To use the `resize-controller` in your web component, follow these steps: + +1. Import the necessary classes and functions from the `lit` package: + +```javascript +import { ResizeController } from 'resize-controller'; +``` + +2. Create an instance of the `ResizeController` and pass the host element and options: + +```javascript +resizeController = new ResizeController(this); +``` + +## API Reference + +### `ResizeController` + +The `ResizeController` class provides methods to observe resize events and perform actions based on the new size of the host element. + +#### Constructor + +```javascript +new ResizeController(host: ReactiveControllerHost & HTMLElement, options?: ResizeControllerOptions) +``` + +- `host`: The host element of the web component. +- `options` (optional): An object specifying the options for the `ResizeController`. It can include the following properties: + - `debounce`: The delay in milliseconds to debounce the resize event. Defaults to `200`. + - `breakpoints`: An array of breakpoints for different size ranges. Defaults to `[768]`. + - `elementToRerender`: The element to trigger a re-render when the size changes. Defaults to the `host` element. + +#### Properties + +- `onResize`: A callback function that will be called when the element is resized. Override this method in your component to handle the resize event. + +#### Methods + +- `hostConnected()`: Called when the host element is connected to the DOM. Observes the element for size changes. +- `hostDisconnected()`: Called when the host element is disconnected from the DOM. Stops observing size changes. + +## Example + +Here's an example that demonstrates how to use the `resize-controller` in a web component: + +```javascript +@customElement('my-component') +export class MyComponent extends LitElement { + resizeController = new ResizeController(this, { + breakpoints: [768, 1440], + }); + + render() { + const Classes = { + 'mobile': this.resizeController.currentBreakpointRange === 0, + 'medium': this.resizeController.currentBreakpointRange === 1, + 'large': this.resizeController.currentBreakpointRange === 2, + }; + return html` +
+ Hello World +
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'my-component': MyComponent; + } +} +``` + +In this example, `resizeController` is initialized to support the following breakpoints: + +- 0: 0-767px +- 1: 768px-1439px +- 2: 1440px - 100000px + +When my-component's width crosses from one range to another, the resize controller will call the component's `render()` function. diff --git a/packages/controllers/resize-controller/index.ts b/packages/controllers/resize-controller/index.ts new file mode 100644 index 000000000..726f3c8f8 --- /dev/null +++ b/packages/controllers/resize-controller/index.ts @@ -0,0 +1 @@ +export { ResizeController } from './src/resize-controller'; diff --git a/packages/controllers/resize-controller/package.json b/packages/controllers/resize-controller/package.json new file mode 100644 index 000000000..a4f9a62d8 --- /dev/null +++ b/packages/controllers/resize-controller/package.json @@ -0,0 +1,41 @@ +{ + "name": "@phase2/outline-controller-resize-controller", + "version": "0.0.1", + "description": "Controller to help with managing classes / markup updates based on component's width", + "keywords": [ + "outline components", + "outline design", + "resize" + ], + "main": "index.ts", + "types": "index.ts", + "typings": "index.d.ts", + "files": [ + "/dist/", + "/src/", + "!/dist/tsconfig.build.tsbuildinfo" + ], + "author": "Phase2 Technology", + "repository": { + "type": "git", + "url": "https://github.com/phase2/outline.git", + "directory": "packages/controllers/resize-controller" + }, + "license": "BSD-3-Clause", + "scripts": { + "build": "node ../../../scripts/build.js", + "package": "yarn publish" + }, + "dependencies": { + "lit": "^2.3.1" + }, + "devDependencies": { + "tslib": "^2.1.0" + }, + "publishConfig": { + "access": "public" + }, + "exports": { + ".": "./index.ts" + } + } diff --git a/packages/controllers/resize-controller/src/resize-controller.ts b/packages/controllers/resize-controller/src/resize-controller.ts new file mode 100644 index 000000000..acfe8f160 --- /dev/null +++ b/packages/controllers/resize-controller/src/resize-controller.ts @@ -0,0 +1,186 @@ +import { ReactiveControllerHost, ReactiveController } from 'lit'; + +/** + * Debounces a function + * @template T + * @param {T} func - The function to debounce + * @param {number} delay - The delay in milliseconds + * @param {boolean} [immediate=false] - Whether to execute the function immediately + * @returns {(...args: Parameters) => void} - The debounced function + */ +export const debounce = ) => void>( + func: T, + delay: number, + immediate = false +): ((...args: Parameters) => void) => { + let timeoutId: ReturnType | undefined = undefined; + + return function debounced(...args: Parameters) { + const executeFunc = () => func(...args); + + clearTimeout(timeoutId); + + if (immediate && timeoutId === undefined) { + executeFunc(); + } + + timeoutId = setTimeout(executeFunc, delay); + }; +}; + +export type breakpointsRangeType = { + min: number; + max: number; +}; + +/** + * ResizeController class + * @implements {ReactiveController} + */ +export class ResizeController implements ReactiveController { + host: ReactiveControllerHost & HTMLElement; + resizeObserver: ResizeObserver; + elementToObserve: Element; + options: { + debounce: number; + breakpoints: number[]; + elementToRerender: ReactiveControllerHost & HTMLElement; + }; + currentComponentWidth: number; + currentBreakpointRange: number; + breakpointsRangeArray: breakpointsRangeType[] = []; + + /** + * Create a constructor that takes a host and options + * @param {ReactiveControllerHost & Element} host - The host element + * @param {{debounce?: number; breakpoints?: number[]}} [options={}] - The options object + */ + constructor( + host: ReactiveControllerHost & HTMLElement, + options: { + debounce?: number; + breakpoints?: number[]; + elementToRerender?: ReactiveControllerHost & HTMLElement; + } = {} + ) { + const defaultOptions = { + debounce: 200, + breakpoints: [768], + elementToRerender: host, + }; + + /** + * Remove any undefined variables from options object + */ + const filteredOptionsObject = Object.fromEntries( + Object.entries(options).filter(([_, value]) => value !== undefined) + ); + this.options = { ...defaultOptions, ...filteredOptionsObject }; + + this.host = host; + this.host.addController(this); + + this.initializeBreakpointsRangeType(); + } + + /** + * Initialize the breakpoints range array + * + * The default breakpoints array ([768]) will create this breakpoints range array: + * [{min: 0, max: 767}, {min: 768, max: 100000}] + * + * If custom breakpoints array is provided, (for example [768, 1200, 2000]) this breakpoints range array will be created: + * [{min: 0, max: 767}, {min: 768, max: 1199}, {min: 1200, max: 1999}, {min: 2000, max: 100000}] + * + */ + initializeBreakpointsRangeType() { + // This will allow create an additional breakpoint from the last custom breakpoint to 100000 + this.options.breakpoints?.push(100000); + + let minBreakpoint = 0; + this.options.breakpoints?.forEach(breakpoint => { + const newBreakpointRange = { + min: minBreakpoint, + max: breakpoint - 1, + }; + minBreakpoint = breakpoint; + this.breakpointsRangeArray.push(newBreakpointRange); + }); + } + + /** + * Called when the host element is connected to the DOM + */ + hostConnected() { + if (!this.host.style.display) { + // adding `display: block` to :host of component + this.host.style.setProperty( + 'display', + 'var(--style-added-by-resize-controller, block)' + ); + } + + // Create a new ResizeObserver and pass in the function to be called when the element is resized + this.resizeObserver = new ResizeObserver( + (entries: ResizeObserverEntry[]) => { + // Create a debounced version of the onElementResize function + debounce( + this.onElementResize.bind(this), + this.options.debounce + )(entries); + } + ); + + // Get a reference to the element you want to observe + this.elementToObserve = this.host; + + // Observe the element for size changes + this.resizeObserver.observe(this.elementToObserve); + } + + /** + * Called when the host element is disconnected from the DOM + */ + hostDisconnected() { + this.resizeObserver.disconnect(); + } + + /** + * Called when the element is resized + * @param {ResizeObserverEntry[]} _entries - The ResizeObserverEntry array + */ + onElementResize(_entries: ResizeObserverEntry[]) { + this.currentComponentWidth = _entries[0].contentRect.width; + + // skip if width is not yet set + if (this.currentComponentWidth) { + this.calculateNewBreakpointRange(); + } else if (this.currentComponentWidth === 0) { + // eslint-disable-next-line no-console + console.warn( + `resize-controller: No width detected in <${this.host.localName}>. Please confirm it has display: block` + ); + } + } + + /** + * Calculate the new breakpoint based on the current width + */ + calculateNewBreakpointRange() { + let newBreakpointRange = this.currentBreakpointRange; + + this.breakpointsRangeArray.forEach((breakpoint, index) => { + if ( + this.currentComponentWidth >= breakpoint.min && + this.currentComponentWidth <= breakpoint.max + ) { + newBreakpointRange = index; + } + }); + + if (newBreakpointRange !== this.currentBreakpointRange) { + this.currentBreakpointRange = newBreakpointRange; + this.options.elementToRerender.requestUpdate(); + } + } +} diff --git a/packages/controllers/resize-controller/tsconfig.build.json b/packages/controllers/resize-controller/tsconfig.build.json new file mode 100644 index 000000000..fac75484a --- /dev/null +++ b/packages/controllers/resize-controller/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "outDir": "./dist" + }, + "include": ["index.ts", "src/**/*", "tests/**/*"], + "references": [{ "path": "../../outline-core/tsconfig.build.json" }] + } + \ No newline at end of file From 1a994c88fa0ceea0b706478c62bc288f384c6765 Mon Sep 17 00:00:00 2001 From: Jake Strawn Date: Mon, 22 May 2023 14:06:54 -0400 Subject: [PATCH 2/4] fix(prettier): Fixing improper indentations. --- .../resize-controller/package.json | 78 +++++++++---------- .../resize-controller/tsconfig.build.json | 17 ++-- 2 files changed, 47 insertions(+), 48 deletions(-) diff --git a/packages/controllers/resize-controller/package.json b/packages/controllers/resize-controller/package.json index a4f9a62d8..e8f16e6f2 100644 --- a/packages/controllers/resize-controller/package.json +++ b/packages/controllers/resize-controller/package.json @@ -1,41 +1,41 @@ { - "name": "@phase2/outline-controller-resize-controller", - "version": "0.0.1", - "description": "Controller to help with managing classes / markup updates based on component's width", - "keywords": [ - "outline components", - "outline design", - "resize" - ], - "main": "index.ts", - "types": "index.ts", - "typings": "index.d.ts", - "files": [ - "/dist/", - "/src/", - "!/dist/tsconfig.build.tsbuildinfo" - ], - "author": "Phase2 Technology", - "repository": { - "type": "git", - "url": "https://github.com/phase2/outline.git", - "directory": "packages/controllers/resize-controller" - }, - "license": "BSD-3-Clause", - "scripts": { - "build": "node ../../../scripts/build.js", - "package": "yarn publish" - }, - "dependencies": { - "lit": "^2.3.1" - }, - "devDependencies": { - "tslib": "^2.1.0" - }, - "publishConfig": { - "access": "public" - }, - "exports": { - ".": "./index.ts" - } + "name": "@phase2/outline-controller-resize-controller", + "version": "0.0.1", + "description": "Controller to help with managing classes / markup updates based on component's width", + "keywords": [ + "outline components", + "outline design", + "resize" + ], + "main": "index.ts", + "types": "index.ts", + "typings": "index.d.ts", + "files": [ + "/dist/", + "/src/", + "!/dist/tsconfig.build.tsbuildinfo" + ], + "author": "Phase2 Technology", + "repository": { + "type": "git", + "url": "https://github.com/phase2/outline.git", + "directory": "packages/controllers/resize-controller" + }, + "license": "BSD-3-Clause", + "scripts": { + "build": "node ../../../scripts/build.js", + "package": "yarn publish" + }, + "dependencies": { + "lit": "^2.3.1" + }, + "devDependencies": { + "tslib": "^2.1.0" + }, + "publishConfig": { + "access": "public" + }, + "exports": { + ".": "./index.ts" } +} diff --git a/packages/controllers/resize-controller/tsconfig.build.json b/packages/controllers/resize-controller/tsconfig.build.json index fac75484a..5eac9d313 100644 --- a/packages/controllers/resize-controller/tsconfig.build.json +++ b/packages/controllers/resize-controller/tsconfig.build.json @@ -1,10 +1,9 @@ { - "extends": "../../../tsconfig.json", - "compilerOptions": { - "rootDir": ".", - "outDir": "./dist" - }, - "include": ["index.ts", "src/**/*", "tests/**/*"], - "references": [{ "path": "../../outline-core/tsconfig.build.json" }] - } - \ No newline at end of file + "extends": "../../../tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "outDir": "./dist" + }, + "include": ["index.ts", "src/**/*", "tests/**/*"], + "references": [{ "path": "../../outline-core/tsconfig.build.json" }] +} From b769a0c5f9f4d591a9f71ffdb3ac82da81d5776e Mon Sep 17 00:00:00 2001 From: Morgan Smith Date: Tue, 6 Feb 2024 09:51:30 -0500 Subject: [PATCH 3/4] Update package.json --- packages/controllers/resize-controller/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/controllers/resize-controller/package.json b/packages/controllers/resize-controller/package.json index e8f16e6f2..a2ce96877 100644 --- a/packages/controllers/resize-controller/package.json +++ b/packages/controllers/resize-controller/package.json @@ -1,6 +1,6 @@ { "name": "@phase2/outline-controller-resize-controller", - "version": "0.0.1", + "version": "0.0.0", "description": "Controller to help with managing classes / markup updates based on component's width", "keywords": [ "outline components", From 29ff7ba80b4c12dca75b85a0fd038b0d93e108a8 Mon Sep 17 00:00:00 2001 From: Morgan Smith Date: Tue, 6 Feb 2024 09:52:34 -0500 Subject: [PATCH 4/4] Update README.md --- packages/controllers/resize-controller/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/controllers/resize-controller/README.md b/packages/controllers/resize-controller/README.md index 7e895f6cf..62006747c 100644 --- a/packages/controllers/resize-controller/README.md +++ b/packages/controllers/resize-controller/README.md @@ -7,7 +7,7 @@ The `resize-controller` is a utility module that allows you to observe and react You can install the `resize-controller` package using yarn: ``` -yarn add -D @phase2/resize-controller +yarn add -D @phase2/outline-controller-resize-controller ``` ## Usage @@ -17,7 +17,7 @@ To use the `resize-controller` in your web component, follow these steps: 1. Import the necessary classes and functions from the `lit` package: ```javascript -import { ResizeController } from 'resize-controller'; +import { ResizeController } from '@phase2/outline-controller-resize-controller'; ``` 2. Create an instance of the `ResizeController` and pass the host element and options: