-
Notifications
You must be signed in to change notification settings - Fork 4.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Edit in place hook #28924
Edit in place hook #28924
Conversation
Size Change: +5.68 kB (0%) Total Size: 1.38 MB
ℹ️ View Unchanged
|
Would be good to add a story for this component so we can better test this PR. All components in this package are supposed to have one, anyway. |
@grzim This is an interesting idea! I checked out the code and the story. |
@grzim Haii!! Here's my idea :) I think this interaction would work very well as a hook. However, the editing toggling mechanism remains the same - it's just the elements may be different :). With that in mind, I refactored the main logic from the component into a dedicated hook: function cancelEvent( event ) {
event.preventDefault();
event.stopPropagation();
}
function mergeEvent( handler, otherHandler ) {
return ( event ) => {
if ( typeof handler === 'function' ) {
handler( event );
}
if ( typeof otherHandler === 'function' ) {
otherHandler( event );
}
};
}
function useInlineEdit( {
validate = negate( isUndefined ),
onChange = noop,
value: valueProp,
} ) {
const [ isEdit, setIsEdit ] = useState( false );
const [ editingValue, setEditingValue ] = useState( valueProp );
const inputRef = useRef();
const toggleRef = useRef();
useEffect( () => {
if ( isEdit ) {
inputRef.current.focus();
inputRef.current.select();
setEditingValue( valueProp );
} else {
toggleRef.current.focus();
}
}, [ isEdit, valueProp ] );
const handleOnCommit = ( event ) => {
const { value } = event.target;
cancelEvent( event );
if ( validate( value ) ) {
setIsEdit( false );
onChange( value );
}
};
const handleOnChange = ( event ) => {
setEditingValue( event.target.value );
};
const handleOnKeyDown = ( event ) => {
if ( 'Enter' === event.key ) {
handleOnCommit( event );
}
if ( 'Escape' === event.key ) {
cancelEvent( event );
event.target.blur();
}
};
const handleOnClick = () => setIsEdit( true );
const getInputProps = ( inputProps ) => ( {
ref: inputRef,
onChange: mergeEvent( handleOnChange, inputProps?.onChange ),
onKeyDown: mergeEvent( handleOnKeyDown, inputProps?.onKeyDown ),
onBlur: mergeEvent( handleOnCommit, inputProps?.onBlur ),
} );
const getToggleProps = ( toggleProps ) => ( {
ref: toggleRef,
onClick: mergeEvent( handleOnClick, toggleProps?.toggleProps ),
} );
const value = isEdit ? editingValue : valueProp;
return {
isEdit,
getInputProps,
getToggleProps,
value,
};
} Note: This doesn't have to be the final implementation of the hook. There are probably some enhancements + refactors we can do . I updated the hook to better handle This hook doesn't have any UI. You would bind it to UI like this: function MyCustomEditInPlaceControl( { value: valueProp, onChange = noop } ) {
const { isEdit, getInputProps, getToggleProps, value } = useInlineEdit( {
onChange,
value: valueProp,
} );
const customHandleOnClick = noop;
return (
<>
{ isEdit ? (
<input { ...getInputProps() } value={ value } />
) : (
<button
{ ...getToggleProps( { onClick: customHandleOnClick } ) }
>
{ value }
</button>
) }
</>
);
} With this setup, we can use whatever elements we need to. We also have the freedom to add whatever callback handlers we need as well (e.g. Let me know what you think :) Also, cc'ing @diegohaz for thoughts! I've learned a lot about hook-based setups from Reakit. Would love to know your thoughts Diego! I think we could perhaps add some a11y handling in the hook as well. ❤️ |
It looks cool as a hook. To which package should it be added though? It is not a component now |
@grzim That's a great question. I'm not sure 🤔 . The general hooks package is cc'ing @sarayourfriend What do you think :) |
I think we should add it to Move the hook into a |
@sarayourfriend I don't think we'd need an actual component with this one. Just the hook :). Some maybe something like:
|
useInlineEdit marked as ustable Co-authored-by: sarayourfriend <[email protected]>
Great work @grzim! I don't think we should encourage this component/hook to be used for toolbar items though (like in your
If there's a toolbar with a text input element, it's better not to use the Besides that, this component alone is really hard to get right in terms of accessibility. When using a screen reader, you'll hear The simplest way to handle this interaction is to have a separate button for the action (for example, with a pencil icon and an explicit or implicit label) and keep the state as a text element. I see no problem in replacing this separate button with an input and temporarily hiding the text element while the input has focus. |
@diegohaz the current PR consists of a hook only (please see the updated description). Any kind of aria-labels, roles etc. should be used with components and they are not a part of a hook itself (hooks are part of the business logic and should not be tightly coupled with view). |
Co-authored-by: sarayourfriend <[email protected]>
Co-authored-by: sarayourfriend <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The types and code LGTM but lets wait for @diegohaz to respond about the a11y concerns. I want to make sure we're not introducing any negative patterns by accident 🙂 Great work so far, thank you!
useEffect( () => { | ||
setEditingValue( propValue ); | ||
if ( isInvalid( value ) ) onWrongInput( value ); | ||
}, [ propValue ] ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isInvalid
and onWrongInput
functions are being omitted from the effect dependencies. I understand it would be annoying to include them there since those props could be passed as inline functions and would trigger the effect on every re-render. But I think we should at least add a comment explaining that the effect will get out of sync if the validate
logic is changed while value
remains the same. I remember @ItsJonQ worked on something similar.
The local value
is also omitted from the deps.
Can't we just call onWrongInput
on the onChange
/onBlur
event? It would be much simpler than doing that in an effect.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I remember @ItsJonQ worked on something similar.
@diegohaz Ah! Are you referring to this usePropRef
pattern I came up with?
https://github.com/ItsJonQ/g2/blob/main/packages/utils/src/hooks/use-prop-ref.js
// example
const propRefs = usePropRef({ value, step })
const increment = useCallback(() => {
const { value, step } = propRefs.current
onChange(value + step)
}, [onChange, propRefs])
call onWrongInput on the onChange/onBlur event?
I haven't tried it, but that seems like that would be simpler 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@diegohaz As far as I understand what you mean is that we want to make it possible the component will react if a validation (or error handling) function changes. From my perspective, we can do only two things: add those functions as the dependencies and the component will rerender every time, which will lead to poor performance or to skip these dependencies (with a comment as you suggest) as I think it is an edge case.
Solution with usePropRef
would not apply here as the component would not react for a function change, please see the sandbox:
https://codesandbox.io/s/suspicious-sanne-74lv9?file=/src/App.js
We can use this pattern though, in order to add references to the hooks dependency array and not to make it rerender every time. This will make a code cleaner but the problem will remain the same.
But all in all, if we validate the value only onCommit (see my recent commit) then the entire problem just vanishes 🦩
@grzim Hmm! I think now the description lacks a reason why we need this hook in the first place. Do you have concrete use cases in mind? My concern is that this hook encourages a pattern that is really hard to get right in terms of accessibility, and this is a design problem. Are we considering alternatives to this pattern? Like this one:
|
Co-authored-by: Haz <[email protected]>
@grzim + @diegohaz Haiii!! Helping to expand on this.. I personally can't identify a specific application interaction at the moment. I've also seen this pattern used in UI like contact/user/profiles. To provide a way to quickly edit a single field in UI with many fields within limited space. Implementation aside... Do you think this can work from an a11y perspective?
I'm having a bit of trouble imagining this one 😊 . Would you be able to mock it up super fast in a CodeSandbox? Thank you 🙏 ! |
That's a data grid, where Here's an example: https://codesandbox.io/s/reakit-data-grid-m95te The inputs are already there in the markup. They aren't buttons that turn into inputs.
As I said, this component, the way it's designed here, is really hard to get right. If we don't have enough space to show an "Edit" button, let's just render an input with a default value and give it appropriate styles on focus or hover. I still think that the lack of indication that it's an editable element is problematic even for sighted users, but at least keyboard and screen reader users wouldn't be affected. The problem is that this component has both a state (the value) and an action (edit the value). Unlike "Buttons" that represent both state and action have different semantics (like
Sure! Here's a CodeSandbox with the two alternatives I had in mind: https://codesandbox.io/s/edit-in-place-bvn2o The first one is just an input, which is the most simple solution I can think of. The second one is the one you asked about, but I ended up using a read-only input that turns into an editable input instead of replacing the element. The implementation could be improved, but I think the input and the button together communicate more clearly the state and the action. I guess the challenge there is controlling the width of the input. |
This thread took a long way beginning in #25343 where designs included an edit-in-place component. As it was not needed there was an idea to leave this component anyway if there is a solid reason to do so. The next iteration was to change it into a hook to make it more flexible. |
Description
Hook providing with refs to bind two elements.
One ref is used to set 'isEdit' state to true upon clicking. The second is used to set 'isEdit' state to false on 'Enter'/onBlur event and triggering onCommit callback with its value.
This PR included a component in the first place, but the hook gives more flexibility.
How has this been tested?
Tested manually.
Screenshots
Types of changes
New feature
Checklist: