-
Notifications
You must be signed in to change notification settings - Fork 394
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(form-builder): move the handling of ensuring parent item visibili…
…ty to the inputs that needs it. (#2329) When a document node value is being edited in a dialog we want to make sure the input/preview of the parent/enclosing is scrolled into view in the background. Originally we solved this by responding to changes in the first element of the focus path (e.g. the top most document field name), and whenever this changed, we would scroll to the dom element with this as the value for `data-focus-path`. As a consequence, opening an item for edit would scroll the editor to the path of the _parent input_ into view, and this would sometimes trigger a scroll jump actually scrolling the field out of view because scrollIntoView would ensure the top edge of the parent input was in view. Instead, what we really want to ensure here is that the edited _item_ is in the view. This fixes the issue by moving the logic for keeping the item in view to the individual input components that needs this behavior. For now, it is only the ArrayOfObjectsInput and the PortableTextInput that requires this. Technically, ImageInput and FileInput also has this behavior, but these don't support deep linking, so it's less of a pressing issue here. As a bonus, this PR also adds a subtle focus ring on the background element that is currently being edited.
- Loading branch information
Showing
15 changed files
with
149 additions
and
42 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
/* eslint-disable no-shadow */ | ||
// eslint is giving false positives no-shadow for typed arguments here | ||
|
||
import {useEffect} from 'react' | ||
import {usePrevious} from './usePrevious' | ||
|
||
/** | ||
* A hook for doing side effects as a response to a change in a hook value between renders | ||
* Usage: | ||
* ```js | ||
* useDidUpdate(hasFocus, (hadFocus, hasFocus) => { | ||
* if (hasFocus) { | ||
* scrollIntoView(elementRef.current) | ||
* } | ||
* }) | ||
* ``` | ||
* @param current The value you want to respond to changes in | ||
* @param didUpdate Callback to run when the value changes | ||
*/ | ||
export function useDidUpdate<T>(current: T, didUpdate: (previous: T, current: T) => void): void | ||
export function useDidUpdate<T>( | ||
current: T, | ||
didUpdate: (previous: T, current: T | undefined) => void | ||
): void { | ||
const previous = usePrevious<T>(current) | ||
|
||
useEffect(() => { | ||
if (previous !== current) { | ||
didUpdate(previous, current) | ||
} | ||
}, [didUpdate, current, previous]) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import {useEffect, useRef} from 'react' | ||
|
||
/** | ||
* A hook that returns the previous value of a component variable | ||
* This might be provided by React in the future (https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state) | ||
* @param value The value to track. Will return undefined for first render | ||
*/ | ||
export function usePrevious<T>(value: T): T { | ||
const ref = useRef<T>() | ||
useEffect(() => { | ||
ref.current = value | ||
}, [value]) | ||
return ref.current | ||
} |
28 changes: 28 additions & 0 deletions
28
packages/@sanity/form-builder/src/hooks/useScrollIntoViewOnFocusWithin.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import {useCallback} from 'react' | ||
import scrollIntoView from 'scroll-into-view-if-needed' | ||
import {useDidUpdate} from './useDidUpdate' | ||
|
||
const SCROLL_OPTIONS = {scrollMode: 'if-needed'} as const | ||
|
||
/** | ||
* A hook to help make sure the parent element of a value edited in a dialog (or "out of band") stays | ||
visible in the background | ||
* @param elementRef The element to scroll into view when the proivided focusWithin changes from true to false | ||
* @param hasFocusWithin A boolean indicating whether we have has focus within the currently edited value | ||
*/ | ||
export function useScrollIntoViewOnFocusWithin( | ||
elementRef: {current?: HTMLElement}, | ||
hasFocusWithin: boolean | ||
): void { | ||
return useDidUpdate( | ||
hasFocusWithin, | ||
useCallback( | ||
(hadFocus) => { | ||
if (!hadFocus) { | ||
scrollIntoView(elementRef.current, SCROLL_OPTIONS) | ||
} | ||
}, | ||
[elementRef] | ||
) | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
9 changes: 0 additions & 9 deletions
9
packages/@sanity/form-builder/src/inputs/ArrayInput/item/helpers.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,5 @@ | ||
import * as PathUtils from '@sanity/util/paths' | ||
import {IGNORE_KEYS} from './constants' | ||
|
||
export function pathSegmentFrom(value) { | ||
return {_key: value._key} | ||
} | ||
|
||
export function hasFocusInPath(path, value) { | ||
return path.length === 1 && PathUtils.isSegmentEqual(path[0], pathSegmentFrom(value)) | ||
} | ||
|
||
export function isEmpty(value) { | ||
return Object.keys(value).every((key) => IGNORE_KEYS.includes(key)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import {isKeyedObject, isKeySegment, Path, PathSegment} from '@sanity/types' | ||
|
||
// Tests whether a keyed value matches a given keyed pathSegment | ||
function matchesSegment(segment: PathSegment, value: unknown) { | ||
return isKeyedObject(value) && isKeySegment(segment) && value._key === segment._key | ||
} | ||
|
||
// Utility to check if the given focusPath terminates at the given keyed value | ||
// E.g. focus is on the value itself and not a child node | ||
export function hasFocusAtPath(path: Path, value: unknown): boolean { | ||
return path.length === 1 && matchesSegment(path[0], value) | ||
} | ||
|
||
// Utility to check if the given focusPath terminates at a child node of the given keyed value | ||
// E.g. focus is on a child node of the value and not the value itself | ||
export function hasFocusWithinPath(path: Path, value: unknown): boolean { | ||
return path.length > 1 && matchesSegment(path[0], value) | ||
} |