Skip to content
This repository has been archived by the owner on Jun 30, 2024. It is now read-only.

Commit

Permalink
Merge pull request #78 from rpearce/fix/resize-scenarios
Browse files Browse the repository at this point in the history
Fix: Also resize via ResizeObserver and changes to additional props
  • Loading branch information
rpearce committed Jan 3, 2022
2 parents 73a6758 + b9cae2b commit a83373d
Show file tree
Hide file tree
Showing 7 changed files with 1,559 additions and 1,400 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
!.storybook/
dist/
docs/
source/types/
2,805 changes: 1,420 additions & 1,385 deletions package-lock.json

Large diffs are not rendered by default.

15 changes: 8 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,17 @@
"@storybook/react": "^6.3.12",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.1.2",
"@types/jest": "^27.0.2",
"@types/node": "^17.0.1",
"@types/react": "^17.0.33",
"@types/jest": "^27.4.0",
"@types/node": "^17.0.7",
"@types/react": "^17.0.38",
"@types/react-dom": "^17.0.10",
"@typescript-eslint/eslint-plugin": "^5.2.0",
"@typescript-eslint/parser": "^5.2.0",
"@typescript-eslint/eslint-plugin": "^5.9.0",
"@typescript-eslint/parser": "^5.9.0",
"all-contributors-cli": "^6.20.0",
"eslint": "^8.1.0",
"eslint-plugin-jest": "^25.2.2",
"eslint-plugin-jest": "^25.3.4",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react": "^7.26.1",
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.2.0",
"jest": "^27.3.1",
"npm-run-all": "^4.1.5",
Expand All @@ -91,6 +91,7 @@
"react": "^16.8.0 || ^17.0.0"
},
"dependencies": {
"fast-shallow-equal": "^1.0.0",
"react-with-forwarded-ref": "^0.3.3",
"tslib": "^2.0.3"
}
Expand Down
39 changes: 38 additions & 1 deletion source/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import React, {
CSSProperties,
ChangeEvent,
FC,
MutableRefObject,
RefObject,
TextareaHTMLAttributes,
useCallback,
useLayoutEffect,
useMemo,
useRef,
} from 'react'
import withForwardedRef from 'react-with-forwarded-ref'
import { equal as isShallowEqual } from 'fast-shallow-equal'

// =============================================================================
export interface GetHeight {
(rows: number, el: HTMLTextAreaElement): number
}
Expand Down Expand Up @@ -46,6 +50,7 @@ export const getHeight: GetHeight = (rows, el) => {
return Math.max(rowHeight, scrollHeight)
}

// =============================================================================
export interface Resize {
(rows: number, el: HTMLTextAreaElement | null): void
}
Expand All @@ -69,6 +74,21 @@ export const resize: Resize = (rows, el) => {
}
}

// =============================================================================
const useShallowObjectMemo = <A,>(obj: A): A => {
const refObject = useRef<A>(obj)
const refCounter = useRef(0)

if (!isShallowEqual(obj, refObject.current)) {
refObject.current = obj
refCounter.current += 1
}

// eslint-disable-next-line react-hooks/exhaustive-deps
return useMemo(() => refObject.current, [refCounter.current])
}

// =============================================================================
type RefFn = (node: HTMLTextAreaElement) => void

