@wordpress/editor 12.10.0 → 12.13.0

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 (154) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/build/components/document-outline/index.js +7 -9
  3. package/build/components/document-outline/index.js.map +1 -1
  4. package/build/components/error-boundary/index.js +3 -0
  5. package/build/components/error-boundary/index.js.map +1 -1
  6. package/build/components/index.js +61 -9
  7. package/build/components/index.js.map +1 -1
  8. package/build/components/local-autosave-monitor/index.js +24 -19
  9. package/build/components/local-autosave-monitor/index.js.map +1 -1
  10. package/build/components/page-attributes/order.js +3 -7
  11. package/build/components/page-attributes/order.js.map +1 -1
  12. package/build/components/page-attributes/parent.js +8 -4
  13. package/build/components/page-attributes/parent.js.map +1 -1
  14. package/build/components/post-format/index.js +5 -8
  15. package/build/components/post-format/index.js.map +1 -1
  16. package/build/components/post-locked-modal/index.js +1 -1
  17. package/build/components/post-locked-modal/index.js.map +1 -1
  18. package/build/components/post-publish-panel/maybe-category-panel.js +2 -4
  19. package/build/components/post-publish-panel/maybe-category-panel.js.map +1 -1
  20. package/build/components/post-schedule/label.js +93 -13
  21. package/build/components/post-schedule/label.js.map +1 -1
  22. package/build/components/post-slug/index.js +8 -13
  23. package/build/components/post-slug/index.js.map +1 -1
  24. package/build/components/post-taxonomies/flat-term-selector.js +8 -4
  25. package/build/components/post-taxonomies/flat-term-selector.js.map +1 -1
  26. package/build/components/post-taxonomies/hierarchical-term-selector.js +1 -0
  27. package/build/components/post-taxonomies/hierarchical-term-selector.js.map +1 -1
  28. package/build/components/post-taxonomies/index.js +3 -1
  29. package/build/components/post-taxonomies/index.js.map +1 -1
  30. package/build/components/post-template/index.js +1 -2
  31. package/build/components/post-template/index.js.map +1 -1
  32. package/build/components/post-title/index.js +8 -2
  33. package/build/components/post-title/index.js.map +1 -1
  34. package/build/components/post-url/check.js +54 -0
  35. package/build/components/post-url/check.js.map +1 -0
  36. package/build/components/post-url/index.js +115 -0
  37. package/build/components/post-url/index.js.map +1 -0
  38. package/build/components/post-url/label.js +30 -0
  39. package/build/components/post-url/label.js.map +1 -0
  40. package/build/components/post-visibility/index.js +3 -1
  41. package/build/components/post-visibility/index.js.map +1 -1
  42. package/build/components/post-visibility/label.js +5 -0
  43. package/build/components/post-visibility/label.js.map +1 -1
  44. package/build/components/provider/index.native.js +3 -2
  45. package/build/components/provider/index.native.js.map +1 -1
  46. package/build/components/provider/use-block-editor-settings.js +2 -2
  47. package/build/components/provider/use-block-editor-settings.js.map +1 -1
  48. package/build/components/table-of-contents/panel.js +5 -1
  49. package/build/components/table-of-contents/panel.js.map +1 -1
  50. package/build/components/time-to-read/index.js +60 -0
  51. package/build/components/time-to-read/index.js.map +1 -0
  52. package/build/store/actions.js +5 -1
  53. package/build/store/actions.js.map +1 -1
  54. package/build/store/reducer.js +1 -1
  55. package/build/store/reducer.js.map +1 -1
  56. package/build/store/selectors.js +3 -3
  57. package/build/store/selectors.js.map +1 -1
  58. package/build-module/components/document-outline/index.js +7 -8
  59. package/build-module/components/document-outline/index.js.map +1 -1
  60. package/build-module/components/error-boundary/index.js +2 -0
  61. package/build-module/components/error-boundary/index.js.map +1 -1
  62. package/build-module/components/index.js +7 -4
  63. package/build-module/components/index.js.map +1 -1
  64. package/build-module/components/local-autosave-monitor/index.js +24 -18
  65. package/build-module/components/local-autosave-monitor/index.js.map +1 -1
  66. package/build-module/components/page-attributes/order.js +3 -6
  67. package/build-module/components/page-attributes/order.js.map +1 -1
  68. package/build-module/components/page-attributes/parent.js +6 -5
  69. package/build-module/components/page-attributes/parent.js.map +1 -1
  70. package/build-module/components/post-format/index.js +5 -8
  71. package/build-module/components/post-format/index.js.map +1 -1
  72. package/build-module/components/post-locked-modal/index.js +1 -1
  73. package/build-module/components/post-locked-modal/index.js.map +1 -1
  74. package/build-module/components/post-publish-panel/maybe-category-panel.js +2 -4
  75. package/build-module/components/post-publish-panel/maybe-category-panel.js.map +1 -1
  76. package/build-module/components/post-schedule/label.js +90 -13
  77. package/build-module/components/post-schedule/label.js.map +1 -1
  78. package/build-module/components/post-slug/index.js +8 -14
  79. package/build-module/components/post-slug/index.js.map +1 -1
  80. package/build-module/components/post-taxonomies/flat-term-selector.js +8 -6
  81. package/build-module/components/post-taxonomies/flat-term-selector.js.map +1 -1
  82. package/build-module/components/post-taxonomies/hierarchical-term-selector.js +1 -2
  83. package/build-module/components/post-taxonomies/hierarchical-term-selector.js.map +1 -1
  84. package/build-module/components/post-taxonomies/index.js +4 -1
  85. package/build-module/components/post-taxonomies/index.js.map +1 -1
  86. package/build-module/components/post-template/index.js +1 -2
  87. package/build-module/components/post-template/index.js.map +1 -1
  88. package/build-module/components/post-title/index.js +8 -2
  89. package/build-module/components/post-title/index.js.map +1 -1
  90. package/build-module/components/post-url/check.js +44 -0
  91. package/build-module/components/post-url/check.js.map +1 -0
  92. package/build-module/components/post-url/index.js +102 -0
  93. package/build-module/components/post-url/index.js.map +1 -0
  94. package/build-module/components/post-url/label.js +18 -0
  95. package/build-module/components/post-url/label.js.map +1 -0
  96. package/build-module/components/post-visibility/index.js +4 -2
  97. package/build-module/components/post-visibility/index.js.map +1 -1
  98. package/build-module/components/post-visibility/label.js +3 -0
  99. package/build-module/components/post-visibility/label.js.map +1 -1
  100. package/build-module/components/provider/index.native.js +3 -2
  101. package/build-module/components/provider/index.native.js.map +1 -1
  102. package/build-module/components/provider/use-block-editor-settings.js +2 -2
  103. package/build-module/components/provider/use-block-editor-settings.js.map +1 -1
  104. package/build-module/components/table-of-contents/panel.js +4 -1
  105. package/build-module/components/table-of-contents/panel.js.map +1 -1
  106. package/build-module/components/time-to-read/index.js +50 -0
  107. package/build-module/components/time-to-read/index.js.map +1 -0
  108. package/build-module/store/actions.js +5 -1
  109. package/build-module/store/actions.js.map +1 -1
  110. package/build-module/store/reducer.js +2 -2
  111. package/build-module/store/reducer.js.map +1 -1
  112. package/build-module/store/selectors.js +4 -4
  113. package/build-module/store/selectors.js.map +1 -1
  114. package/build-style/style-rtl.css +19 -18
  115. package/build-style/style.css +23 -18
  116. package/package.json +30 -29
  117. package/src/components/README.md +1 -1
  118. package/src/components/document-outline/index.js +9 -8
  119. package/src/components/error-boundary/index.js +3 -0
  120. package/src/components/index.js +13 -4
  121. package/src/components/local-autosave-monitor/index.js +24 -18
  122. package/src/components/page-attributes/order.js +1 -9
  123. package/src/components/page-attributes/parent.js +6 -13
  124. package/src/components/post-format/index.js +13 -19
  125. package/src/components/post-format/style.scss +2 -17
  126. package/src/components/post-locked-modal/index.js +1 -1
  127. package/src/components/post-publish-panel/maybe-category-panel.js +7 -6
  128. package/src/components/post-publish-panel/test/__snapshots__/index.js.snap +1 -1
  129. package/src/components/post-schedule/label.js +111 -17
  130. package/src/components/post-schedule/test/label.js +127 -15
  131. package/src/components/post-slug/index.js +7 -13
  132. package/src/components/post-slug/test/index.js +7 -6
  133. package/src/components/post-taxonomies/flat-term-selector.js +6 -5
  134. package/src/components/post-taxonomies/hierarchical-term-selector.js +1 -1
  135. package/src/components/post-taxonomies/index.js +3 -1
  136. package/src/components/post-template/index.js +1 -1
  137. package/src/components/post-title/index.js +8 -2
  138. package/src/components/post-title/style.scss +1 -1
  139. package/src/components/post-trash/style.scss +3 -0
  140. package/src/components/post-url/check.js +38 -0
  141. package/src/components/post-url/index.js +122 -0
  142. package/src/components/post-url/label.js +22 -0
  143. package/src/components/post-url/style.scss +16 -0
  144. package/src/components/post-visibility/index.js +2 -2
  145. package/src/components/post-visibility/label.js +4 -0
  146. package/src/components/provider/index.native.js +4 -8
  147. package/src/components/provider/use-block-editor-settings.js +4 -1
  148. package/src/components/table-of-contents/panel.js +7 -2
  149. package/src/components/time-to-read/index.js +59 -0
  150. package/src/store/actions.js +3 -1
  151. package/src/store/reducer.js +2 -2
  152. package/src/store/selectors.js +5 -6
  153. package/src/store/test/selectors.js +8 -10
  154. package/src/style.scss +1 -0
