-
Notifications
You must be signed in to change notification settings - Fork 27
Controller: @phase2/outline-controller-resize-controller
#393
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: next
Are you sure you want to change the base?
Changes from all commits
9392842
18d869a
1a994c8
a6e25a3
b769a0c
29ff7ba
5b3724a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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/outline-controller-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 '@phase2/outline-controller-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` | ||
<div class="${classMap(Classes)}"> | ||
Hello World | ||
</div> | ||
`; | ||
} | ||
} | ||
|
||
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. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { ResizeController } from './src/resize-controller'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
{ | ||
"name": "@phase2/outline-controller-resize-controller", | ||
"version": "0.0.0", | ||
"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" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -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<T>) => void} - The debounced function | ||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||
export const debounce = <T extends (...args: Parameters<T>) => void>( | ||||||||||||||||||||||||||||||||||||||||||
func: T, | ||||||||||||||||||||||||||||||||||||||||||
delay: number, | ||||||||||||||||||||||||||||||||||||||||||
immediate = false | ||||||||||||||||||||||||||||||||||||||||||
): ((...args: Parameters<T>) => void) => { | ||||||||||||||||||||||||||||||||||||||||||
let timeoutId: ReturnType<typeof setTimeout> | undefined = undefined; | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
return function debounced(...args: Parameters<T>) { | ||||||||||||||||||||||||||||||||||||||||||
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); | ||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Modifying the options object directly by pushing to breakpoints array can lead to unexpected side effects. - this.options.breakpoints?.push(100000);
+ const extendedBreakpoints = [...this.options.breakpoints, 100000];
+ this.options = { ...this.options, breakpoints: extendedBreakpoints }; Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
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)' | ||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+115
to
+120
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Setting Consider using a more flexible approach or documenting this behavior clearly to inform users. |
||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
// 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); | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+123
to
+132
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The debounced function is recreated on every resize event, which negates the purpose of debouncing. Move the debounced function creation outside of the ResizeObserver callback to ensure it's created only once. + const debouncedOnElementResize = debounce(
+ this.onElementResize.bind(this),
+ this.options.debounce
+ );
this.resizeObserver = new ResizeObserver(
(entries: ResizeObserverEntry[]) => {
- debounce(
- this.onElementResize.bind(this),
- this.options.debounce
- )(entries);
+ debouncedOnElementResize(entries);
}
); Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
// 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` | ||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+158
to
+163
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Logging a warning to the console for a width of 0 may not always indicate an issue, especially for initially hidden elements. Consider a more robust way to handle or document this scenario to avoid confusion. |
||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||
* 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(); | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"extends": "../../../tsconfig.json", | ||
"compilerOptions": { | ||
"rootDir": ".", | ||
"outDir": "./dist" | ||
}, | ||
"include": ["index.ts", "src/**/*", "tests/**/*"], | ||
"references": [{ "path": "../../outline-core/tsconfig.build.json" }] | ||
} |
Uh oh!
There was an error while loading. Please reload this page.