Skip to content
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

Closed
wants to merge 18 commits into from

Conversation

ellatrix
Copy link
Member

To test, write in an image caption and make sure the placeholder appears when the field is empty, disappears when the field has text.

Copy link
Member

@aduth aduth left a 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 🤔

editor.on( 'NewBlock', this.onNewBlock );
editor.on( 'focusin', this.onFocus );
editor.on( 'input', this.onChange );
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Member Author

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.


return (
<Tag
ref={ this.bindNode }
style={ style }
className={ classes } />
className={ classes }
placeholder={ placeholder } />
Copy link
Member

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?

Copy link
Member Author

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.

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 } );
Copy link
Member

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.

@ellatrix ellatrix added the [Focus] Accessibility (a11y) Changes that impact accessibility and need corresponding review (e.g. markup changes). label Apr 21, 2017
position: relative;
}

&.is-empty:before {
Copy link
Contributor

@BE-Webdesign BE-Webdesign Apr 24, 2017

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?

Copy link
Member

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?

Copy link
Contributor

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.

}
const value = this.editor.getContent();
this.editor.save();
this.props.onChange( value );
Copy link
Contributor

@youknowriad youknowriad Apr 26, 2017

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?

Copy link
Member Author

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?

@ellatrix ellatrix added this to the Prototype Parity milestone Apr 26, 2017
@ellatrix
Copy link
Member Author

#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.

@mtias mtias added the [Priority] High Used to indicate top priority items that need quick attention label May 1, 2017
@mtias mtias modified the milestones: May Week 1, Prototype Parity May 1, 2017
@mtias mtias added the [Feature] Blocks Overall functionality of blocks label May 1, 2017
@ellatrix
Copy link
Member Author

ellatrix commented May 2, 2017

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.

@aduth
Copy link
Member

aduth commented May 2, 2017

I'm not entirely sure if this is intentional

I might expect it's an array (based on behavior of children matcher), but of a mixture of text nodes and strong, a, and em tags, not paragraphs.

@mkaz mkaz mentioned this pull request May 2, 2017
@ellatrix
Copy link
Member Author

ellatrix commented May 3, 2017

@aduth Yeah I'm not sure why it's paragraphs then. :)

@ellatrix
Copy link
Member Author

ellatrix commented May 4, 2017

The empty check feels like it should belong somewhere else... Like React vNode helpers or something. Or state selectors/helpers.

@ellatrix
Copy link
Member Author

ellatrix commented May 4, 2017

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.

@mtias mtias added the [Feature] Block API API that allows to express the block paradigm. label May 4, 2017
@ellatrix ellatrix removed this from the May Week 1 milestone May 4, 2017
@ellatrix
Copy link
Member Author

ellatrix commented May 4, 2017

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 onChange on every change in the editor. Since the content is the same when the new props come in, there is no need to set the content. It is only set if it's different.

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: this.selection.isCollapsed && ! compact( this.selection.start ).length. Similarly logic to split blocks.

Keeping a selection state will prove useful in the future as well when we start thinking about applying formatting.

@ellatrix ellatrix force-pushed the add/editable-placeholder branch 2 times, most recently from 0046e02 to e24bdb7 Compare May 5, 2017 12:05
@ellatrix
Copy link
Member Author

ellatrix commented May 5, 2017

There's an an error sometimes with the formatting, looking into that.

Copy link
Contributor

@youknowriad youknowriad left a 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

return;
}

if ( ! isEqual( this.props.value, this.content ) ) {
Copy link
Contributor

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?

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member Author

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.

this.editor.selection.moveToBookmark( bookmark );
// Saving the editor on updates avoid unecessary onChanges calls
// These calls can make the focus jump
getChildIndex( child ) {
Copy link
Contributor

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 )?

this.editor.save();
this.props.onChange( this.savedContent );
this.content = this.getContent();
this.selection = this.getSelection();
Copy link
Contributor

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.

Copy link
Member Author

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 :)

@ellatrix
Copy link
Member Author

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.

@ellatrix
Copy link
Member Author

Closing, but please keep the branch for now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Feature] Block API API that allows to express the block paradigm. [Feature] Blocks Overall functionality of blocks [Focus] Accessibility (a11y) Changes that impact accessibility and need corresponding review (e.g. markup changes). [Priority] High Used to indicate top priority items that need quick attention
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants