Skip to content

Commit

Permalink
feat: add props to editable (#7403)
Browse files Browse the repository at this point in the history
  • Loading branch information
mosnicholas committed Apr 22, 2023
1 parent c74d576 commit d6b6941
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 4 deletions.
5 changes: 5 additions & 0 deletions .changeset/strong-pianos-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@chakra-ui/editable": major
---

Adds finalFocusRef and onBlur to Editable component
27 changes: 24 additions & 3 deletions packages/components/editable/src/use-editable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ import { mergeRefs } from "@chakra-ui/react-use-merge-refs"
import { useCallbackRef } from "@chakra-ui/react-use-callback-ref"
import { ariaAttr, callAllHandlers } from "@chakra-ui/shared-utils"
import { PropGetter } from "@chakra-ui/react-types"
import { useCallback, useEffect, useRef, useState } from "react"
import { useCallback, useEffect, useRef, useState, RefObject } from "react"

interface FocusableElement {
focus(options?: FocusOptions): void
}

export interface UseEditableProps {
/**
Expand Down Expand Up @@ -53,6 +57,11 @@ export interface UseEditableProps {
* Callback invoked once the user enters edit mode.
*/
onEdit?: () => void
/**
* Callback invoked when the user either submits or cancels.
* It provides the last confirmed value as argument.
*/
onBlur?: (nextValue: string) => void
/**
* If `true`, the input's text will be highlighted on focus.
* @default true
Expand All @@ -62,6 +71,10 @@ export interface UseEditableProps {
* The placeholder text when the value is empty.
*/
placeholder?: string
/**
* The `ref` of element to receive focus when the modal closes.
*/
finalFocusRef?: RefObject<FocusableElement>
}

function contains(parent: HTMLElement | null, child: HTMLElement) {
Expand All @@ -79,6 +92,7 @@ export function useEditable(props: UseEditableProps = {}) {
onChange: onChangeProp,
onCancel: onCancelProp,
onSubmit: onSubmitProp,
onBlur: onBlurProp,
value: valueProp,
isDisabled,
defaultValue,
Expand All @@ -88,6 +102,7 @@ export function useEditable(props: UseEditableProps = {}) {
selectAllOnFocus = true,
placeholder,
onEdit: onEditCallback,
finalFocusRef,
...htmlProps
} = props

Expand Down Expand Up @@ -136,7 +151,11 @@ export function useEditable(props: UseEditableProps = {}) {

useUpdateEffect(() => {
if (!isEditing) {
editButtonRef.current?.focus()
if (finalFocusRef) {
finalFocusRef.current?.focus()
} else {
editButtonRef.current?.focus()
}
return
}

Expand All @@ -163,12 +182,14 @@ export function useEditable(props: UseEditableProps = {}) {
setIsEditing(false)
setValue(prevValue)
onCancelProp?.(prevValue)
}, [onCancelProp, setValue, prevValue])
onBlurProp?.(prevValue)
}, [onCancelProp, onBlurProp, setValue, prevValue])

const onSubmit = useCallback(() => {
setIsEditing(false)
setPrevValue(value)
onSubmitProp?.(value)
onBlurProp?.(prevValue)
}, [value, onSubmitProp])

useEffect(() => {
Expand Down
19 changes: 19 additions & 0 deletions packages/components/editable/stories/editable.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,22 @@ export const Disabled = () => (
<EditableControls />
</Editable>
)

export const FinalFocusRef = () => {
const finalFocusRef = React.useRef(null)

return (
<>
<input ref={finalFocusRef} />
<Editable
finalFocusRef={finalFocusRef}
defaultValue="Final fantasy"
fontSize="xl"
>
<EditablePreview />
<EditableInput />
<EditableControls />
</Editable>
</>
)
}
43 changes: 42 additions & 1 deletion packages/components/editable/tests/editable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ test("controlled: handles callbacks correctly", async () => {
const onCancel = jest.fn()
const onSubmit = jest.fn()
const onEdit = jest.fn()
const onBlur = jest.fn()

const Component = () => {
const [value, setValue] = React.useState("")
Expand All @@ -90,6 +91,7 @@ test("controlled: handles callbacks correctly", async () => {
setValue(val)
onChange(val)
}}
onBlur={onBlur}
onCancel={onCancel}
onSubmit={onSubmit}
onEdit={onEdit}
Expand Down Expand Up @@ -128,8 +130,11 @@ test("controlled: handles callbacks correctly", async () => {
// press `Escape`
fireEvent.keyDown(input, { key: "Escape" })

// calls `onCancel` with previous `value`
// calls `onSubmit` with previous `value`
expect(onSubmit).toHaveBeenCalledWith("World")

// calls `onBlur` with previous value
expect(onBlur).toHaveBeenCalledWith("World")
})

test("handles preview and input callbacks", async () => {
Expand Down Expand Up @@ -292,3 +297,39 @@ test("should not be interactive when disabled", async () => {
await user.click(screen.getByText(/editable/))
expect(screen.getByTestId("input")).not.toBeVisible()
})

test("should return focus to button when closed", async () => {
const Component = () => {
const buttonRef = React.useRef(null)
return (
<>
<button type="button" ref={buttonRef} data-testid="button">
Open
</button>
<Editable finalFocusRef={buttonRef} defaultValue="editable">
<EditablePreview data-testid="preview" />
<EditableInput data-testid="input" />
</Editable>
,
</>
)
}

const screen = render(<Component />)

const preview = screen.getByTestId("preview")
const input = screen.getByTestId("input")
const button = screen.getByTestId("button")

await screen.user.click(preview)

// make sure button isn't focused at the start
expect(button).not.toHaveFocus()
expect(input).toHaveFocus()

// blur the input
fireEvent.blur(input)

// wait for button to be focused
await waitFor(() => expect(button).toHaveFocus())
})

1 comment on commit d6b6941

@vercel
Copy link

@vercel vercel bot commented on d6b6941 Apr 22, 2023

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.