Skip to content

Commit

Permalink
feat(editable): add editable textarea element (#4443)
Browse files Browse the repository at this point in the history
* feat(editable): add editable textarea element

To maintain consistency of editable context,

use union type of textarea and input element to reference

element.

* refactor(editable): change target element type

* refactor(editable): add types on propgetter

To simplify usage and  the type safety, add types where it declared.

* fix(editable): use prop getter v2

The 'height' attribute type that html elemenet delivered is
not compatable with 'height' of chakra. So, use PropGetterV2 to
resolve style incompatiblity.

* fix(editable): use hook for styling textarea

* fix(editable): add missing property referencing on styling

* feat: add textarea to editableAnatomy

* feat: add default styles for EditableTextarea

* chore: update changeset

Co-authored-by: Tim Kolberger <tim@kolberger.eu>
  • Loading branch information
heozeop and TimKolberger committed Feb 28, 2022
1 parent 356b3f8 commit fbe9462
Show file tree
Hide file tree
Showing 9 changed files with 363 additions and 14 deletions.
5 changes: 5 additions & 0 deletions .changeset/beige-poems-fold.md
@@ -0,0 +1,5 @@
---
"@chakra-ui/anatomy": minor
---

Add `textarea` part to `editableAnatomy`
13 changes: 13 additions & 0 deletions .changeset/modern-dryers-kiss.md
@@ -0,0 +1,13 @@
---
"@chakra-ui/editable": minor
---

Added the component `EditableTextarea` to `Editable`. Use the textarea element
to handle multi line text input in an editable context.

```tsx live=false
<Editable defaultValue="Change me" onChange={console.log}>
<EditablePreview />
<EditableTextarea />
</Editable>
```
5 changes: 5 additions & 0 deletions .changeset/proud-camels-dream.md
@@ -0,0 +1,5 @@
---
"@chakra-ui/theme": minor
---

Add styles for new `textarea` element in `Editable`
6 changes: 5 additions & 1 deletion packages/anatomy/src/index.ts
Expand Up @@ -59,7 +59,11 @@ export const drawerAnatomy = anatomy("drawer")
.parts("overlay", "dialogContainer", "dialog")
.extend("header", "closeButton", "body", "footer")

export const editableAnatomy = anatomy("editable").parts("preview", "input")
export const editableAnatomy = anatomy("editable").parts(
"preview",
"input",
"textarea",
)

export const formAnatomy = anatomy("form").parts(
"container",
Expand Down
43 changes: 35 additions & 8 deletions packages/editable/src/editable.tsx
Expand Up @@ -139,7 +139,7 @@ export const EditableInput = forwardRef<EditableInputProps, "input">(
const { getInputProps } = useEditableContext()
const styles = useStyles()

const inputProps = getInputProps(props, ref) as HTMLChakraProps<"input">
const inputProps = getInputProps(props, ref)
const _className = cx("chakra-editable__input", props.className)

return (
Expand All @@ -160,17 +160,44 @@ if (__DEV__) {
EditableInput.displayName = "EditableInput"
}

export interface EditableTextareaProps extends HTMLChakraProps<"textarea"> {}

/**
* EditableTextarea
*
* The textarea used in the `edit` mode
*/
export const EditableTextarea = forwardRef<EditableTextareaProps, "textarea">(
(props, ref) => {
const { getTextareaProps } = useEditableContext()
const styles = useStyles()

const textareaProps = getTextareaProps(props, ref)
const _className = cx("chakra-editable__textarea", props.className)

return (
<chakra.textarea
{...textareaProps}
__css={{
outline: 0,
...commonStyles,
...styles.textarea,
}}
className={_className}
/>
)
},
)

if (__DEV__) {
EditableTextarea.displayName = "EditableTextarea"
}
/**
* React hook use to gain access to the editable state and actions.
*/
export function useEditableState() {
const {
isEditing,
onSubmit,
onCancel,
onEdit,
isDisabled,
} = useEditableContext()
const { isEditing, onSubmit, onCancel, onEdit, isDisabled } =
useEditableContext()

return {
isEditing,
Expand Down
65 changes: 60 additions & 5 deletions packages/editable/src/use-editable.ts
Expand Up @@ -4,7 +4,13 @@ import {
useUpdateEffect,
useSafeLayoutEffect,
} from "@chakra-ui/hooks"
import { EventKeyMap, mergeRefs, PropGetter } from "@chakra-ui/react-utils"
import {
EventKeyMap,
mergeRefs,
PropGetter,
PropGetterV2,
} from "@chakra-ui/react-utils"
import { HTMLChakraProps } from "@chakra-ui/system"
import {
ariaAttr,
callAllHandlers,
Expand Down Expand Up @@ -113,7 +119,7 @@ export function useEditable(props: UseEditableProps = {}) {
/**
* Ref to help focus the input in edit mode
*/
const inputRef = useRef<HTMLInputElement>(null)
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement>(null)
const previewRef = useRef<any>(null)

const editButtonRef = useRef<HTMLButtonElement>(null)
Expand Down Expand Up @@ -168,8 +174,8 @@ export function useEditable(props: UseEditableProps = {}) {
}, [value, onSubmitProp])

const onChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value)
(event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setValue(event.currentTarget.value)
},
[setValue],
)
Expand Down Expand Up @@ -197,6 +203,24 @@ export function useEditable(props: UseEditableProps = {}) {
[onCancel, onSubmit],
)

const onKeyDownWithoutSubmit = useCallback(
(event: React.KeyboardEvent) => {
const eventKey = normalizeEventKey(event)

const keyMap: EventKeyMap = {
Escape: onCancel,
}

const action = keyMap[eventKey]

if (action) {
event.preventDefault()
action(event)
}
},
[onCancel],
)

const isValueEmpty = isEmpty(value)

const onBlur = useCallback(
Expand Down Expand Up @@ -238,7 +262,10 @@ export function useEditable(props: UseEditableProps = {}) {
],
)

const getInputProps: PropGetter = useCallback(
const getInputProps: PropGetterV2<
"input",
HTMLChakraProps<"input">
> = useCallback(
(props = {}, ref = null) => ({
...props,
hidden: !isEditing,
Expand All @@ -254,6 +281,33 @@ export function useEditable(props: UseEditableProps = {}) {
[isDisabled, isEditing, onBlur, onChange, onKeyDown, placeholder, value],
)

const getTextareaProps: PropGetterV2<
"textarea",
HTMLChakraProps<"textarea">
> = useCallback(
(props = {}, ref = null) => ({
...props,
hidden: !isEditing,
placeholder,
ref: mergeRefs(ref, inputRef),
disabled: isDisabled,
"aria-disabled": ariaAttr(isDisabled),
value,
onBlur: callAllHandlers(props.onBlur, onBlur),
onChange: callAllHandlers(props.onChange, onChange),
onKeyDown: callAllHandlers(props.onKeyDown, onKeyDownWithoutSubmit),
}),
[
isDisabled,
isEditing,
onBlur,
onChange,
onKeyDownWithoutSubmit,
placeholder,
value,
],
)

const getEditButtonProps: PropGetter = useCallback(
(props = {}, ref = null) => ({
"aria-label": "Edit",
Expand Down Expand Up @@ -298,6 +352,7 @@ export function useEditable(props: UseEditableProps = {}) {
onSubmit,
getPreviewProps,
getInputProps,
getTextareaProps,
getEditButtonProps,
getSubmitButtonProps,
getCancelButtonProps,
Expand Down
18 changes: 18 additions & 0 deletions packages/editable/stories/editable.stories.tsx
Expand Up @@ -5,6 +5,7 @@ import {
Editable,
EditableInput,
EditablePreview,
EditableTextarea,
useEditableControls,
} from "../src"

Expand Down Expand Up @@ -107,3 +108,20 @@ export const CodeSandboxTopbar = () => {
</chakra.div>
)
}

export const TextareaAsInput = () => {
return (
<Editable
defaultValue="Hello!!"
fontSize="xl"
textAlign="center"
isPreviewFocusable={false}
submitOnBlur={false}
onChange={console.log}
>
<EditablePreview />
<EditableTextarea />
<EditableControls />
</Editable>
)
}

1 comment on commit fbe9462

@vercel
Copy link

@vercel vercel bot commented on fbe9462 Feb 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.