-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Add placeholder functionality to Editable component #475
Conversation
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 wonder what's the accessibility impact on fake placeholders 🤔
blocks/components/editable/index.js
Outdated
editor.on( 'NewBlock', this.onNewBlock ); | ||
editor.on( 'focusin', this.onFocus ); | ||
editor.on( 'input', this.onChange ); |
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'm concerned about the performance impact of binding to input
in combination with getContent
, which will become even worse with #466.
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.
Without any content processing, and with the content size being so small, I think this is fine if we keep it in mind. For blocks like freeform, less so.
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.
My concern here is that this fires too often which will cause the updateContent
to trigger often which can create focus jumps while typing.
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.
Tested this branch and I'm already having focus jumps while typing. These are really hard to solve in my exp with the per-block prototype.
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'll have another look at this in a bit.
blocks/components/editable/index.js
Outdated
|
||
return ( | ||
<Tag | ||
ref={ this.bindNode } | ||
style={ style } | ||
className={ classes } /> | ||
className={ classes } | ||
placeholder={ placeholder } /> |
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.
Wouldn't placeholder
be an invalid attribute for non-input/textarea elements? Maybe data-placeholder
instead?
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 did a quick check and was unable to find an answers so far for contenteditable
. data-*
is probably safer.
blocks/components/editable/index.js
Outdated
const { tagName: Tag = 'div', style, className } = this.props; | ||
const classes = classnames( 'blocks-editable', className ); | ||
const { tagName: Tag = 'div', style, className, value, placeholder } = this.props; | ||
const classes = classnames( 'blocks-editable', className, { 'is-empty': ! value } ); |
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.
This and the above onChange
logic on value
will need some reconsideration if #466 is to come to be.
position: relative; | ||
} | ||
|
||
&.is-empty:before { |
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.
Should the ::placeholder
pseudo element be used here? The browser support is not good, so ::before
is probably best?
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.
@BE-Webdesign Huh, I wasn't aware of that pseudo-element. You're probably right on the point about browser support. Out of curiosity, for those browsers that do support it, would it work on non-input types like how we're using it here?
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.
After looking it over, I believe it only exists on <input>
, and <textarea>
. I forgot that we are using content editable lol. ::placeholder
and various vendor prefixes are often used to create consistency across the various browsers for the way they display the placeholder text, so I think ::before
definitely makes the most sense now, disregard my comments :). I don't think you can set the content
as a property for ::placeholder
, so even if using it made sense it won't help us at all.
blocks/components/editable/index.js
Outdated
} | ||
const value = this.editor.getContent(); | ||
this.editor.save(); | ||
this.props.onChange( value ); |
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 think relying on the dirty
status is a good thing. More performant that value checks, especially after a rebase (which will update the shape of the value).
Are these changes mandatory for this PR?
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.
If we sync changes on input, why would we save and dirty check?
#523 would be good to have for this, so we might be able to sync changes as the DOM changes, instead of just on blur. |
Since #523 is merged, I had another look at this, but it turns out the captions are now an array of paragraphs. I'm not entirely sure if this is intentional, but it makes placeholders a bit more difficult to implement. |
I might expect it's an array (based on behavior of |
@aduth Yeah I'm not sure why it's paragraphs then. :) |
dbb6a10
to
69b128e
Compare
The empty check feels like it should belong somewhere else... Like React vNode helpers or something. Or state selectors/helpers. |
69b128e
to
9569254
Compare
It is true that we are getting and setting bookmarks on updateContent... I feel like we should sync the selection here and reset it from state rather than injecting a DOM node. Looking. |
9569254
to
b5fdb00
Compare
b5fdb00
to
98be27b
Compare
This last commit is very much a work in progress. Maybe it should be split off in another PR. The idea is to continuously keep the state in sync. This means calling Having the content in sync is handy to show a placeholder because we can directly check the props. Keeping a simple selection state has several benefits. We don't need to set a bookmark as we can derive a new range from this state. If new content comes in, and the selection doesn't match what was selected before, it will be reset. You can try this by e.g. changing the heading levels. New content comes in, old selection is applied. Another cool side effect is that some checks become quite simple. Checking if the caret is at the start of the editor: Keeping a selection state will prove useful in the future as well when we start thinking about applying formatting. |
0046e02
to
e24bdb7
Compare
There's an an error sometimes with the formatting, looking into that. |
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 like how this normalize how we store the selection. I think we could benefit from having this selection in the focus
config in the state.
Some bugs I'm seeing:
- After merging two text blocks, the selection should be collapsed
- The link control is not working
- Hovering a text block can select its content (not always)
- Merging a text with a heading create 'br' tags
blocks/components/editable/index.js
Outdated
return; | ||
} | ||
|
||
if ( ! isEqual( this.props.value, this.content ) ) { |
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'm thinking the isEqual
might not be super useful? Will this always be true?
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.
It is not always equal, actually it will be more unequal as equal. No need to call onChange needlessly. Re selectionChange
, I've found it be be one of the best events to pick up content changes. It always work regardless of the input method.
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 think I'd raised it previously, but I'm concerned about the performance implication here. isEqual
is a deep equality check which can be very slow, and here it's occurring on every keyboard event, selection interaction, focus, etc.
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.
@aduth What would you suggest? Calling onChange all the time? We might want to do it anyway later if we set the selection too.
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.
@aduth What would you suggest? Calling onChange all the time? We might want to do it anyway later if we set the selection too.
Personally I didn't see it being hugely problematic that we only triggered the change callback when removing focus from the block. Another issue we have with this is that UPDATE_BLOCK
is repeatedly dispatched while typing. This might be ideal from the perspective of ensuring the Editable value and state are kept in sync, but is fairly wasteful to reconcile. For example, with these changes we're not only doing a deep equality here, but another needless one in componentDidUpdate
.
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 see what you're getting at, but I also don't think it's desirable to undo one character at a time. We'll probably want to consider some interval to update state (e.g. debounced change). Do you have any insight on how browsers or TinyMCE manage it?
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.
Hm, I was hoping this is something we'd handle in the reducers, or dispatch ADD_UNDO_LEVEL.
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.
Hm, I was hoping this is something we'd handle in the reducers, or dispatch ADD_UNDO_LEVEL.
I might not be communicating well, but I agree with you on this. I just don't think we need each keystroke to trigger an update to state; we can choose different intervals instead (focus out, after a 5s debounce delay, every new paragraph added, etc). Not something we necessarily need to flesh out here or now.
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.
Or maybe we want to add some content transformations in the future, and things like checking empty content, which all need immediate feedback.
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.
It could also be local state with local reducers? Would that be a strange thing? Not sure.
blocks/components/editable/index.js
Outdated
this.editor.selection.moveToBookmark( bookmark ); | ||
// Saving the editor on updates avoid unecessary onChanges calls | ||
// These calls can make the focus jump | ||
getChildIndex( child ) { |
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.
Could this be changed to Array.from( parentNode.childNodes ).indexOf( child )
?
blocks/components/editable/index.js
Outdated
this.editor.save(); | ||
this.props.onChange( this.savedContent ); | ||
this.content = this.getContent(); | ||
this.selection = this.getSelection(); |
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.
Maybe we should do something like this.props.onFocus( this.getSelection() )
instead of storing the selection locally. We'll get the selection back in the focus
prop. And we'll be able to use it outside the Editable
to perform the merge/split or any selection related action outside the editable itself.
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.
Yep, I saw this as one of the next steps :)
92cd128
to
a45f9d4
Compare
a45f9d4
to
02b4fc2
Compare
6ea0bef
to
d81a2b6
Compare
I acknowledge that there are still a few range issues to be solved, and I have a good idea on how to solve these and keep on moving toward a more controlled component. It will require some extra things though, that may be better tried in a separate PR. I don't want to keep this issue open much longer. I'll adjust this so we only sync the state on blur and empty/fill, for now not when the formatting changes. |
Closing, but please keep the branch for now. |
To test, write in an image caption and make sure the placeholder appears when the field is empty, disappears when the field has text.