Skip to content

Commit

Permalink
feat: resize textarea
Browse files Browse the repository at this point in the history
  • Loading branch information
segunadebayo committed Oct 12, 2024
1 parent a58f2d0 commit 253603b
Show file tree
Hide file tree
Showing 8 changed files with 11,177 additions and 8,460 deletions.
5 changes: 5 additions & 0 deletions .changeset/twelve-llamas-yawn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@zag-js/auto-resize": minor
---

Add support for resizing textarea
24 changes: 24 additions & 0 deletions examples/next-ts/pages/autoresize.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { autoresizeTextarea } from "@zag-js/auto-resize"
import { useEffect, useRef } from "react"

export default function Autoresize() {
const textareaRef = useRef<HTMLTextAreaElement>(null)
useEffect(() => {
return autoresizeTextarea(textareaRef.current)
}, [])
return (
<main>
<textarea
ref={textareaRef}
rows={4}
style={{
width: "100%",
resize: "none",
padding: 20,
scrollPaddingBlock: 20,
maxHeight: 180,
}}
/>
</main>
)
}
2 changes: 1 addition & 1 deletion examples/next-ts/pages/compositions
42 changes: 42 additions & 0 deletions packages/utilities/auto-resize/src/autoresize-input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { getDocument, getWindow } from "@zag-js/dom-query"
import { getVisualStyles } from "./visual-style"

function createGhostElement(doc: Document) {
var el = doc.createElement("div")
el.id = "ghost"
el.style.cssText =
"display:inline-block;height:0;overflow:hidden;position:absolute;top:0;visibility:hidden;white-space:nowrap;"
doc.body.appendChild(el)
return el
}

export function autoResizeInput(input: HTMLInputElement | null) {
if (!input) return

const doc = getDocument(input)
const win = getWindow(input)

const ghost = createGhostElement(doc)

const cssText = getVisualStyles(input)
if (cssText) ghost.style.cssText += cssText

function resize() {
win.requestAnimationFrame(() => {
ghost.innerHTML = input!.value
const rect = win.getComputedStyle(ghost)
input?.style.setProperty("width", rect.width)
})
}

resize()

input?.addEventListener("input", resize)
input?.addEventListener("change", resize)

return () => {
doc.body.removeChild(ghost)
input?.removeEventListener("input", resize)
input?.removeEventListener("change", resize)
}
}
36 changes: 36 additions & 0 deletions packages/utilities/auto-resize/src/autoresize-textarea.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { getComputedStyle } from "@zag-js/dom-query"

export const autoresizeTextarea = (el: HTMLTextAreaElement | null) => {
if (!el) return

const style = getComputedStyle(el)
const win = el.ownerDocument.defaultView || window

const onInput = () => {
el.style.height = "auto"
const borderTopWidth = parseInt(style.borderTopWidth, 10)
const borderBottomWidth = parseInt(style.borderBottomWidth, 10)
el.style.height = `${el.scrollHeight + borderTopWidth + borderBottomWidth}px`
}

el.addEventListener("input", onInput)

const elementPrototype = Object.getPrototypeOf(el)
const descriptor = Object.getOwnPropertyDescriptor(elementPrototype, "value")
Object.defineProperty(el, "value", {
...descriptor,
set() {
// @ts-ignore
descriptor?.set?.apply(this, arguments as unknown as [unknown])
onInput()
},
})

const resizeObserver = new win.ResizeObserver(() => onInput())

resizeObserver.observe(el)

return () => {
el.removeEventListener("input", onInput)
}
}
73 changes: 2 additions & 71 deletions packages/utilities/auto-resize/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,71 +1,2 @@
import { getDocument, getWindow } from "@zag-js/dom-query"

