@wordpress/format-library 4.27.2 → 4.28.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/build/bold/index.js +1 -2
  3. package/build/bold/index.js.map +1 -1
  4. package/build/code/index.js +1 -2
  5. package/build/code/index.js.map +1 -1
  6. package/build/default-formats.js +1 -2
  7. package/build/default-formats.js.map +1 -1
  8. package/build/default-formats.native.js +1 -2
  9. package/build/default-formats.native.js.map +1 -1
  10. package/build/image/index.js +1 -2
  11. package/build/image/index.js.map +1 -1
  12. package/build/italic/index.js +1 -2
  13. package/build/italic/index.js.map +1 -1
  14. package/build/keyboard/index.js +1 -2
  15. package/build/keyboard/index.js.map +1 -1
  16. package/build/language/index.js +1 -2
  17. package/build/language/index.js.map +1 -1
  18. package/build/link/index.js +73 -24
  19. package/build/link/index.js.map +1 -1
  20. package/build/link/index.native.js +1 -2
  21. package/build/link/index.native.js.map +1 -1
  22. package/build/link/inline.js +74 -76
  23. package/build/link/inline.js.map +1 -1
  24. package/build/link/modal-screens/link-picker-screen.native.js +1 -2
  25. package/build/link/modal-screens/link-picker-screen.native.js.map +1 -1
  26. package/build/link/modal-screens/link-settings-screen.native.js +1 -2
  27. package/build/link/modal-screens/link-settings-screen.native.js.map +1 -1
  28. package/build/link/modal-screens/screens.native.js +1 -2
  29. package/build/link/modal-screens/screens.native.js.map +1 -1
  30. package/build/link/modal.native.js +1 -2
  31. package/build/link/modal.native.js.map +1 -1
  32. package/build/link/use-link-instance-key.js +1 -2
  33. package/build/link/use-link-instance-key.js.map +1 -1
  34. package/build/strikethrough/index.js +1 -2
  35. package/build/strikethrough/index.js.map +1 -1
  36. package/build/subscript/index.js +1 -2
  37. package/build/subscript/index.js.map +1 -1
  38. package/build/superscript/index.js +1 -2
  39. package/build/superscript/index.js.map +1 -1
  40. package/build/text-color/index.js +6 -7
  41. package/build/text-color/index.js.map +1 -1
  42. package/build/text-color/index.native.js +1 -2
  43. package/build/text-color/index.native.js.map +1 -1
  44. package/build/text-color/inline.js +6 -12
  45. package/build/text-color/inline.js.map +1 -1
  46. package/build/underline/index.js +1 -2
  47. package/build/underline/index.js.map +1 -1
  48. package/build/unknown/index.js +1 -2
  49. package/build/unknown/index.js.map +1 -1
  50. package/build-module/link/index.js +74 -24
  51. package/build-module/link/index.js.map +1 -1
  52. package/build-module/link/inline.js +72 -73
  53. package/build-module/link/inline.js.map +1 -1
  54. package/build-module/text-color/index.js +2 -1
  55. package/build-module/text-color/index.js.map +1 -1
  56. package/build-module/text-color/inline.js +7 -13
  57. package/build-module/text-color/inline.js.map +1 -1
  58. package/package.json +14 -14
  59. package/src/link/index.js +84 -35
  60. package/src/link/inline.js +87 -93
  61. package/src/text-color/index.js +1 -0
  62. package/src/text-color/inline.js +2 -12
package/src/link/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * WordPress dependencies
3
3
  */
4
4
  import { __ } from '@wordpress/i18n';