export interface TextareaProps
Expand All @@ -85,6 +105,7 @@ const ExpandingTextarea: FC<TextareaProps> = ({
...props
}: TextareaProps) => {
const isForwardedRefFn = typeof forwardedRef === 'function'
const style = useShallowObjectMemo<CSSProperties | undefined>(props.style)
const internalRef = useRef<HTMLTextAreaElement>()
const ref = (
isForwardedRefFn || !forwardedRef ? internalRef : forwardedRef
Expand All @@ -94,7 +115,23 @@ const ExpandingTextarea: FC<TextareaProps> = ({

useLayoutEffect(() => {
resize(rows, ref.current)
}, [ref, rows, props.value])
}, [props.className, props.value, ref, rows, style])

useLayoutEffect(() => {
if (!window.ResizeObserver) {
return
}

const observer = new ResizeObserver(() => {
resize(rows, ref.current)
})

observer.observe(ref.current)

return () => {
observer.disconnect()
}
}, [ref, rows])

const handleInput = useCallback(
(e) => {
Expand Down
90 changes: 84 additions & 6 deletions source/stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import React, {
useCallback,
useEffect,
useRef,
useState,
} from 'react'
import { number, text } from '@storybook/addon-knobs'
import '../.storybook/base.css'
Expand Down Expand Up @@ -77,7 +78,7 @@ export const RegularTextarea: FC = () => {
)
}

export const WithMinimum3Rows: FC = () => {
export const Minimum3Rows: FC = () => {
const textareaRef = useRef<HTMLTextAreaElement>()

const handleChange = useCallback((e) => {
Expand Down Expand Up @@ -107,7 +108,7 @@ export const WithMinimum3Rows: FC = () => {
)
}

export const WithMaxHeight: FC = () => {
export const MaxHeight: FC = () => {
const textareaRef = useRef<HTMLTextAreaElement>()

const handleChange = useCallback((e) => {
Expand Down Expand Up @@ -144,7 +145,7 @@ interface FunctionRefState {
value: string
}

class FunctionRef extends Component<FunctionRefProps, FunctionRefState> {
class FunctionRefComp extends Component<FunctionRefProps, FunctionRefState> {
el: HTMLTextAreaElement | null = null

constructor(props: FunctionRefProps) {
Expand Down Expand Up @@ -184,11 +185,11 @@ class FunctionRef extends Component<FunctionRefProps, FunctionRefState> {
}
}

export const WithFunctionRef: FC = () => {
return <FunctionRef />
export const FunctionRef: FC = () => {
return <FunctionRefComp />
}

export const WithValueFromProps: FC = () => {
export const ValueFromProps: FC = () => {
const textareaRef = useRef<HTMLTextAreaElement>()

useEffect(() => {
Expand All @@ -213,3 +214,80 @@ export const WithValueFromProps: FC = () => {
</main>
)
}

export const StyleChanges: FC = () => {
const [isWide0, setIsWide0] = useState(false)
const [isWide1, setIsWide1] = useState(false)
const [, setCounter0] = useState(0)
const [, setCounter1] = useState(0)

const handleClickToggle0 = useCallback(() => {
setIsWide0(x => !x)
}, [])

const handleClickToggle1 = useCallback(() => {
setIsWide1(x => !x)
}, [])

const handleClickCounter0 = useCallback(() => {
setCounter0(x => x + 1)
}, [])

const handleClickCounter1 = useCallback(() => {
setCounter1(x => x + 1)
}, [])

return (
<main>
<h1>Textarea&apos;s width / style changes</h1>
<section>
<h2>Toggling the parent&apos;s width</h2>
<p>
When it goes from smaller to larger, there should not be any extra
whitespace leftover at the bottom from its height when it was small.
This will only work if <code>ResizeObserver</code> is available in
your browser.
</p>
<button onClick={handleClickToggle0} type="button">
Toggle textarea parent&apos;s width
</button>
<button onClick={handleClickCounter0} type="button">
Force a state update (for testing)
</button>
<div>
<label htmlFor="my-textarea0">Please Enter Some Details:</label>
<div style={{ width: isWide0 ? 400 : 200 }}>
<Textarea
defaultValue="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dsa
das
d
dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est labor"
id="my-textarea0"
style={{ display: 'block', width: '100%' }}
/>
</div>
</div>
</section>
<section>
<h2>Toggling the textarea&apos;s width</h2>
<button onClick={handleClickToggle1} type="button">
Toggle textarea width
</button>
<button onClick={handleClickCounter1} type="button">
Force a state update (for testing)
</button>
<div>
<label htmlFor="my-textarea1">Please Enter Some Details:</label>
<Textarea
defaultValue="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dsa
das
d
dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est labor"
id="my-textarea1"
style={{ display: 'block', width: isWide1 ? 400 : 200 }}
/>
</div>
</section>
</main>
)
}
3 changes: 3 additions & 0 deletions source/types/fast-shallow-equal.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
declare module "fast-shallow-equal" {
export function equal(a: unknown, b: unknown): boolean
}
6 changes: 5 additions & 1 deletion tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@
"lib": ["es2020", "dom"],
"moduleResolution": "node",
"noImplicitAny": true,
"paths": {
"*": ["./source/types/*"]
},
"sourceMap": false,
"strict": true,
"target": "es5"
}
},
"exclude": ["./source/types/*"]
}

0 comments on commit a83373d

Please sign in to comment.