From 44aeeb9f8526018e74c3de780aafc0836396a744 Mon Sep 17 00:00:00 2001 From: Doug MacKenzie Date: Thu, 24 Oct 2024 09:35:16 +1100 Subject: [PATCH 1/4] FilterBar: manage focus on add/remove filters (#5194) * FilterBar: manage focus on add/remove filter * Fix up typings --------- Co-authored-by: Cassandra Tam --- .changeset/many-plums-jam.md | 5 ++ .../src/Filter/FilterBar/FilterBar.spec.tsx | 87 +++++++++++++++++++ .../FilterBar/context/FilterBarContext.tsx | 13 ++- .../context/reducer/filterBarStateReducer.ts | 7 ++ .../context/reducer/setupFilterBarState.ts | 1 + .../src/Filter/FilterBar/context/types.ts | 1 + .../AddFiltersMenu/AddFiltersMenu.tsx | 14 ++- .../FilterBarButton/FilterBarButton.tsx | 12 ++- .../FilterBarMultiSelect.tsx | 21 ++++- .../FilterMultiSelect/FilterMultiSelect.tsx | 4 +- .../MenuTriggerProvider.tsx | 5 +- 11 files changed, 159 insertions(+), 11 deletions(-) create mode 100644 .changeset/many-plums-jam.md diff --git a/.changeset/many-plums-jam.md b/.changeset/many-plums-jam.md new file mode 100644 index 00000000000..f039aea7961 --- /dev/null +++ b/.changeset/many-plums-jam.md @@ -0,0 +1,5 @@ +--- +"@kaizen/components": patch +--- + +FilterBar: manage focus on add/remove filters diff --git a/packages/components/src/Filter/FilterBar/FilterBar.spec.tsx b/packages/components/src/Filter/FilterBar/FilterBar.spec.tsx index 1fafb1db965..dc9f4955fa7 100644 --- a/packages/components/src/Filter/FilterBar/FilterBar.spec.tsx +++ b/packages/components/src/Filter/FilterBar/FilterBar.spec.tsx @@ -65,6 +65,7 @@ const simpleFilters = [ type ValuesRemovable = { flavour: string topping: string + others: string } const filtersRemovable = [ @@ -86,6 +87,37 @@ const filtersRemovable = [ ), isRemovable: true, }, + { + id: "others", + name: "Others", + Component: ( + + {(): JSX.Element => ( + <> + + + {({ allItems }): JSX.Element | JSX.Element[] => + allItems.map(item => ( + + )) + } + + + + + + + )} + + ), + isRemovable: true, + }, ] satisfies Filters type ValuesDependent = { @@ -288,6 +320,61 @@ describe("", () => { expect(filters[1]).toHaveTextContent("Flavour") expect(filters[2]).toHaveTextContent("Sugar Level") }) + + it("moves focus to recently added filter button", async () => { + const { getByRole } = render( + + filters={simpleFilters.map(filter => ({ + ...filter, + isRemovable: true, + }))} + /> + ) + await waitForI18nContent() + + const addFiltersButton = getByRole("button", { name: "Add Filters" }) + await user.click(addFiltersButton) + + const menuOptionIceLevel = getByRole("button", { name: "Ice Level" }) + await user.click(menuOptionIceLevel) + + expect(getByRole("button", { name: "Ice Level" })).toHaveFocus() + }) + + it("moves focus to recently added filter button in the FilterMultiSelect case", async () => { + const { getByRole } = render( + filters={filtersRemovable} /> + ) + await waitForI18nContent() + + const addFiltersButton = getByRole("button", { name: "Add Filters" }) + await user.click(addFiltersButton) + + const menuOptionOthers = getByRole("button", { name: "Others" }) + await user.click(menuOptionOthers) + + expect(getByRole("button", { name: "Others" })).toHaveFocus() + }) + + it("restores focus to the add filter button after remove", async () => { + const { getByRole } = render( + + filters={filtersRemovable} + defaultValues={{ topping: "pearls" }} + /> + ) + await waitForI18nContent() + + const filterButton = getByRole("button", { name: "Topping : Pearls" }) + expect(filterButton).toBeVisible() + + await user.click(getByRole("button", { name: "Remove filter - Topping" })) + await waitFor(() => { + expect(filterButton).not.toBeInTheDocument() + }) + + expect(getByRole("button", { name: "Add Filters" })).toHaveFocus() + }) }) describe("Dependent filters", () => { diff --git a/packages/components/src/Filter/FilterBar/context/FilterBarContext.tsx b/packages/components/src/Filter/FilterBar/context/FilterBarContext.tsx index 38bf26046d6..293e0e062ba 100644 --- a/packages/components/src/Filter/FilterBar/context/FilterBarContext.tsx +++ b/packages/components/src/Filter/FilterBar/context/FilterBarContext.tsx @@ -43,6 +43,8 @@ export type FilterBarContextValue< hideFilter: (id: Id) => void getInactiveFilters: () => Array> clearAllFilters: () => void + setFocus: (id: Id | undefined) => void + focusId?: keyof ValuesMap } const FilterBarContext = React.createContext | null>( @@ -119,10 +121,13 @@ export const FilterBarProvider = ({ values: { ...values, [id]: getValidValue(newValue) }, }) }, - showFilter: (id: Id): void => - dispatch({ type: "activate_filter", id }), + showFilter: (id: Id): void => { + dispatch({ type: "activate_filter", id }) + dispatch({ type: "set_focus", id }) + }, hideFilter: (id: Id): void => { dispatch({ type: "deactivate_filter", id }) + dispatch({ type: "set_focus", id: "add_filter" }) }, getInactiveFilters: () => getInactiveFilters(state), clearAllFilters: () => { @@ -132,6 +137,10 @@ export const FilterBarProvider = ({ }) dispatch({ type: "update_values", values: {} }) }, + setFocus: (id: Id | undefined) => { + dispatch({ type: "set_focus", id }) + }, + focusId: state.focusId, } satisfies FilterBarContextValue useEffect(() => { diff --git a/packages/components/src/Filter/FilterBar/context/reducer/filterBarStateReducer.ts b/packages/components/src/Filter/FilterBar/context/reducer/filterBarStateReducer.ts index c3830812a11..e5b08cea22b 100644 --- a/packages/components/src/Filter/FilterBar/context/reducer/filterBarStateReducer.ts +++ b/packages/components/src/Filter/FilterBar/context/reducer/filterBarStateReducer.ts @@ -15,12 +15,19 @@ type Actions = | { type: "activate_filter"; id: keyof ValuesMap } | { type: "deactivate_filter"; id: keyof ValuesMap } | { type: "update_filter_labels"; data: Filters } + | { type: "set_focus"; id: keyof ValuesMap | undefined } export const filterBarStateReducer = ( state: FilterBarState, action: Actions ): FilterBarState => { switch (action.type) { + case "set_focus": + return { + ...state, + focusId: action.id, + } + case "update_values": return { ...updateValues(state, action.values) } diff --git a/packages/components/src/Filter/FilterBar/context/reducer/setupFilterBarState.ts b/packages/components/src/Filter/FilterBar/context/reducer/setupFilterBarState.ts index 848ed622faa..d8ee0f08325 100644 --- a/packages/components/src/Filter/FilterBar/context/reducer/setupFilterBarState.ts +++ b/packages/components/src/Filter/FilterBar/context/reducer/setupFilterBarState.ts @@ -35,6 +35,7 @@ export const setupFilterBarState = ( values, dependentFilterIds: new Set(), hasUpdatedValues: false, + focusId: undefined, } as FilterBarState ) diff --git a/packages/components/src/Filter/FilterBar/context/types.ts b/packages/components/src/Filter/FilterBar/context/types.ts index 6ab82064d47..92358675459 100644 --- a/packages/components/src/Filter/FilterBar/context/types.ts +++ b/packages/components/src/Filter/FilterBar/context/types.ts @@ -32,6 +32,7 @@ export type FilterBarState = { activeFilterIds: Set values: Partial dependentFilterIds: Set + focusId?: keyof ValuesMap } export type ActiveFiltersArray = Array< diff --git a/packages/components/src/Filter/FilterBar/subcomponents/AddFiltersMenu/AddFiltersMenu.tsx b/packages/components/src/Filter/FilterBar/subcomponents/AddFiltersMenu/AddFiltersMenu.tsx index 8d567a84d78..d084581b848 100644 --- a/packages/components/src/Filter/FilterBar/subcomponents/AddFiltersMenu/AddFiltersMenu.tsx +++ b/packages/components/src/Filter/FilterBar/subcomponents/AddFiltersMenu/AddFiltersMenu.tsx @@ -1,4 +1,4 @@ -import React from "react" +import React, { useEffect, useRef } from "react" import { useIntl } from "@cultureamp/i18n-react-intl" import { Menu, MenuList, MenuItem, Button } from "~components/__actions__/v2" import { Icon } from "~components/__future__/Icon" @@ -6,6 +6,7 @@ import { useFilterBarContext } from "../../context/FilterBarContext" export const AddFiltersMenu = (): JSX.Element => { const { formatMessage } = useIntl() + const buttonRef = useRef(null) const menuButtonLabel = formatMessage({ id: "filterBar.addFiltersMenu.buttonLabel", @@ -14,13 +15,22 @@ export const AddFiltersMenu = (): JSX.Element => { "Menu button label to show additional available filter options", }) - const { getInactiveFilters, showFilter } = useFilterBarContext() + const { getInactiveFilters, showFilter, focusId, setFocus } = + useFilterBarContext() const inactiveFilters = getInactiveFilters() + useEffect(() => { + if (focusId === "add_filter") { + buttonRef.current?.focus() + setFocus(undefined) + } + }, [focusId]) + return ( (({ filterId, isRemovable = false, ...props }, ref): JSX.Element => { - const { hideFilter } = useFilterBarContext() + const { hideFilter, focusId, setFocus } = useFilterBarContext() + + useEffect(() => { + if (focusId === filterId && isRefObject(ref)) { + ref?.current?.triggerRef?.current?.focus() + setFocus(undefined) + } + }, [focusId]) return isRemovable ? ( { - const { getFilterState, setFilterOpenState, updateValue, hideFilter } = - useFilterBarContext() + const { + getFilterState, + setFilterOpenState, + updateValue, + hideFilter, + focusId, + setFocus, + } = useFilterBarContext() const [items, setItems] = useState(propsItems) + const buttonRef = useRef(null) if (!id) throw Error("Missing `id` prop in FilterBarMultiSelect") @@ -70,6 +77,13 @@ export const FilterBarMultiSelect = ({ } }, [items]) + useEffect(() => { + if (focusId === id) { + buttonRef.current?.focus() + setFocus(undefined) + } + }, [focusId]) + return ( ) }} + triggerRef={buttonRef} {...props} > {children} diff --git a/packages/components/src/Filter/FilterMultiSelect/FilterMultiSelect.tsx b/packages/components/src/Filter/FilterMultiSelect/FilterMultiSelect.tsx index 75ad15c7bdf..0c21309a0f4 100644 --- a/packages/components/src/Filter/FilterMultiSelect/FilterMultiSelect.tsx +++ b/packages/components/src/Filter/FilterMultiSelect/FilterMultiSelect.tsx @@ -41,6 +41,7 @@ type SelectionProps = { export type FilterMultiSelectProps = { trigger: (value?: MenuTriggerProviderContextType) => React.ReactNode children: (value?: SelectionProviderContextType) => React.ReactNode // the content of the menu + triggerRef?: React.RefObject } & Omit & Omit & SelectionProps @@ -60,8 +61,9 @@ export const FilterMultiSelect = ({ onSelectionChange, selectionMode = "multiple", onSearchInputChange, + triggerRef, }: FilterMultiSelectProps): JSX.Element => { - const menuTriggerProps = { isOpen, defaultOpen, onOpenChange } + const menuTriggerProps = { isOpen, defaultOpen, onOpenChange, triggerRef } const menuPopupProps = { isLoading, loadingSkeleton } const disabledKeys: Selection = new Set( items diff --git a/packages/components/src/Filter/FilterMultiSelect/context/MenuTriggerProvider/MenuTriggerProvider.tsx b/packages/components/src/Filter/FilterMultiSelect/context/MenuTriggerProvider/MenuTriggerProvider.tsx index d0e031ed664..a83ad203ac7 100644 --- a/packages/components/src/Filter/FilterMultiSelect/context/MenuTriggerProvider/MenuTriggerProvider.tsx +++ b/packages/components/src/Filter/FilterMultiSelect/context/MenuTriggerProvider/MenuTriggerProvider.tsx @@ -14,6 +14,7 @@ export type MenuTriggerProviderProps = { defaultOpen?: boolean onOpenChange?: (isOpen: boolean) => void children: React.ReactNode + triggerRef?: React.RefObject } export type MenuTriggerProviderContextType = { @@ -32,12 +33,14 @@ export function MenuTriggerProvider({ defaultOpen, onOpenChange, children, + triggerRef, }: MenuTriggerProviderProps): JSX.Element { // Create state based on the incoming props to manage the open/close const state = useMenuTriggerState({ isOpen, defaultOpen, onOpenChange }) // Get A11y attributes and events for the menu trigger and menu elements - const ref = useRef(null) + const fallbackRef = useRef(null) + const ref = triggerRef || fallbackRef const { menuTriggerProps, menuProps } = useMenuTrigger( {}, state, From b73d4372115231b6ea5e9ad2e4fda78e0404596f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 23 Oct 2024 22:38:10 +0000 Subject: [PATCH 2/4] version packages (#5196) Co-authored-by: github-actions[bot] --- .changeset/many-plums-jam.md | 5 ----- packages/components/CHANGELOG.md | 6 ++++++ packages/components/package.json | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) delete mode 100644 .changeset/many-plums-jam.md diff --git a/.changeset/many-plums-jam.md b/.changeset/many-plums-jam.md deleted file mode 100644 index f039aea7961..00000000000 --- a/.changeset/many-plums-jam.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kaizen/components": patch ---- - -FilterBar: manage focus on add/remove filters diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index d8261f2d7dd..127524ed376 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## 1.67.3 + +### Patch Changes + +- [#5194](https://github.com/cultureamp/kaizen-design-system/pull/5194) [`44aeeb9f8526018e74c3de780aafc0836396a744`](https://github.com/cultureamp/kaizen-design-system/commit/44aeeb9f8526018e74c3de780aafc0836396a744) - FilterBar: manage focus on add/remove filters + ## 1.67.2 ### Patch Changes diff --git a/packages/components/package.json b/packages/components/package.json index ef7871a01aa..cde3e8c0764 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@kaizen/components", - "version": "1.67.2", + "version": "1.67.3", "description": "Kaizen component library", "author": "Geoffrey Chong ", "homepage": "https://cultureamp.design", From b5cc795efb301cd684c843997ca4003effbff299 Mon Sep 17 00:00:00 2001 From: Doug MacKenzie Date: Thu, 24 Oct 2024 17:20:52 +1100 Subject: [PATCH 3/4] fix(TextArea): Refactor autogrow to remove visual jank (#5197) Convert TextArea sass to css DRY up placeholder mixin for future copy+paste removal CSS tidy ups Linting Restore default border colour Patch disabled opacity Fix class name typo in CSS empty commit --- .changeset/nervous-bobcats-turn.md | 5 + .../src/TextArea/TextArea.module.css | 142 ++++++++++++++++++ .../src/TextArea/TextArea.module.scss | 137 ----------------- packages/components/src/TextArea/TextArea.tsx | 78 +++------- packages/components/styles/utils/_forms.scss | 32 +--- 5 files changed, 173 insertions(+), 221 deletions(-) create mode 100644 .changeset/nervous-bobcats-turn.md create mode 100644 packages/components/src/TextArea/TextArea.module.css delete mode 100644 packages/components/src/TextArea/TextArea.module.scss diff --git a/.changeset/nervous-bobcats-turn.md b/.changeset/nervous-bobcats-turn.md new file mode 100644 index 00000000000..f437d277e88 --- /dev/null +++ b/.changeset/nervous-bobcats-turn.md @@ -0,0 +1,5 @@ +--- +"@kaizen/components": patch +--- + +fix(TextArea: Refactor autogrow to remove visual jank diff --git a/packages/components/src/TextArea/TextArea.module.css b/packages/components/src/TextArea/TextArea.module.css new file mode 100644 index 00000000000..eadef596233 --- /dev/null +++ b/packages/components/src/TextArea/TextArea.module.css @@ -0,0 +1,142 @@ +.wrapper { + font-family: var(--typography-paragraph-body-font-family); + font-size: var(--typography-paragraph-body-font-size); + font-weight: var(--typography-paragraph-body-font-weight); + line-height: var(--typography-paragraph-body-line-height); + letter-spacing: var(--typography-paragraph-body-letter-spacing); + color: var(--color-purple-800-rgb); +} + +.wrapperAutogrow { + display: grid; +} + +.wrapperAutogrow::after { + content: attr(data-value) " "; + white-space: pre-wrap; + visibility: hidden; +} + +/* these properties need to be set on both for autogrow to work properly */ +.textarea, +.wrapperAutogrow::after { + border: var(--border-solid-border-width) var(--border-solid-border-style) + var(--color-gray-500); + border-radius: var(--border-solid-border-radius); + padding: var(--spacing-sm); + box-sizing: border-box; + width: 100%; + font: inherit; +} + +.textareaAutogrow, +.wrapperAutogrow::after { + grid-area: 2 / 1; +} + +.textarea { + display: block; + border: var(--border-solid-border-width) var(--border-solid-border-style) + var(--color-gray-500); + border-radius: var(--border-solid-border-radius); + padding: var(--spacing-sm); + resize: vertical; + + &:focus { + outline: var(--border-focus-ring-border-width) + var(--border-focus-ring-border-style) var(--color-blue-500); + outline-offset: 1px; + } + + &:disabled { + resize: none; + } +} + +.textareaAutogrow { + overflow: hidden; +} + +.default { + &:not(.error, .caution) { + &:disabled { + border-color: rgba(var(--color-gray-500-rgb), 0.3); + } + } + + &:focus:not([disabled]), + &:hover:not([disabled]), + &:hover:focus:not([disabled]) { + background-color: var(--color-gray-200); + border-color: var(--color-gray-600); + } + + &.error { + border-color: var(--color-red-500); + + &:focus:not([disabled]), + &:hover:not([disabled]), + &:hover:focus:not([disabled]) { + border-color: var(--color-red-500); + } + } + + &.caution { + border-color: var(--color-yellow-600); + + &:focus:not([disabled]), + &:hover:not([disabled]), + &:hover:focus:not([disabled]) { + border-color: var(--color-yellow-600); + } + } + + &.disabled { + background-color: var(--color-white); + border-color: rgba(var(--color-gray-500-rgb), 0.3); + color: rgba(var(--color-purple-800-rgb), 0.3); + } +} + +.reversed { + border-color: rgba(var(--color-white-rgb), 0.65); + background: transparent; + color: var(--color-white); + + &:focus { + outline-color: var(--color-blue-300); + } + + &:focus:not([disabled]), + &:hover:not([disabled]), + &:hover:focus:not([disabled]) { + background: rgba(var(--color-white-rgb), 0.1); + border-color: var(--color-white); + } + + &.error { + border-color: var(--color-red-300); + + &:focus:not([disabled]), + &:hover:not([disabled]), + &:hover:focus:not([disabled]) { + border-color: var(--color-red-300); + } + } + + &.caution { + border-color: var(--color-yellow-400); + + &:focus:not([disabled]), + &:hover:not([disabled]), + &:hover:focus:not([disabled]) { + border-color: var(--color-yellow-400); + } + } + + &.disabled { + background: transparent; + border-color: rgba(var(--color-white-rgb), 0.3); + color: rgba(var(--color-white-rgb), 0.3); + } +} diff --git a/packages/components/src/TextArea/TextArea.module.scss b/packages/components/src/TextArea/TextArea.module.scss deleted file mode 100644 index a56fb7adb5f..00000000000 --- a/packages/components/src/TextArea/TextArea.module.scss +++ /dev/null @@ -1,137 +0,0 @@ -@import "~@kaizen/design-tokens/sass/spacing"; -@import "~@kaizen/design-tokens/sass/color"; -@import "~@kaizen/design-tokens/sass/border"; -@import "../../styles/utils/forms"; -@import "../../styles/utils/form-variables"; - -// Vars -$input-disabled-opacity: 0.3; -$input-disabled-border-alpha: 50%; - -.wrapper { - position: relative; -} - -.textarea { - @include form-input-reset; - - border-radius: $border-solid-border-radius; - width: 100%; - border: $border-solid-border-width $border-solid-border-style $color-gray-500; - padding: $spacing-sm; - color: $color-purple-800-rgb; - display: block; - resize: vertical; - - @include form-input-placeholder { - line-height: 1.5; - color: $dt-color-form-text-color-placeholder; - } - - &:disabled { - resize: none; - } -} - -.textarea:focus + .focusRing { - $focus-ring-offset: 3px; - - position: absolute; - background: transparent; - border-radius: $border-focus-ring-border-radius; - border-width: $border-focus-ring-border-width; - border-style: $border-focus-ring-border-style; - border-color: transparent; - inset: -$focus-ring-offset; - pointer-events: none; -} - -.textarea.default { - @include form-input-focus-state { - background-color: $color-gray-200; - border-color: $color-gray-600; - } - - &:focus + .focusRing { - border-color: $color-blue-500; - } - - &:not(.error, .caution) { - &:disabled { - border-color: rgba($color-gray-500-rgb, $input-disabled-opacity); - } - } - - &.error { - border-color: $color-red-500; - - @include form-input-focus-state { - border-color: $color-red-500; - } - } - - &.caution { - border-color: $color-yellow-600; - - @include form-input-focus-state { - border-color: $color-yellow-600; - } - } - - &.disabled { - background-color: $color-white; - border-color: rgba($color-gray-500-rgb, $input-disabled-opacity); - color: rgba($color-purple-800-rgb, $input-disabled-opacity); - - @include form-input-placeholder { - opacity: $input-disabled-opacity; - } - } -} - -// Reversed (Dark Backgrounds) -.textarea.reversed { - border-color: rgba($color-white-rgb, 0.65); - background: transparent; - color: $color-white; - - @include form-input-focus-state { - background: rgba($color-white-rgb, 0.1); - border-color: $color-white; - } - - @include form-input-placeholder { - line-height: 1.5; - color: $color-white; - } - - &:focus + .focusRing { - border-color: $color-blue-300; - } - - &.error { - border-color: $color-red-300; - - @include form-input-focus-state { - border-color: $color-red-300; - } - } - - &.caution { - border-color: $color-yellow-400; - - @include form-input-focus-state { - border-color: $color-yellow-400; - } - } - - &.disabled { - background: transparent; - border-color: rgba($color-white-rgb, $input-disabled-opacity); - color: rgba($color-white-rgb, $input-disabled-opacity); - - @include form-input-placeholder { - opacity: $input-disabled-opacity; - } - } -} diff --git a/packages/components/src/TextArea/TextArea.tsx b/packages/components/src/TextArea/TextArea.tsx index 32103e62d16..849be49bcae 100644 --- a/packages/components/src/TextArea/TextArea.tsx +++ b/packages/components/src/TextArea/TextArea.tsx @@ -1,16 +1,15 @@ -import React, { - TextareaHTMLAttributes, - useEffect, - useRef, - useState, -} from "react" +import React, { TextareaHTMLAttributes, useRef, useState } from "react" import classnames from "classnames" import { OverrideClassName } from "~components/types/OverrideClassName" -import styles from "./TextArea.module.scss" +import styles from "./TextArea.module.css" export type TextAreaProps = { textAreaRef?: React.RefObject status?: "default" | "error" | "caution" + /** + * Grows the input height as more content is added + * Replace with CSS field-sizing once it's supported by all major browsers + */ autogrow?: boolean reversed?: boolean /** @@ -32,73 +31,46 @@ export const TextArea = ({ onChange: propsOnChange, ...restProps }: TextAreaProps): JSX.Element => { - const [textAreaHeight, setTextAreaHeight] = useState("auto") - const [parentHeight, setParentHeight] = useState("auto") const [internalValue, setInternalValue] = useState< string | number | readonly string[] | undefined - >(autogrow ? defaultValue : undefined) + >(autogrow && !value ? defaultValue : undefined) // ^ holds an internal state of the value so that autogrow can still work with uncontrolled textareas - // essentially forces the textarea into an (interally) controlled mode if autogrow is true - const textAreaRef = propsTextAreaRef || useRef(null) - - useEffect(() => { - if (!autogrow) return - - const scrollHeight = textAreaRef.current!.scrollHeight - if (scrollHeight < 1) return - - const borderWidth = textAreaRef.current - ? parseInt(getComputedStyle(textAreaRef.current).borderTopWidth, 10) - : 0 - const newHeight = scrollHeight + borderWidth * 2 - setParentHeight(`${newHeight}px`) - setTextAreaHeight(`${newHeight}px`) - }, [internalValue]) - - const onChange = !autogrow - ? undefined - : (event: React.ChangeEvent): void => { - setTextAreaHeight("auto") - // ^ this is required to avoid the textarea height from building up indefinitely - // see https://medium.com/@lucasalgus/creating-a-custom-auto-resize-textarea-component-for-your-react-web-application-6959c0ad68bc#2dee - - setInternalValue(event.target.value) - if (propsOnChange) { - propsOnChange(event) - } - } - - const getWrapperStyle = (): { minHeight: string } | undefined => - autogrow ? { minHeight: parentHeight } : undefined - - const getTextAreaStyle = (): { height: string } | undefined => - autogrow ? { height: textAreaHeight } : undefined + // essentially forces the textarea into an (interally) controlled mode if autogrow is true and mode is uncontrolled const controlledValue = value || internalValue + const textAreaRef = propsTextAreaRef || useRef(null) + + const onChange = (event: React.ChangeEvent): void => { + propsOnChange && propsOnChange(event) + setInternalValue(event.target.value) + } return ( -
+