function copyVisualStyles(fromEl: HTMLElement | null, toEl: HTMLElement) {
if (!fromEl) return

const win = getWindow(fromEl)
const el = win.getComputedStyle(fromEl)

// prettier-ignore
const cssText = 'box-sizing:' + el.boxSizing +
';border-left:' + el.borderLeftWidth + ' solid red' +
';border-right:' + el.borderRightWidth + ' solid red' +
';font-family:' + el.fontFamily +
';font-feature-settings:' + el.fontFeatureSettings +
';font-kerning:' + el.fontKerning +
';font-size:' + el.fontSize +
';font-stretch:' + el.fontStretch +
';font-style:' + el.fontStyle +
';font-variant:' + el.fontVariant +
';font-variant-caps:' + el.fontVariantCaps +
';font-variant-ligatures:' + el.fontVariantLigatures +
';font-variant-numeric:' + el.fontVariantNumeric +
';font-weight:' + el.fontWeight +
';letter-spacing:' + el.letterSpacing +
';margin-left:' + el.marginLeft +
';margin-right:' + el.marginRight +
';padding-left:' + el.paddingLeft +
';padding-right:' + el.paddingRight +
';text-indent:' + el.textIndent +
';text-transform:' + el.textTransform

toEl.style.cssText += cssText
}

function createGhostElement(doc: Document) {
var el = doc.createElement("div")
el.id = "ghost"
el.style.cssText =
"display:inline-block;height:0;overflow:hidden;position:absolute;top:0;visibility:hidden;white-space:nowrap;"
doc.body.appendChild(el)
return el
}

export function autoResizeInput(input: HTMLInputElement | null) {
if (!input) return
const doc = getDocument(input)
const win = getWindow(input)

const ghost = createGhostElement(doc)

copyVisualStyles(input, ghost)

function resize() {
win.requestAnimationFrame(() => {
ghost.innerHTML = input!.value
const rect = win.getComputedStyle(ghost)
input?.style.setProperty("width", rect.width)
})
}

resize()

input?.addEventListener("input", resize)
input?.addEventListener("change", resize)

return () => {
doc.body.removeChild(ghost)
input?.removeEventListener("input", resize)
input?.removeEventListener("change", resize)
}
}
export * from "./autoresize-input"
export * from "./autoresize-textarea"
42 changes: 42 additions & 0 deletions packages/utilities/auto-resize/src/visual-style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { getComputedStyle } from "@zag-js/dom-query"

export function getVisualStyles(node: HTMLElement | null) {
if (!node) return
const style = getComputedStyle(node)

// prettier-ignore
return 'box-sizing:' + style.boxSizing +
';border-left:' + style.borderLeftWidth + ' solid red' +
';border-right:' + style.borderRightWidth + ' solid red' +
';font-family:' + style.fontFamily +
';font-feature-settings:' + style.fontFeatureSettings +
';font-kerning:' + style.fontKerning +
';font-size:' + style.fontSize +
';font-stretch:' + style.fontStretch +
';font-style:' + style.fontStyle +
';font-variant:' + style.fontVariant +
';font-variant-caps:' + style.fontVariantCaps +
';font-variant-ligatures:' + style.fontVariantLigatures +
';font-variant-numeric:' + style.fontVariantNumeric +
';font-weight:' + style.fontWeight +
';letter-spacing:' + style.letterSpacing +
';margin-left:' + style.marginLeft +
';margin-right:' + style.marginRight +
';padding-left:' + style.paddingLeft +
';padding-right:' + style.paddingRight +
';text-indent:' + style.textIndent +
';text-transform:' + style.textTransform
}

export function getLayoutStyles(node: HTMLElement | null) {
if (!node) return
const style = getComputedStyle(node)
// prettier-ignore
return 'width:' + style.width +
';max-width:' + style.maxWidth +
';min-width:' + style.minWidth +
';height:' + style.height +
';max-height:' + style.maxHeight +
';min-height:' + style.minHeight +
';box-sizing:' + style.boxSizing
}
Loading

0 comments on commit 253603b

Please sign in to comment.