5
- import { useState } from '@wordpress/element';
5
+ import { useState, useLayoutEffect } from '@wordpress/element';
6
6
  import {
7
7
  getTextContent,
8
8
  applyFormat,
@@ -18,7 +18,7 @@ import {
18
18
  RichTextShortcut,
19
19
  } from '@wordpress/block-editor';
20
20
  import { decodeEntities } from '@wordpress/html-entities';
21
- import { link as linkIcon, linkOff } from '@wordpress/icons';
21
+ import { link as linkIcon } from '@wordpress/icons';
22
22
  import { speak } from '@wordpress/a11y';
23
23
 
24
24
  /**
@@ -39,18 +39,47 @@ function Edit( {
39
39
  contentRef,
40
40
  } ) {
41
41
  const [ addingLink, setAddingLink ] = useState( false );
42
+ // We only need to store the button element that opened the popover. We can ignore the other states, as they will be handled by the onFocus prop to return to the rich text field.
43
+ const [ openedBy, setOpenedBy ] = useState( null );
42
44
 
43
- function addLink() {
45
+ useLayoutEffect( () => {
46
+ const editableContentElement = contentRef.current;
47
+ if ( ! editableContentElement ) {
48
+ return;
49
+ }
50
+
51
+ function handleClick( event ) {
52
+ // There is a situation whereby there is an existing link in the rich text
53
+ // and the user clicks on the leftmost edge of that link and fails to activate
54
+ // the link format, but the click event still fires on the `<a>` element.
55
+ // This causes the `addingLink` state to be set to `true` and the link UI
56
+ // to be rendered in "creating" mode. We need to check isActive to see if
57
+ // we have an active link format.
58
+ if ( event.target.tagName !== 'A' || ! isActive ) {
59
+ return;
60
+ }
61
+
62
+ setAddingLink( true );
63
+ }
64
+
65
+ editableContentElement.addEventListener( 'click', handleClick );
66
+
67
+ return () => {
68
+ editableContentElement.removeEventListener( 'click', handleClick );
69
+ };
70
+ }, [ contentRef, isActive ] );
71
+
72
+ function addLink( target ) {
44
73
  const text = getTextContent( slice( value ) );
45
74
 
46
- if ( text && isURL( text ) && isValidHref( text ) ) {
75
+ if ( ! isActive && text && isURL( text ) && isValidHref( text ) ) {
47
76
  onChange(
48
77
  applyFormat( value, {
49
78
  type: name,
50
79
  attributes: { url: text },
51
80
  } )
52
81
  );
53
- } else if ( text && isEmail( text ) ) {
82
+ } else if ( ! isActive && text && isEmail( text ) ) {
54
83
  onChange(
55
84
  applyFormat( value, {
56
85
  type: name,
@@ -58,15 +87,48 @@ function Edit( {
58
87
  } )
59
88
  );
60
89
  } else {
90
+ if ( target ) {
91
+ setOpenedBy( target );
92
+ }
61
93
  setAddingLink( true );
62
94
  }
63
95
  }
64
96
 
65
- function stopAddingLink( returnFocus = true ) {
97
+ /**
98
+ * Runs when the popover is closed via escape keypress, unlinking the selected text,
99
+ * but _not_ on a click outside the popover. onFocusOutside handles that.
100
+ */
101
+ function stopAddingLink() {
102
+ // Don't let the click handler on the toolbar button trigger again.
103
+
104
+ // There are two places for us to return focus to on Escape keypress:
105
+ // 1. The rich text field.
106
+ // 2. The toolbar button.
107
+
108
+ // The toolbar button is the only one we need to handle returning focus to.
109
+ // Otherwise, we rely on the passed in onFocus to return focus to the rich text field.
110
+
111
+ // Close the popover
66
112
  setAddingLink( false );
67
- if ( returnFocus ) {
113
+ // Return focus to the toolbar button or the rich text field
114
+ if ( openedBy?.tagName === 'BUTTON' ) {
115
+ openedBy.focus();
116
+ } else {
68
117
  onFocus();
69
118
  }
119
+ // Remove the openedBy state
120
+ setOpenedBy( null );
121
+ }
122
+
123
+ // Test for this:
124
+ // 1. Click on the link button
125
+ // 2. Click the Options button in the top right of header
126
+ // 3. Focus should be in the dropdown of the Options button
127
+ // 4. Press Escape
128
+ // 5. Focus should be on the Options button
129
+ function onFocusOutside() {
130
+ setAddingLink( false );
131
+ setOpenedBy( null );
70
132
  }
71
133
 
72
134
  function onRemoveFormat() {
@@ -82,36 +144,23 @@ function Edit( {
82
144
  character="k"
83
145
  onUse={ onRemoveFormat }
84
146
  />
85
- { isActive && (
86
- <RichTextToolbarButton
87
- name="link"
88
- icon={ linkOff }
89
- title={ __( 'Unlink' ) }
90
- onClick={ onRemoveFormat }
91
- isActive={ isActive }
92
- shortcutType="primaryShift"
93
- shortcutCharacter="k"
94
- aria-haspopup="true"
95
- aria-expanded={ addingLink || isActive }
96
- />
97
- ) }
98
- { ! isActive && (
99
- <RichTextToolbarButton
100
- name="link"
101
- icon={ linkIcon }
102
- title={ title }
103
- onClick={ addLink }
104
- isActive={ isActive }
105
- shortcutType="primary"
106
- shortcutCharacter="k"
107
- aria-haspopup="true"
108
- aria-expanded={ addingLink || isActive }
109
- />
110
- ) }
111
- { ( addingLink || isActive ) && (
147
+ <RichTextToolbarButton
148
+ name="link"
149
+ icon={ linkIcon }
150
+ title={ isActive ? __( 'Link' ) : title }
151
+ onClick={ ( event ) => {
152
+ addLink( event.currentTarget );
153
+ } }
154
+ isActive={ isActive || addingLink }
155
+ shortcutType="primary"
156
+ shortcutCharacter="k"
157
+ aria-haspopup="true"
158
+ aria-expanded={ addingLink }
159
+ />
160
+ { addingLink && (
112
161
  <InlineLinkUI
113
- addingLink={ addingLink }
114
162
  stopAddingLink={ stopAddingLink }
163
+ onFocusOutside={ onFocusOutside }
115
164
  isActive={ isActive }
116
165
  activeAttributes={ activeAttributes }
117
166
  value={ value }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * WordPress dependencies
3
3
  */
4
- import { useMemo, useRef, createInterpolateElement } from '@wordpress/element';
4
+ import { useMemo, createInterpolateElement } from '@wordpress/element';
5
5
  import { __, sprintf } from '@wordpress/i18n';
6
6
  import { speak } from '@wordpress/a11y';
7
7
  import { Popover } from '@wordpress/components';
@@ -21,9 +21,8 @@ import {
21
21
  import {
22
22
  __experimentalLinkControl as LinkControl,
23
23
  store as blockEditorStore,
24
- useCachedTruthy,
25
24
  } from '@wordpress/block-editor';
26
- import { useSelect } from '@wordpress/data';
25
+ import { useDispatch, useSelect } from '@wordpress/data';
27
26
 
28
27
  /**
29
28
  * Internal dependencies
@@ -42,9 +41,9 @@ const LINK_SETTINGS = [
42
41
  function InlineLinkUI( {
43
42
  isActive,
44
43
  activeAttributes,
45
- addingLink,
46
44
  value,
47
45
  onChange,
46
+ onFocusOutside,
48
47
  stopAddingLink,
49
48
  contentRef,
50
49
  } ) {
@@ -53,15 +52,22 @@ function InlineLinkUI( {
53
52
  // Get the text content minus any HTML tags.
54
53
  const richTextText = richLinkTextValue.text;
55
54
 
56
- const { createPageEntity, userCanCreatePages } = useSelect( ( select ) => {
57
- const { getSettings } = select( blockEditorStore );
58
- const _settings = getSettings();
59
-
60
- return {
61
- createPageEntity: _settings.__experimentalCreatePageEntity,
62
- userCanCreatePages: _settings.__experimentalUserCanCreatePages,
63
- };
64
- }, [] );
55
+ const { selectionChange } = useDispatch( blockEditorStore );
56
+
57
+ const { createPageEntity, userCanCreatePages, selectionStart } = useSelect(
58
+ ( select ) => {
59
+ const { getSettings, getSelectionStart } =
60
+ select( blockEditorStore );
61
+ const _settings = getSettings();
62
+
63
+ return {
64
+ createPageEntity: _settings.__experimentalCreatePageEntity,
65
+ userCanCreatePages: _settings.__experimentalUserCanCreatePages,
66
+ selectionStart: getSelectionStart(),
67
+ };
68
+ },
69
+ []
70
+ );
65
71
 
66
72
  const linkValue = useMemo(
67
73
  () => ( {
@@ -113,78 +119,85 @@ function InlineLinkUI( {
113
119
 
114
120
  const newText = nextValue.title || newUrl;
115
121
 
122
+ // Scenario: we have any active text selection or an active format.
123
+ let newValue;
116
124
  if ( isCollapsed( value ) && ! isActive ) {
117
125
  // Scenario: we don't have any actively selected text or formats.
118
- const toInsert = applyFormat(
119
- create( { text: newText } ),
126
+ const inserted = insert( value, newText );
127
+
128
+ newValue = applyFormat(
129
+ inserted,
120
130
  linkFormat,
121
- 0,
122
- newText.length
131
+ value.start,
132
+ value.start + newText.length
123
133
  );
124
- onChange( insert( value, toInsert ) );
125
- } else {
126
- // Scenario: we have any active text selection or an active format.
127
- let newValue;
128
-
129
- if ( newText === richTextText ) {
130
- // If we're not updating the text then ignore.
131
- newValue = applyFormat( value, linkFormat );
132
- } else {
133
- // Create new RichText value for the new text in order that we
134
- // can apply formats to it.
135
- newValue = create( { text: newText } );
136
-
137
- // Apply the new Link format to this new text value.
138
- newValue = applyFormat(
139
- newValue,
140
- linkFormat,
141
- 0,
142
- newText.length
143
- );
144
-
145
- // Get the boundaries of the active link format.
146
- const boundary = getFormatBoundary( value, {
147
- type: 'core/link',
148
- } );
149
-
150
- // Split the value at the start of the active link format.
151
- // Passing "start" as the 3rd parameter is required to ensure
152
- // the second half of the split value is split at the format's
153
- // start boundary and avoids relying on the value's "end" property
154
- // which may not correspond correctly.
155
- const [ valBefore, valAfter ] = split(
156
- value,
157
- boundary.start,
158
- boundary.start
159
- );
160
-
161
- // Update the original (full) RichTextValue replacing the
162
- // target text with the *new* RichTextValue containing:
163
- // 1. The new text content.
164
- // 2. The new link format.
165
- // As "replace" will operate on the first match only, it is
166
- // run only against the second half of the value which was
167
- // split at the active format's boundary. This avoids a bug
168
- // with incorrectly targetted replacements.
169
- // See: https://github.com/WordPress/gutenberg/issues/41771.
170
- // Note original formats will be lost when applying this change.
171
- // That is expected behaviour.
172
- // See: https://github.com/WordPress/gutenberg/pull/33849#issuecomment-936134179.
173
- const newValAfter = replace( valAfter, richTextText, newValue );
174
-
175
- newValue = concat( valBefore, newValAfter );
176
- }
177
134
 
178
135
  onChange( newValue );
136
+
137
+ // Close the Link UI.
138
+ stopAddingLink();
139
+
140
+ // Move the selection to the end of the inserted link outside of the format boundary
141
+ // so the user can continue typing after the link.
142
+ selectionChange( {
143
+ clientId: selectionStart.clientId,
144
+ identifier: selectionStart.attributeKey,
145
+ start: value.start + newText.length + 1,
146
+ } );
147
+
148
+ return;
149
+ } else if ( newText === richTextText ) {
150
+ newValue = applyFormat( value, linkFormat );
151
+ } else {
152
+ // Scenario: Editing an existing link.
153
+
154
+ // Create new RichText value for the new text in order that we
155
+ // can apply formats to it.
156
+ newValue = create( { text: newText } );
157
+ // Apply the new Link format to this new text value.
158
+ newValue = applyFormat( newValue, linkFormat, 0, newText.length );
159
+
160
+ // Get the boundaries of the active link format.
161
+ const boundary = getFormatBoundary( value, {
162
+ type: 'core/link',
163
+ } );
164
+
165
+ // Split the value at the start of the active link format.
166
+ // Passing "start" as the 3rd parameter is required to ensure
167
+ // the second half of the split value is split at the format's
168
+ // start boundary and avoids relying on the value's "end" property
169
+ // which may not correspond correctly.
170
+ const [ valBefore, valAfter ] = split(
171
+ value,
172
+ boundary.start,
173
+ boundary.start
174
+ );
175
+
176
+ // Update the original (full) RichTextValue replacing the
177
+ // target text with the *new* RichTextValue containing:
178
+ // 1. The new text content.
179
+ // 2. The new link format.
180
+ // As "replace" will operate on the first match only, it is
181
+ // run only against the second half of the value which was
182
+ // split at the active format's boundary. This avoids a bug
183
+ // with incorrectly targetted replacements.
184
+ // See: https://github.com/WordPress/gutenberg/issues/41771.
185
+ // Note original formats will be lost when applying this change.
186
+ // That is expected behaviour.
187
+ // See: https://github.com/WordPress/gutenberg/pull/33849#issuecomment-936134179.
188
+ const newValAfter = replace( valAfter, richTextText, newValue );
189
+
190
+ newValue = concat( valBefore, newValAfter );
179
191
  }
180
192
 
193
+ onChange( newValue );
194
+
181
195
  // Focus should only be returned to the rich text on submit if this link is not
182
196
  // being created for the first time. If it is then focus should remain within the
183
197
  // Link UI because it should remain open for the user to modify the link they have
184
198
  // just created.
185
199
  if ( ! isNewLink ) {
186
- const returnFocusToRichText = true;
187
- stopAddingLink( returnFocusToRichText );
200
+ stopAddingLink();
188
201
  }
189
202
 
190
203
  if ( ! isValidHref( newUrl ) ) {
@@ -203,26 +216,9 @@ function InlineLinkUI( {
203
216
 
204
217
  const popoverAnchor = useAnchor( {
205
218
  editableContentElement: contentRef.current,
206
- settings,
219
+ settings: { ...settings, isActive },
207
220
  } );
208
221
 
209
- // As you change the link by interacting with the Link UI
210
- // the return value of document.getSelection jumps to the field you're editing,
211
- // not the highlighted text. Given that useAnchor uses document.getSelection,
212
- // it will return null, since it can't find the <mark> element within the Link UI.
213
- // This caches the last truthy value of the selection anchor reference.
214
- // This ensures the Popover is positioned correctly on initial submission of the link.
215
- const cachedRect = useCachedTruthy( popoverAnchor.getBoundingClientRect() );
216
- popoverAnchor.getBoundingClientRect = () => cachedRect;
217
-
218
- // Focus should only be moved into the Popover when the Link is being created or edited.
219
- // When the Link is in "preview" mode focus should remain on the rich text because at
220
- // this point the Link dialog is informational only and thus the user should be able to
221
- // continue editing the rich text.
222
- // Ref used because the focusOnMount prop shouldn't evolve during render of a Popover
223
- // otherwise it causes a render of the content.
224
- const focusOnMount = useRef( addingLink ? 'firstElement' : false );
225
-
226
222
  async function handleCreate( pageTitle ) {
227
223
  const page = await createPageEntity( {
228
224
  title: pageTitle,
@@ -252,9 +248,8 @@ function InlineLinkUI( {
252
248
  return (
253
249
  <Popover
254
250
  anchor={ popoverAnchor }
255
- focusOnMount={ focusOnMount.current }
256
251
  onClose={ stopAddingLink }
257
- onFocusOutside={ () => stopAddingLink( false ) }
252
+ onFocusOutside={ onFocusOutside }
258
253
  placement="bottom"
259
254
  offset={ 10 }
260
255
  shift
@@ -263,7 +258,6 @@ function InlineLinkUI( {
263
258
  value={ linkValue }
264
259
  onChange={ onChangeLink }
265
260
  onRemove={ removeLink }
266
- forceIsEditingLink={ addingLink }
267
261
  hasRichPreviews
268
262
  createSuggestion={ createPageEntity && handleCreate }
269
263
  withCreateSuggestion={ userCanCreatePages }
@@ -120,6 +120,7 @@ function TextColorEdit( {
120
120
  value={ value }
121
121
  onChange={ onChange }
122
122
  contentRef={ contentRef }
123
+ isActive={ isActive }
123
124
  />
124
125
  ) }
125
126
  </>
@@ -15,7 +15,6 @@ import {
15
15
  getColorObjectByColorValue,
16
16
  getColorObjectByAttributeValues,
17
17
  store as blockEditorStore,
18
- useCachedTruthy,
19
18
  } from '@wordpress/block-editor';
20
19
  import {
21
20
  Popover,
@@ -147,22 +146,13 @@ export default function InlineColorUI( {
147
146
  onChange,
148
147
  onClose,
149
148
  contentRef,
149
+ isActive,
150
150
  } ) {
151
151
  const popoverAnchor = useAnchor( {
152
152
  editableContentElement: contentRef.current,
153
- settings,
153
+ settings: { ...settings, isActive },
154
154
  } );
155
155
 
156
- /*
157
- As you change the text color by typing a HEX value into a field,
158
- the return value of document.getSelection jumps to the field you're editing,
159
- not the highlighted text. Given that useAnchor uses document.getSelection,
160
- it will return null, since it can't find the <mark> element within the HEX input.
161
- This caches the last truthy value of the selection anchor reference.
162
- */
163
- const cachedRect = useCachedTruthy( popoverAnchor.getBoundingClientRect() );
164
- popoverAnchor.getBoundingClientRect = () => cachedRect;
165
-
166
156
  return (
167
157
  <Popover
168
158
  onClose={ onClose }