@@ -1,8 +1,3 @@
1
- /**
2
- * External dependencies
3
- */
4
- import { countBy, flatMap, get } from 'lodash';
5
-
6
1
  /**
7
2
  * WordPress dependencies
8
3
  */
@@ -50,7 +45,7 @@ const multipleH1Headings = [
50
45
  * @return {Array} An array of heading blocks enhanced with the properties described above.
51
46
  */
52
47
  const computeOutlineHeadings = ( blocks = [] ) => {
53
- return flatMap( blocks, ( block = {} ) => {
48
+ return blocks.flatMap( ( block = {} ) => {
54
49
  if ( block.name === 'core/heading' ) {
55
50
  return {
56
51
  ...block,
@@ -83,7 +78,13 @@ export const DocumentOutline = ( {
83
78
  // Not great but it's the simplest way to locate the title right now.
84
79
  const titleNode = document.querySelector( '.editor-post-title__input' );
85
80
  const hasTitle = isTitleSupported && title && titleNode;
86
- const countByLevel = countBy( headings, 'level' );
81
+ const countByLevel = headings.reduce(
82
+ ( acc, heading ) => ( {
83
+ ...acc,
84
+ [ heading.level ]: ( acc[ heading.level ] || 0 ) + 1,
85
+ } ),
86
+ {}
87
+ );
87
88
  const hasMultipleH1 = countByLevel[ 1 ] > 1;
88
89
 
89
90
  return (
@@ -155,7 +156,7 @@ export default compose(
155
156
  return {
156
157
  title: getEditedPostAttribute( 'title' ),
157
158
  blocks: getBlocks(),
158
- isTitleSupported: get( postType, [ 'supports', 'title' ], false ),
159
+ isTitleSupported: postType?.supports?.title ?? false,
159
160
  };
160
161
  } )
161
162
  )( DocumentOutline );
@@ -7,6 +7,7 @@ import { Button } from '@wordpress/components';
7
7
  import { select } from '@wordpress/data';
8
8
  import { Warning } from '@wordpress/block-editor';
9
9
  import { useCopyToClipboard } from '@wordpress/compose';
10
+ import { doAction } from '@wordpress/hooks';
10
11
 
11
12
  /**
12
13
  * Internal dependencies
@@ -36,6 +37,8 @@ class ErrorBoundary extends Component {
36
37
 
37
38
  componentDidCatch( error ) {
38
39
  this.setState( { error } );
40
+
41
+ doAction( 'editor.ErrorBoundary.errorLogged', error );
39
42
  }
40
43
 
41
44
  reboot() {
@@ -41,23 +41,32 @@ export { default as PostPublishPanel } from './post-publish-panel';
41
41
  export { default as PostSavedState } from './post-saved-state';
42
42
  export { default as PostSchedule } from './post-schedule';
43
43
  export { default as PostScheduleCheck } from './post-schedule/check';
44
- export { default as PostScheduleLabel } from './post-schedule/label';
44
+ export {
45
+ default as PostScheduleLabel,
46
+ usePostScheduleLabel,
47
+ } from './post-schedule/label';
45
48
  export { default as PostSlug } from './post-slug';
46
49
  export { default as PostSlugCheck } from './post-slug/check';
47
50
  export { default as PostSticky } from './post-sticky';
48
51
  export { default as PostStickyCheck } from './post-sticky/check';
49
52
  export { default as PostSwitchToDraftButton } from './post-switch-to-draft-button';
50
53
  export { default as PostTaxonomies } from './post-taxonomies';
51
- export { default as PostTaxonomiesFlatTermSelector } from './post-taxonomies/flat-term-selector';
52
- export { default as PostTaxonomiesHierarchicalTermSelector } from './post-taxonomies/hierarchical-term-selector';
54
+ export { FlatTermSelector as PostTaxonomiesFlatTermSelector } from './post-taxonomies/flat-term-selector';
55
+ export { HierarchicalTermSelector as PostTaxonomiesHierarchicalTermSelector } from './post-taxonomies/hierarchical-term-selector';
53
56
  export { default as PostTaxonomiesCheck } from './post-taxonomies/check';
54
57
  export { default as PostTextEditor } from './post-text-editor';
55
58
  export { default as PostTitle } from './post-title';
56
59
  export { default as PostTrash } from './post-trash';
57
60
  export { default as PostTrashCheck } from './post-trash/check';
58
61
  export { default as PostTypeSupportCheck } from './post-type-support-check';
62
+ export { default as PostURL } from './post-url';
63
+ export { default as PostURLCheck } from './post-url/check';
64
+ export { default as PostURLLabel, usePostURLLabel } from './post-url/label';
59
65
  export { default as PostVisibility } from './post-visibility';
60
- export { default as PostVisibilityLabel } from './post-visibility/label';
66
+ export {
67
+ default as PostVisibilityLabel,
68
+ usePostVisibilityLabel,
69
+ } from './post-visibility/label';
61
70
  export { default as PostVisibilityCheck } from './post-visibility/check';
62
71
  export { default as TableOfContents } from './table-of-contents';
63
72
  export { default as ThemeSupportCheck } from './theme-support-check';
@@ -1,8 +1,3 @@
1
- /**
2
- * External dependencies
3
- */
4
- import { once, uniqueId, omit } from 'lodash';
5
-
6
1
  /**
7
2
  * WordPress dependencies
8
3
  */
@@ -27,23 +22,30 @@ const requestIdleCallback = window.requestIdleCallback
27
22
  ? window.requestIdleCallback
28
23
  : window.requestAnimationFrame;
29
24
 
25
+ let hasStorageSupport;
26
+ let uniqueId = 0;
27
+
30
28
  /**
31
29
  * Function which returns true if the current environment supports browser
32
30
  * sessionStorage, or false otherwise. The result of this function is cached and
33
31
  * reused in subsequent invocations.
34
32
  */
35
- const hasSessionStorageSupport = once( () => {
36
- try {
37
- // Private Browsing in Safari 10 and earlier will throw an error when
38
- // attempting to set into sessionStorage. The test here is intentional in
39
- // causing a thrown error as condition bailing from local autosave.
40
- window.sessionStorage.setItem( '__wpEditorTestSessionStorage', '' );
41
- window.sessionStorage.removeItem( '__wpEditorTestSessionStorage' );
42
- return true;
43
- } catch ( error ) {
44
- return false;
33
+ const hasSessionStorageSupport = () => {
34
+ if ( typeof hasStorageSupport === 'undefined' ) {
35
+ try {
36
+ // Private Browsing in Safari 10 and earlier will throw an error when
37
+ // attempting to set into sessionStorage. The test here is intentional in
38
+ // causing a thrown error as condition bailing from local autosave.
39
+ window.sessionStorage.setItem( '__wpEditorTestSessionStorage', '' );
40
+ window.sessionStorage.removeItem( '__wpEditorTestSessionStorage' );
41
+ hasStorageSupport = true;
42
+ } catch ( error ) {
43
+ hasStorageSupport = false;
44
+ }
45
45
  }
46
- } );
46
+
47
+ return hasStorageSupport;
48
+ };
47
49
 
48
50
  /**
49
51
  * Custom hook which manages the creation of a notice prompting the user to
@@ -98,7 +100,7 @@ function useAutosaveNotice() {
98
100
  return;
99
101
  }
100
102
 
101
- const noticeId = uniqueId( 'wpEditorAutosaveRestore' );
103
+ const noticeId = `wpEditorAutosaveRestore${ ++uniqueId }`;
102
104
  createWarningNotice(
103
105
  __(
104
106
  'The backup of this post in your browser is different from the version below.'
@@ -109,7 +111,11 @@ function useAutosaveNotice() {
109
111
  {
110
112
  label: __( 'Restore the backup' ),
111
113
  onClick() {
112
- editPost( omit( edits, [ 'content' ] ) );
114
+ const {
115
+ content: editsContent,
116
+ ...editsWithoutContent
117
+ } = edits;
118
+ editPost( editsWithoutContent );
113
119
  resetEditorBlocks( parse( edits.content ) );
114
120
  removeNotice( noticeId );
115
121
  },
@@ -1,8 +1,3 @@
1
- /**
2
- * External dependencies
3
- */
4
- import { invoke } from 'lodash';
5
-
6
1
  /**
7
2
  * WordPress dependencies
8
3
  */
@@ -24,10 +19,7 @@ export const PageAttributesOrder = ( { onUpdateOrder, order = 0 } ) => {
24
19
  const setUpdatedOrder = ( value ) => {
25
20
  setOrderInput( value );
26
21
  const newOrder = Number( value );
27
- if (
28
- Number.isInteger( newOrder ) &&
29
- invoke( value, [ 'trim' ] ) !== ''
30
- ) {
22
+ if ( Number.isInteger( newOrder ) && value.trim?.() !== '' ) {
31
23
  onUpdateOrder( Number( value ) );
32
24
  }
33
25
  };
@@ -1,15 +1,8 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import {
5
- get,
6
- unescape as unescapeString,
7
- debounce,
8
- repeat,
9
- find,
10
- flatten,
11
- deburr,
12
- } from 'lodash';
4
+ import { get, unescape as unescapeString, debounce, find } from 'lodash';
5
+ import removeAccents from 'remove-accents';
13
6
 
14
7
  /**
15
8
  * WordPress dependencies
@@ -34,8 +27,8 @@ function getTitle( post ) {
34
27
  }
35
28
 
36
29
  export const getItemPriority = ( name, searchValue ) => {
37
- const normalizedName = deburr( name ).toLowerCase();
38
- const normalizedSearch = deburr( searchValue ).toLowerCase();
30
+ const normalizedName = removeAccents( name || '' ).toLowerCase();
31
+ const normalizedSearch = removeAccents( searchValue || '' ).toLowerCase();
39
32
  if ( normalizedName === normalizedSearch ) {
40
33
  return 0;
41
34
  }
@@ -99,7 +92,7 @@ export function PageAttributesParent() {
99
92
  {
100
93
  value: treeNode.id,
101
94
  label:
102
- repeat( '— ', level ) + unescapeString( treeNode.name ),
95
+ '— '.repeat( level ) + unescapeString( treeNode.name ),
103
96
  rawName: treeNode.name,
104
97
  },
105
98
  ...getOptionsFromTree( treeNode.children || [], level + 1 ),
@@ -111,7 +104,7 @@ export function PageAttributesParent() {
111
104
  return priorityA >= priorityB ? 1 : -1;
112
105
  } );
113
106
 
114
- return flatten( sortedNodes );
107
+ return sortedNodes.flat();
115
108
  };
116
109
 
117
110
  let tree = pageItems.map( ( item ) => ( {
@@ -81,24 +81,18 @@ export default function PostFormat() {
81
81
  return (
82
82
  <PostFormatCheck>
83
83
  <div className="editor-post-format">
84
- <div className="editor-post-format__content">
85
- <label htmlFor={ postFormatSelectorId }>
86
- { __( 'Post Format' ) }
87
- </label>
88
- <SelectControl
89
- value={ postFormat }
90
- onChange={ ( format ) => onUpdatePostFormat( format ) }
91
- id={ postFormatSelectorId }
92
- options={ formats.map( ( format ) => ( {
93
- label: format.caption,
94
- value: format.id,
95
- } ) ) }
96
- />
97
- </div>
98
-
84
+ <SelectControl
85
+ label={ __( 'Post Format' ) }
86
+ value={ postFormat }
87
+ onChange={ ( format ) => onUpdatePostFormat( format ) }
88
+ id={ postFormatSelectorId }
89
+ options={ formats.map( ( format ) => ( {
90
+ label: format.caption,
91
+ value: format.id,
92
+ } ) ) }
93
+ />
99
94
  { suggestion && suggestion.id !== postFormat && (
100
- <div className="editor-post-format__suggestion">
101
- { __( 'Suggestion:' ) }{ ' ' }
95
+ <p className="editor-post-format__suggestion">
102
96
  <Button
103
97
  variant="link"
104
98
  onClick={ () =>
@@ -107,11 +101,11 @@ export default function PostFormat() {
107
101
  >
108
102
  { sprintf(
109
103
  /* translators: %s: post format */
110
- __( 'Apply format: %s' ),
104
+ __( 'Apply suggested format: %s' ),
111
105
  suggestion.caption
112
106
  ) }
113
107
  </Button>
114
- </div>
108
+ </p>
115
109
  ) }
116
110
  </div>
117
111
  </PostFormatCheck>
@@ -1,18 +1,3 @@
1
- .editor-post-format {
2
- flex-direction: column;
3
- align-items: stretch;
4
- width: 100%;
5
- }
6
-
7
- .editor-post-format__content {
8
- display: inline-flex;
9
- justify-content: space-between;
10
- align-items: center;
11
- width: 100%;
12
- }
13
-
14
- .editor-post-format__suggestion {
15
- padding: $grid-unit-15 * 0.5;
16
- text-align: right;
17
- font-size: $default-font-size;
1
+ [class].editor-post-format__suggestion {
2
+ margin: $grid-unit-05 0 0 0;
18
3
  }
@@ -197,7 +197,7 @@ export default function PostLockedModal() {
197
197
  ? sprintf(
198
198
  /* translators: %s: user's display name */
199
199
  __(
200
- '<strong>%s</strong> now has editing control of this posts (<PreviewLink />). Don’t worry, your changes up to this moment have been saved.'
200
+ '<strong>%s</strong> now has editing control of this post (<PreviewLink />). Don’t worry, your changes up to this moment have been saved.'
201
201
  ),
202
202
  userDisplayName
203
203
  )
@@ -23,14 +23,15 @@ function MaybeCategoryPanel() {
23
23
  const postType = select( editorStore ).getCurrentPostType();
24
24
  const categoriesTaxonomy =
25
25
  select( coreStore ).getTaxonomy( 'category' );
26
- const defaultCategorySlug = 'uncategorized';
27
- const defaultCategory = select( coreStore ).getEntityRecords(
26
+ const defaultCategoryId = select( coreStore ).getEntityRecord(
27
+ 'root',
28
+ 'site'
29
+ )?.default_category;
30
+ const defaultCategory = select( coreStore ).getEntityRecord(
28
31
  'taxonomy',
29
32
  'category',
30
- {
31
- slug: defaultCategorySlug,
32
- }
33
- )?.[ 0 ];
33
+ defaultCategoryId
34
+ );
34
35
  const postTypeSupportsCategories =
35
36
  categoriesTaxonomy &&
36
37
  some( categoriesTaxonomy.types, ( type ) => type === postType );
@@ -184,7 +184,7 @@ exports[`PostPublishPanel should render the spinner if the post is being saved 1
184
184
  <div
185
185
  className="editor-post-publish-panel__content"
186
186
  >
187
- <Spinner />
187
+ <ForwardRef(UnforwardedSpinner) />
188
188
  </div>
189
189
  <div
190
190
  className="editor-post-publish-panel__footer"
@@ -1,28 +1,122 @@
1
1
  /**
2
2
  * WordPress dependencies
3
3
  */
4
- import { __ } from '@wordpress/i18n';
5
- import { format, __experimentalGetSettings } from '@wordpress/date';
6
- import { withSelect } from '@wordpress/data';
4
+ import { __, _x, sprintf, isRTL } from '@wordpress/i18n';
5
+ import { __experimentalGetSettings, getDate, dateI18n } from '@wordpress/date';
6
+ import { useSelect } from '@wordpress/data';
7
7
 
8
8
  /**
9
9
  * Internal dependencies
10
10
  */
11
11
  import { store as editorStore } from '../../store';
12
12
 
13
- export function PostScheduleLabel( { date, isFloating } ) {
14
- const settings = __experimentalGetSettings();
15
- return date && ! isFloating
16
- ? format(
17
- `${ settings.formats.date } ${ settings.formats.time }`,
18
- date
19
- )
20
- : __( 'Immediately' );
13
+ export default function PostScheduleLabel( props ) {
14
+ return usePostScheduleLabel( props );
21
15
  }
22
16
 
23
- export default withSelect( ( select ) => {
24
- return {
25
- date: select( editorStore ).getEditedPostAttribute( 'date' ),
26
- isFloating: select( editorStore ).isEditedPostDateFloating(),
27
- };
28
- } )( PostScheduleLabel );
17
+ export function usePostScheduleLabel( { full = false } = {} ) {
18
+ const { date, isFloating } = useSelect(
19
+ ( select ) => ( {
20
+ date: select( editorStore ).getEditedPostAttribute( 'date' ),
21
+ isFloating: select( editorStore ).isEditedPostDateFloating(),
22
+ } ),
23
+ []
24
+ );
25
+
26
+ return full
27
+ ? getFullPostScheduleLabel( date )
28
+ : getPostScheduleLabel( date, { isFloating } );
29
+ }
30
+
31
+ export function getFullPostScheduleLabel( dateAttribute ) {
32
+ const date = getDate( dateAttribute );
33
+
34
+ const timezoneAbbreviation = getTimezoneAbbreviation();
35
+ const formattedDate = dateI18n(
36
+ // translators: If using a space between 'g:i' and 'a', use a non-breaking sapce.
37
+ _x( 'F j, Y g:i\xa0a', 'post schedule full date format' ),
38
+ date
39
+ );
40
+ return isRTL()
41
+ ? `${ timezoneAbbreviation } ${ formattedDate }`
42
+ : `${ formattedDate } ${ timezoneAbbreviation }`;
43
+ }
44
+
45
+ export function getPostScheduleLabel(
46
+ dateAttribute,
47
+ { isFloating = false, now = new Date() } = {}
48
+ ) {
49
+ if ( ! dateAttribute || isFloating ) {
50
+ return __( 'Immediately' );
51
+ }
52
+
53
+ // If the user timezone does not equal the site timezone then using words
54
+ // like 'tomorrow' is confusing, so show the full date.
55
+ if ( ! isTimezoneSameAsSiteTimezone( now ) ) {
56
+ return getFullPostScheduleLabel( dateAttribute );
57
+ }
58
+
59
+ const date = getDate( dateAttribute );
60
+
61
+ if ( isSameDay( date, now ) ) {
62
+ return sprintf(
63
+ // translators: %s: Time of day the post is scheduled for.
64
+ __( 'Today at %s' ),
65
+ // translators: If using a space between 'g:i' and 'a', use a non-breaking sapce.
66
+ dateI18n( _x( 'g:i\xa0a', 'post schedule time format' ), date )
67
+ );
68
+ }
69
+
70
+ const tomorrow = new Date( now );
71
+ tomorrow.setDate( tomorrow.getDate() + 1 );
72
+
73
+ if ( isSameDay( date, tomorrow ) ) {
74
+ return sprintf(
75
+ // translators: %s: Time of day the post is scheduled for.
76
+ __( 'Tomorrow at %s' ),
77
+ // translators: If using a space between 'g:i' and 'a', use a non-breaking sapce.
78
+ dateI18n( _x( 'g:i\xa0a', 'post schedule time format' ), date )
79
+ );
80
+ }
81
+
82
+ if ( date.getFullYear() === now.getFullYear() ) {
83
+ return dateI18n(
84
+ // translators: If using a space between 'g:i' and 'a', use a non-breaking sapce.
85
+ _x( 'F j g:i\xa0a', 'post schedule date format without year' ),
86
+ date
87
+ );
88
+ }
89
+
90
+ return dateI18n(
91
+ // translators: Use a non-breaking space between 'g:i' and 'a' if appropriate.
92
+ _x( 'F j, Y g:i\xa0a', 'post schedule full date format' ),
93
+ date
94
+ );
95
+ }
96
+
97
+ function getTimezoneAbbreviation() {
98
+ const { timezone } = __experimentalGetSettings();
99
+
100
+ if ( timezone.abbr && isNaN( Number( timezone.abbr ) ) ) {
101
+ return timezone.abbr;
102
+ }
103
+
104
+ const symbol = timezone.offset < 0 ? '' : '+';
105
+ return `UTC${ symbol }${ timezone.offset }`;
106
+ }
107
+
108
+ function isTimezoneSameAsSiteTimezone( date ) {
109
+ const { timezone } = __experimentalGetSettings();
110
+
111
+ const siteOffset = Number( timezone.offset );
112
+ const dateOffset = -1 * ( date.getTimezoneOffset() / 60 );
113
+ return siteOffset === dateOffset;
114
+ }
115
+
116
+ function isSameDay( left, right ) {
117
+ return (
118
+ left.getDate() === right.getDate() &&
119
+ left.getMonth() === right.getMonth() &&
120
+ left.getFullYear() === right.getFullYear()
121
+ );
122
+ }
@@ -1,32 +1,144 @@
1
1
  /**
2
- * External dependencies
2
+ * WordPress dependencies
3
3
  */
4
- import { shallow } from 'enzyme';
4
+ import { __experimentalGetSettings, setSettings } from '@wordpress/date';
5
5
 
6
6
  /**
7
7
  * Internal dependencies
8
8
  */
9
- import { PostScheduleLabel } from '../label';
9
+ import { getFullPostScheduleLabel, getPostScheduleLabel } from '../label';
10
10
 
11
- describe( 'PostScheduleLabel', () => {
11
+ describe( 'getFullPostScheduleLabel', () => {
12
+ it( 'should show a date', () => {
13
+ const label = getFullPostScheduleLabel( '2022-04-28T15:30:00' );
14
+ expect( label ).toBe( 'April 28, 2022 3:30\xa0pm UTC+0' ); // Unused, for backwards compatibility.
15
+ } );
16
+
17
+ it( "should show site's timezone abbr", () => {
18
+ const settings = __experimentalGetSettings();
19
+
20
+ setSettings( {
21
+ ...settings,
22
+ timezone: { offset: 10, string: 'Australia/Sydney', abbr: 'AEST' },
23
+ } );
24
+
25
+ const label = getFullPostScheduleLabel( '2022-04-28T15:30:00' );
26
+ expect( label ).toBe( 'April 28, 2022 3:30\xa0pm AEST' );
27
+
28
+ setSettings( settings );
29
+ } );
30
+
31
+ it( "should show site's timezone offset", () => {
32
+ const settings = __experimentalGetSettings();
33
+
34
+ setSettings( {
35
+ ...settings,
36
+ timezone: { offset: 10 },
37
+ } );
38
+
39
+ const label = getFullPostScheduleLabel( '2022-04-28T15:30:00' );
40
+ expect( label ).toBe( 'April 28, 2022 3:30\xa0pm UTC+10' );
41
+
42
+ setSettings( settings );
43
+ } );
44
+ } );
45
+
46
+ describe( 'getPostScheduleLabel', () => {
12
47
  it( 'should show the post will be published immediately if no publish date is set', () => {
13
- const wrapper = shallow( <PostScheduleLabel date={ undefined } /> );
14
- expect( wrapper.text() ).toBe( 'Immediately' );
48
+ const label = getPostScheduleLabel( undefined );
49
+ expect( label ).toBe( 'Immediately' );
15
50
  } );
16
51
 
17
52
  it( 'should show the post will be published immediately if it has a floating date', () => {
18
- const date = '2018-09-17T01:23:45.678Z';
19
- const wrapper = shallow(
20
- <PostScheduleLabel date={ date } isFloating={ true } />
53
+ const label = getPostScheduleLabel( '2022-04-28T15:30:00', {
54
+ isFloating: true,
55
+ } );
56
+ expect( label ).toBe( 'Immediately' );
57
+ } );
58
+
59
+ it( "should show full date if user timezone does not equal site's timezone", () => {
60
+ const now = new Date( '2022-04-28T13:00:00.000Z' );
61
+ jest.spyOn( now, 'getTimezoneOffset' ).mockImplementationOnce(
62
+ () => 10 * -60 // UTC+10
21
63
  );
22
- expect( wrapper.text() ).toBe( 'Immediately' );
64
+
65
+ const label = getPostScheduleLabel( '2022-04-28T15:30:00', { now } );
66
+ expect( label ).toBe( 'April 28, 2022 3:30\xa0pm UTC+0' );
23
67
  } );
24
68
 
25
- it( 'should show the scheduled publish date if a date has been set', () => {
26
- const date = '2018-09-17T01:23:45.678Z';
27
- const wrapper = shallow(
28
- <PostScheduleLabel date={ date } isFloating={ false } />
69
+ it( "should show today if date is same day as now and user timezone equals site's timezone", () => {
70
+ const settings = __experimentalGetSettings();
71
+
72
+ setSettings( {
73
+ ...settings,
74
+ timezone: { offset: 10 },
75
+ } );
76
+
77
+ const now = new Date( '2022-04-28T03:00:00.000Z' );
78
+ jest.spyOn( now, 'getTimezoneOffset' ).mockImplementationOnce(
79
+ () => 10 * -60 // UTC+10
29
80
  );
30
- expect( wrapper.text() ).not.toBe( 'Immediately' );
81
+
82
+ const label = getPostScheduleLabel( '2022-04-28T15:30:00', { now } );
83
+ expect( label ).toBe( 'Today at 3:30\xa0pm' );
84
+
85
+ setSettings( settings );
86
+ } );
87
+
88
+ it( "should show tomorrow if date is same day as now + 1 day and user timezone equals site's timezone", () => {
89
+ const settings = __experimentalGetSettings();
90
+
91
+ setSettings( {
92
+ ...settings,
93
+ timezone: { offset: 10 },
94
+ } );
95
+
96
+ const now = new Date( '2022-04-28T03:00:00.000Z' );
97
+ jest.spyOn( now, 'getTimezoneOffset' ).mockImplementationOnce(
98
+ () => 10 * -60 // UTC+10
99
+ );
100
+
101
+ const label = getPostScheduleLabel( '2022-04-29T15:30:00', { now } );
102
+ expect( label ).toBe( 'Tomorrow at 3:30\xa0pm' );
103
+
104
+ setSettings( settings );
105
+ } );
106
+
107
+ it( "should hide year if date is same year as now and user timezone equals site's timezone", () => {
108
+ const settings = __experimentalGetSettings();
109
+
110
+ setSettings( {
111
+ ...settings,
112
+ timezone: { offset: 10 },
113
+ } );
114
+
115
+ const now = new Date( '2022-04-28T03:00:00.000Z' );
116
+ jest.spyOn( now, 'getTimezoneOffset' ).mockImplementationOnce(
117
+ () => 10 * -60 // UTC+10
118
+ );
119
+
120
+ const label = getPostScheduleLabel( '2022-12-25T15:30:00', { now } );
121
+ expect( label ).toBe( 'December 25 3:30\xa0pm' );
122
+
123
+ setSettings( settings );
124
+ } );
125
+
126
+ it( "should show year if date is not same year as now and user timezone equals site's timezone", () => {
127
+ const settings = __experimentalGetSettings();
128
+
129
+ setSettings( {
130
+ ...settings,
131
+ timezone: { offset: 10 },
132
+ } );
133
+
134
+ const now = new Date( '2022-04-28T03:00:00.000Z' );
135
+ jest.spyOn( now, 'getTimezoneOffset' ).mockImplementationOnce(
136
+ () => 10 * -60 // UTC+10
137
+ );
138
+
139
+ const label = getPostScheduleLabel( '2023-04-28T15:30:00', { now } );
140
+ expect( label ).toBe( 'April 28, 2023 3:30\xa0pm' );
141
+
142
+ setSettings( settings );
31
143
  } );
32
144
  } );