-
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
Declare and override autocompleters via filter #4609
Declare and override autocompleters via filter #4609
Conversation
If others agree this approach is reasonable, I'd also like to add a filter for transforming the default list of completers. For my use case, I'd want to add a completer for cross-posting to different P2s (e.g., |
91b616e
to
72d8371
Compare
components/autocomplete/index.js
Outdated
|
||
// Filter the options getter itself because we only want to invoke one, | ||
// whether it be default or custom. | ||
const optionsGetter = applyFilters( `autocomplete.${ type }.options.getter`, completer.getOptions ); |
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.
We need to standardize the way we name filters to have one pattern that is easy to follow. So far we were prefixing the hooks name with the package name followed by the component's name. So, in this case it would be something like: components.Autocomplete
. Another thing here is that we can combine those 3 filters into one as follows and call it once only when the component gets initialized:
this.props.completers = applyFilters( `components.Autocomplete.completers', this.props.defaultCompleters );
@brandonpayton, you can totally go for it. It's a valid use case and we should support a wide range of extensibility options. My only concern that your initial proposal it too granular and it would make it harder to discover for plugin developers. Another concern I have is that ideally all hooks should be applied only once when a component gets mounted. If we want to allow to dynamically add/remove/update hooks we can do it using event listeners provided by the |
Thanks for the review, encouragement, and guidance, @gziolo! I think you're right about the granularity, especially for a first pass. I'll update with a single filter that runs after the component is mounted. |
I noticed that @noisysocks is working on some refactoring related to autocompleters in #4769. Cross-linking for better visibility :) |
@noisysocks and I spoke briefly yesterday. Autocompleting reusable blocks means the completer needs access to the latest list. I spent some time thinking today, and here's what I'm seeing.
Here's an example of what I'm imagining for the blocks completer. {
completes: 'blocks',
className: 'blocks-autocompleters__block',
triggerPrefix: '/',
getOptions,
getOptionKeywords,
renderOption,
allowContext,
getOptionValue,
onSelect,
} I'm interested in your thoughts. If you have time, let's talk more on Monday. |
I may be wrong about this if the only expected context for a blocks completer is the editor. I've been assuming the editor only applies when editing a post, but if it also will be used in things like customization, then it probably doesn't make sense to provide a default I think the main thing for this PR is that option retrieval should be separated from rendering, so each can be overridden separately. |
@brandonpayton, I'm looking forward to seeing this PR updated. I need to go through the code to better understand how this would work. I don't know that well the internal implementation of autocompleters :) |
@gziolo I'm eager to work on this today but have to take care of something first. It might have to wait until tomorrow. Thanks for your comments and interest! |
72d8371
to
3b17dec
Compare
I updated the PR to...
I am working on...
|
I ran into serious a11y questions with moving to a single top-level Autocomplete instance and am keeping multiple instances for now. I updated Autocomplete to look up completers based on its containing block type but still need to update the tests. I also changed completers to simply return a result for a selected option and made it the responsibility of Autocomplete to insert text or replace the current block. This is the general idea, but it probably needs some cleanup. I'd still like a way for autocompletion to automatically apply for blocks with defined autocompleters, but I'm not sure how to do that in an accessible way since autocomplete-related aria attributes and UI should be directly associated with each input field. |
Currently, I'm thinking autocompleters should be added to the context with |
I'm thinking Autocomplete should remain a simple props-driven This will allow autocompleters to be declared along with block registration and overridden at that granularity. To support blocks with multiple text inputs and different autocompleters for each, this will be insufficient. I'm thinking we should get this working at the block level first and iterate if we need further granularity. |
blocks/autocompleters/index.js
Outdated
export function blockAutocompleter() { | ||
let promisedOptions; | ||
|
||
const getOptions = () => promisedOptions || ( promisedOptions = Promise.resolve( |
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 you want to make sure this is called only once you can use lodash.once
const getOptions = once( () => Promise.resolve... );
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.
Thanks for the tip! That's cleaner.
I'll make the update.
blocks/autocompleters/index.js
Outdated
const onSelect = ( blockName ) => { | ||
onReplace( createBlock( blockName ) ); | ||
}; | ||
const getOptionResult = blockData => createBlock( blockData.name ); |
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 don't know this code so just wanted to double check why to remove onReplace
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.
It started with a dependency issue when moving completers to block registration. We didn't yet have an onReplace
.
Then I thought completers should be more declarative anyway. The presence of an onSomething
name made me think about that. Why should a completer know that it is replacing something in the editor? The idea is that the editor has a better idea of what it wants to do with the result of a completer selection.
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 it ensures we remove the existing paragraph block.
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 Autocomplete
component now calls onReplace
instead when a block is returned.
ae62091#diff-8d7b3f0b32214bb1340578e3be07ed96R176
The idea is that a completer should not be involved in making actual changes to the content.
blocks/autocompleters/index.js
Outdated
<span key="name" className="blocks-autocompleters__user-name">{ user.name }</span>, | ||
<span key="slug" className="blocks-autocompleters__user-slug">{ user.slug }</span>, | ||
]; | ||
|
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 this part, it makes it nicely composable out of smaller pieces.
components/autocomplete/index.js
Outdated
if ( getOptionResult ) { | ||
const replacement = getOptionResult( option.value, range, query ); | ||
|
||
if ( replacement && typeof replacement === 'object' && getBlockType( replacement.name ) ) { |
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.
Is there maybe an existing API method which validated if the input is a block? There is a similar logic in here:
https://github.com/WordPress/gutenberg/blob/master/blocks/block-edit/index.js#L22
We might want to extract this code and put in the method that would be used it in both places.
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.
Agreed. I'll look at 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 took a look and am wondering whether it is worth it to a function for the check. The only thing in common is that both make sure a truthy value is returned from getBlockType
. Defining an isBlockType
function in blocks/api/registration
may allow us to be more direct though.
Currently, I simply moved the conditions from components/Autocomplete
to blocks/contextual-autocomplete
in the form of the isReplacement
prop.
blocks/autocompleters/test/index.js
Outdated
] ); | ||
const completer = blockAutocompleter( { } ); | ||
const renderedLabels = getBlockTypes().map( | ||
blockType => completer.renderOptionLabel( blockType ) |
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 looks like you can perform all assertions inside this loop.
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.
Updated.
blocks/autocompleters/test/index.js
Outdated
renderedLabels.forEach( label => { | ||
// Only verify that a populated label is returned. | ||
// It is likely to be fragile to assert that the contents are renderable by @wordpress/element. | ||
if ( Array.isArray( label ) ) { |
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 can we make it more explicit what is what? At the moment it might work, but it's hard to decipher what we are checking 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.
Updated.
blocks/library/paragraph/index.js
Outdated
blockAutocompleter( { onReplace } ), | ||
userAutocompleter(), | ||
] }> | ||
<Autocomplete getBlockType={ getBlockType } onReplace={ onReplace }> |
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.
We might want to iterate on getBlockType
passed as prop. Maybe it should be a part of the autocompleter object. Not sure.
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 will go away when I make the update for BlockEdit to provide autocompleters via context. There will be no need to find the containing block type to provide autocompletion context because that will be provided naturally via React context.
blocks/library/paragraph/index.js
Outdated
@@ -254,6 +251,11 @@ export const settings = { | |||
], | |||
}, | |||
|
|||
autocompleters: [ |
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 probably makes the most sense to have it here 👍 Let's see how it goes :)
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.
Is there a particular use case we have in mind for having a block type define what autocompleters it wants? (I like to err on the side of you aren't gonna need it, especially since registerBlockType
is a public API.)
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.
You might want different autocompleters per block. A concrete example is that a block autocompleter makes sense in a paragraph but not in the caption field of a cover image. If you offer block completion there, it is more likely an opportunity for surprising replacement than a feature IMO.
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.
What If you want different autocompleters per RichText? is this a valid use case? could this be a prop of RichText
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'm not sure, but it seems reasonable to want different autocompleters per RichText. I'm not sure what overriding should look like in that case though.
I'm wondering whether this PR is trying to do too much and, in retrospect, would like to have created an autocompletion extensibility issue for discussion and smaller PRs. But if we can adjust in the future, maybe it would be good to continue as-is and get more feedback once merged.
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.
Sounds good to me @brandonpayton
components/autocomplete/index.js
Outdated
const blockNode = inputNode.closest( '.editor-block-list__block' ); | ||
const blockName = get( blockNode, 'dataset.type', '' ); | ||
completers = blockName ? | ||
get( getBlockType( blockName ), 'autocompleters', [] ) : |
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.
Yes, we might want to inject this, too. This is really interesting :)
bf0c6ce
to
e3277e1
Compare
I updated this PR according to review notes made by @pento and believe it is ready for another review.
cc @gziolo |
The API for creating autocompleters is feeling a lot more accessible, nice work @brandonpayton! Could you add a Other than that, 👍🏻 from me for the API. @gziolo, could you review the internals? |
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 started the review. I left a few comments. I will download all code tomorrow and perform some testing.
contenteditable="true" | ||
data-is-placeholder-visible="true" | ||
/> | ||
<div> |
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.
There are 3 additional divs in here, at least 2 of them look obsolete. It should be investigated further. Hopefully, it can be fixed.
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 noticed this as well and was initially puzzled, but they are divs added by the Autocomplete component now used within RichText:
Autocomplete
adds a div to wrap TinyMCE, adding event listeners and a sibling popup.withFocusOutside
wrapsAutocomplete
and adds a parent div because it adds event listeners.- The autocompleters hook adds another parent div in order to add its own event listeners.
I dislike this but do not currently see a better solution. I would love to be able to add event listeners to a Fragment (mentioned here as a possible future capability) to avoid rendering the elements. I'll give it some thought. Do you have any suggestions?
docs/extensibility.md
Outdated
{% ES5 %} | ||
```js | ||
// Our completer | ||
function abbreviationCompleter( completers ) { |
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.
completers
is not used inside the function, should be removed.
docs/extensibility.md
Outdated
{% ESNext %} | ||
```js | ||
// Our completer | ||
function abbreviationCompleter( completers ) { |
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.
completers
is not used inside the function, should be removed.
blocks/hooks/autocompleters.js
Outdated
} | ||
} | ||
|
||
updateFilteredCompleters( nextCompleters ) { |
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 can be also simplified to:
updateFilteredCompleters( nextCompleters = defaultCompleters ) {
by using a default param 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.
Thanks, I'm not sure how I missed that :). Based on one of your other comments, I've updated the PR to provide defaults via a filter instead, so this no longer applies.
blocks/hooks/autocompleters.js
Outdated
|
||
this.setState( { | ||
hasStaleCompleters: false, | ||
completers: applyFilters( |
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.
There is also hasFilter
method which would be useful here to avoid state updates when there are no filters registered.
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.
In this case, the state must always be updated after new completer props are received because the filtered completers are maintained in the state. This is done on demand when the component first contains focus after receiving new completer props.
We can still use hasFilter
to avoid copying the completers list when there is no filter added, and I'm making that change now.
blocks/hooks/autocompleters.js
Outdated
* @param {Function} Autocomplete Original component. | ||
* @return {Function} Wrapped component | ||
*/ | ||
export function filterAutocomplete( Autocomplete ) { |
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.
There is a convention to prefix all HOCs with with
prefix. In this case, it might be better to name it withFilteredAutocompleters
.
blocks/hooks/test/autocompleters.js
Outdated
wrapper.find( 'input' ).simulate( 'focus' ); | ||
expect( completersFilter ).toHaveBeenCalledWith( expectedCompleters ); | ||
} finally { | ||
wrapper.unmount(); |
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'd be less code to put wrapper.unmount()
inside afterEach
call instead of using try / finally in all tests.
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 was actually doing it that way initially but switched away because I thought I would be able to use shallow
for most of the tests. I was wrong. :)
I started to prefer the explicit try/finally because it is easy to accidentally declare a local const wrapper
that shadows the outer variable, but I just noticed we have a no-shadow
lint rule that covers this case.
I'll switch it back.
blocks/hooks/autocompleters.js
Outdated
*/ | ||
import defaultCompleters from '../autocompleters'; | ||
|
||
/** |
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.
As far as I understand what happens here, it seems like it would be enough to have only one filter registered for autocompleters: components.Autocomplete.completers
.
Most of the logic that is inside this file is directly related to the Autocomplete component. Instead of adding filter on the component.Autocomplete
, it is also possible to set default autocompleters using:
const setDefaultAutocompleters = () => [ ...defaultCompleters ];
addFilter(
'components.Autocomplete.completers',
'core/autocompleters/set-default-autocompleters',
setDefaultAutocompleters
);
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 not sure I correctly understand your comment, but my reading is that this component wrapper may not be necessary. Is that correct?
The purpose of this module is to filter autocompleters passed to Autocomplete components and to provide default completers when none are provided. The filtering is done on-demand per Autocomplete component when focus first enters the component after receiving new completers via props.
Your comment made me realize it would be more idiomatic to provide default completers via a components.Autocomplete.completers
filter, and I've made that update.
I added the first version of a README to |
1661d2a
to
5c637d0
Compare
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 left a few more comments, which circle back to my latest comments. In addition, I would like to make sure we have a good strategy for rollout. We should do our best to inform plugin developers about breaking changes introduced.
docs/extensibility.md
Outdated
@@ -153,3 +153,86 @@ _Note:_ This filter must always be run on every page load, and not in your brows | |||
Extending the editor UI can be accomplished with the `registerPlugin` API, allowing you to define all your plugin's UI elements in one place. | |||
|
|||
Refer to [the plugins module documentation](../plugins/) for more information. | |||
|
|||
## Overriding autocompleters |
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.
There were some major updates to the extensibility docs. I think this should now land into its own file.
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.
OK, I will move them in an autocompletion.md
doc. Does that work for you?
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 all depends if you decide to have it as block specific or component specific :)
Here is the document for Extending Blocks.
It might be also its own Extending Components
document. Autocompletion might be too granular.
components/autocomplete/index.js
Outdated
@@ -3,6 +3,7 @@ | |||
*/ | |||
import classnames from 'classnames'; | |||
import { escapeRegExp, find, filter, map } from 'lodash'; | |||
import 'element-closest'; |
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.
wp.components.Autocomplete
was exposed for some time. Is there any way to make it backward compatible for the next 2 release cycles? We should also add a deprecation message to give a warning about the changes.
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.
We should be able to detect a difference in the completer objects and log a deprecation message then. Do we want to limit such messages or print for every completer object seen with the old interface?
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.
In other cases, it was done per function/component occurrence. See #5833 for reference.
@@ -0,0 +1,117 @@ | |||
Autocompleters |
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 have a feeling that this should be documented next to the original Autocomplete
component. It is also breaking change as far as I understand. We should provide some notes how to update code when someone was using wp.components.Autocomplete
.
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 tend to agree with you. I don't know why the completer interface should be documented within @wordpress/blocks
and not with the wp.components.Autocomplete
component. I plan to move the completer interface JSDoc into components/autocomplete/index.js
. Sound reasonable?
The blocks completer probably belongs within @wordpress/blocks
.
I feel better keeping the user completer within @wordpress/blocks
as well because @wordpress/components
seems to be more generic than the users module which actually hits the WP REST API.
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.
We should provide some notes how to update code when someone was using wp.components.Autocomplete.
Where is a good place to provide such notes?
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 plan to move the completer interface JSDoc into components/autocomplete/index.js. Sound reasonable?
Yes, this was my exactly my point. It's a general interface that should work with every custom Autocomplete
component.
Where is a good place to provide such notes?
I think it is enough to include deprecated
function in the code and leave a link to the new interface since it is now very well documented.
blocks/autocompleters/index.js
Outdated
onSelect, | ||
}; | ||
} | ||
export default [ userAutocompleter ]; |
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 not convinced that we should have the default setup exposed in here. We should rather set it in the hook.
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.
Agreed. I think that is a better idea. I'll make a note to make the update once we decide what to do instead of the components.Autocomplete
filter.
blocks/hooks/autocompleters.js
Outdated
return completers; | ||
} | ||
|
||
addFilter( 'components.Autocomplete', 'core/autocompleters', withFilteredAutocompleters ); |
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 still don't understand why we need components.Autocomplete
filter. It is applied globally so it wraps all autocompleters. Could it be applied directly in components/Autocomplete
? Or should we alternatively create BlockAutocomplete
component and use it inside RichText
?
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 don't believe there is anything here that requires the components.Autocomplete
filter. I am trying to remember my frame of mind when I wrote this. In this comment, I may have thought you were suggesting we add this functionality separately via such a filter, so it is added when blocks are in use. Now that you mention it though, I suppose block and non-block UI may exist simultaneously so that we don't want to do this for all instances.
Should autocompletion extensibility be a feature limited to @wordpress/blocks
or also supported for @wordpress/components
?
My mind has been set on blocks, and I have not given much thought to WordPress UI outside of blocks. I don't yet know what I think about that question, but an answer will help us know what to do instead of this unnecessary filter.
If it is a blocks-only feature, I think creating a BlockAutocomplete
component makes sense to provide the on-focus behavior currently added via the components.Autocomplete
filter, and we should also use a blocks-related filter name instead of the generic components.Autocomplete.completers
.
I'm thinking that the opinion of a default completers list should also be limited to blocks, but if we want autocompleters to be extensible for @wordpress/components
as well, I am not sure how to filter and provide defaults only within a block context because the filter could be applied in both block and non-block contexts.
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 went through a similar thinking process for MediaUpload
component in #5846. I wanted to move it to components
module, but the feedback I received suggests to leave it inside blocks
module together with the accompanying filer. I think it makes a lot of sense because the idea is to have a set of independent components as @youknowriad explained:
I'm not sure we want to move components relying on filters to the components folder. I feel like the components folder is for generic React components while adding filters adds an unwanted dependency the
wp hooks
.
I would follow that advice and remove the components.Autocomplete
filter and introduce BlockAutocomplete
component which uses components.Autocomplete.completers
internally.
5c637d0
to
2bc0bf5
Compare
/> | ||
<p | ||
class="blocks-rich-text__tinymce wp-block-paragraph" | ||
<div |
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.
So it looks like this issue with the obsolete div
elements isn't something new. Let's leave it as it is and circle back later. We can investigate if React 16.3 has something to offer to mitigate it. https://reactjs.org/blog/2018/03/29/react-v-16-3.html - introduces createRef
and forwardRef
- they might be what we need in this case. We still need to expose those methods in a separate 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.
I left another round of comments :)
docs/extensibility/autocompletion.md
Outdated
|
||
// Adding the filter | ||
wp.hooks.addFilter( | ||
'components.Autocomplete.completers', |
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 be blocks
not components
:)
docs/extensibility/autocompletion.md
Outdated
|
||
// Adding the filter | ||
wp.hooks.addFilter( | ||
'components.Autocomplete.completers', |
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 be blocks
not components
:)
```js | ||
// Our completer | ||
var acronymCompleter = { | ||
name: 'acronyms', |
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.
Nit: Should we use acronym
name to match with the variable name and description?
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've struggled with whether to make name singular or plural. It feels weird to call it "acronym" because it is not an acronym, it completes acronyms. I ended up using plural for the user and block completers and wanted to be consistent here. I'm fine changing it to singular everywhere though. Any thoughts?
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.
Let’s keep it consistent. No need to update 👍
} | ||
|
||
export function toCompatibleCompleter( deprecatedCompleter ) { | ||
deprecated( 'Original autocompleter interface', { |
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.
Nice 👍
compatibleCompleter.className = deprecatedCompleter.className; | ||
} | ||
if ( 'allowNode' in deprecatedCompleter ) { | ||
compatibleCompleter.allowNode = ( ...args ) => deprecatedCompleter.allowNode( ...args ); |
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 you map other props like this?
const extraProps = [ 'className', 'allowNode', 'allowContext' ].
filter( key => key in deprecatedCompleter ).
map( key => deprecatedCompleter[ key ];
const compatibleCompleter = {
name: generateCompleterName(),
// ...
...extraProps,
};
It seems like those functions don't add any new logic.
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, that's more concise. I wanted to preserve completer context for the methods in that list, but Autocomplete
already doesn't call them with completer context.
blocks/autocompleters/index.js
Outdated
}; | ||
} | ||
export { default as blockAutocompleter } from './block'; | ||
export { default as userAutocompleter } from './user'; |
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.
Nice, thanks 👍
components/autocomplete/README.md
Outdated
Autocomplete | ||
============ | ||
|
||
TODO |
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 can be very brief - usually, one line is enough :)
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.
Adding a one line description works for me. :) I was thinking I needed to document props for the Autocomplete
component as well since the README didn't yet exist, but that doesn't have to be part of 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.
Follow up is fine
<abbr title={ option.name }>{ option.visual }</abbr> | ||
), | ||
}; | ||
``` |
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.
Can we also add a line where it is wired into an actual component?
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 created a simple example wiring to Autocomplete
wrapping an input
element, but it didn't work. It seems like Autocomplete
is assuming a contenteditable target, since it inserts HTML rather than simple text. I'll play with a simple contenteditable example but don't have anything yet.
blocks/autocompleters/README.md
Outdated
Autocompleters | ||
============== | ||
|
||
Autocompleters enable us to offer users options for completing text input. For example, Gutenberg includes a user autocompleter that provides a list of user names and completes a selection with a user mention like `@mary`. |
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.
Can we add a reference to @wordpress/components
? It contains the same content and will be difficult to keep in sync otherwise.
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 actually meant to remove that entirely after moving the content but missed doing so last night.
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.
Leaving a README with a reference to @wordpress/components
seems good. Is there a better way than direct GitHub links to link to component READMEs?
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.
Unfortunately not yet :(
blocks/autocomplete/index.js
Outdated
const hasFocus = this.parentNode.contains( document.activeElement ); | ||
|
||
// This may trigger another render but only when the component has focus. | ||
if ( hasFocus && this.hasStaleCompleters() ) { |
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.
Can you explain why we need this additional check in here? It seems like having onFocus
handler should be enough.
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 technically possible for the completers
prop to change while the component contains the focus. This check exists for that case, since onFocus
will already have been called. I don't think that is likely to occur, but I'd rather just cover the case. Sound OK?
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 hope it’s good 😃It makes sense, might make sense to leave an inline comment.
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's mostly about not assuming when props will be updated. Sure, I can add a comment. :)
I noticed that this PR introduces regression for the following use cases:
|
Thanks for the last round of comments. I'm going through them and will look into the regressions. I'd noticed some awkwardness focusing the cite input but thought it was unrelated to this PR. I'm glad you caught it. |
36c1585
to
6174f58
Compare
I fixed the regressions and made updates in response to review feedback. I'd like to add a couple more automated tests, but this is ready for another round of manual testing. |
6174f58
to
70f9eb4
Compare
Looking good, I've tested it out and auto complete is working in lists, captions, quotes, heading and I assume other RichText components. Nice work! |
70f9eb4
to
c48f351
Compare
link: 'https://github.com/WordPress/gutenberg/blob/master/components/autocomplete/README.md', | ||
} ); | ||
|
||
const optionalProperties = [ 'className', 'allowNode', 'allowContext' ] |
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 needs to be converted to an object, my fault :)
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 the same thing. It's funny how our minds can just fill in the gaps sometimes. Thanks for fixing it.
I'm seeing the following error once per 20ish focus changes:
It doesn't break anything, but it would be nice to make sure it isn't triggered. |
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.
We went through so many iterations and it looks really solid. I tested it quite long today and didn't find any issues. It is a really great improvement 👍
I made sure that deprecation message is printed to the console when using old autocomplete intereface.
I also tested acronym completer example applied with hook. It works like a charm. It even gets applied just after I install it using JS console.
Great work. There are some random errors that might be not related to this PR, but it would be nice if you would try to reproduce them.
Time to merge 🎉
Sure, I'll play with this to see what I can find. Thanks for fixing the silly mistake with array spread instead of object spread in the completer compat module. Also thank you for all the time you gave reviewing and testing this. 🙇 |
Description
This is a PR for declaring and overriding autocompleters during block registration.
How Has This Been Tested?
I have manually tested with an ad hoc plugin, have added unit tests, and will be adding more.
Types of changes
type
property so they can be identified.onSelect
function and replacing it withgetOptionCompletion
that returns an action/value pair. Currently supported actions areinsert-at-caret
andreplace-block
. This detangles completers from runtime dependencies on things such as theonReplace
function. Now, theAutocompleter
component uses the completion action to decide how to handle the result.getOptionCompletion
function. This isn't a stance against the concept of value, but it seemed like unnecessary indirection in the current implementation.blocks/autocompleters
.RichText
to include autocompletion support, including a newautocompleters
prop that can be used to specify completers in place of the default.blocks/hooks/default-autocompleters
module to provide a default list of completers for blocks that don't explicitly provide their own.blocks.Autocomplete.completers
filter is applied when theAutocomplete
component is first focused after receiving a new list of completers.Checklist: