@wordpress/editor 14.41.0 → 14.41.1-next.v.202603102151.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.
- package/build/components/collab-sidebar/index.cjs +7 -4
- package/build/components/collab-sidebar/index.cjs.map +2 -2
- package/build/components/collab-sidebar/utils.cjs +13 -15
- package/build/components/collab-sidebar/utils.cjs.map +2 -2
- package/build/components/collaborators-overlay/avatar-iframe-styles.cjs +133 -0
- package/build/components/collaborators-overlay/avatar-iframe-styles.cjs.map +7 -0
- package/build/components/collaborators-overlay/collaborator-styles.cjs +38 -2
- package/build/components/collaborators-overlay/collaborator-styles.cjs.map +2 -2
- package/build/components/collaborators-overlay/overlay-iframe-styles.cjs +142 -0
- package/build/components/collaborators-overlay/overlay-iframe-styles.cjs.map +7 -0
- package/build/components/collaborators-overlay/overlay.cjs +59 -201
- package/build/components/collaborators-overlay/overlay.cjs.map +3 -3
- package/build/components/collaborators-overlay/use-block-highlighting.cjs +91 -42
- package/build/components/collaborators-overlay/use-block-highlighting.cjs.map +2 -2
- package/build/components/collaborators-overlay/use-debounced-recompute.cjs +49 -0
- package/build/components/collaborators-overlay/use-debounced-recompute.cjs.map +7 -0
- package/build/components/collaborators-overlay/use-render-cursors.cjs +49 -50
- package/build/components/collaborators-overlay/use-render-cursors.cjs.map +2 -2
- package/build/components/collaborators-presence/avatar/component.cjs +121 -0
- package/build/components/collaborators-presence/avatar/component.cjs.map +7 -0
- package/build/components/collaborators-presence/avatar/index.cjs +37 -0
- package/build/components/collaborators-presence/avatar/index.cjs.map +7 -0
- package/build/components/collaborators-presence/avatar/types.cjs +19 -0
- package/build/components/collaborators-presence/avatar/types.cjs.map +7 -0
- package/build/components/collaborators-presence/avatar/use-image-loading-status.cjs +44 -0
- package/build/components/collaborators-presence/avatar/use-image-loading-status.cjs.map +7 -0
- package/build/components/collaborators-presence/avatar-group/component.cjs +78 -0
- package/build/components/collaborators-presence/avatar-group/component.cjs.map +7 -0
- package/build/components/collaborators-presence/avatar-group/index.cjs +37 -0
- package/build/components/collaborators-presence/avatar-group/index.cjs.map +7 -0
- package/build/components/collaborators-presence/avatar-group/types.cjs +19 -0
- package/build/components/collaborators-presence/avatar-group/types.cjs.map +7 -0
- package/build/components/collaborators-presence/index.cjs +17 -6
- package/build/components/collaborators-presence/index.cjs.map +3 -3
- package/build/components/collaborators-presence/list.cjs +20 -17
- package/build/components/collaborators-presence/list.cjs.map +3 -3
- package/build/components/entities-saved-states/hooks/use-is-dirty.cjs +14 -5
- package/build/components/entities-saved-states/hooks/use-is-dirty.cjs.map +2 -2
- package/build/components/global-styles/index.cjs +15 -24
- package/build/components/global-styles/index.cjs.map +3 -3
- package/build/components/global-styles-sidebar/index.cjs +6 -3
- package/build/components/global-styles-sidebar/index.cjs.map +2 -2
- package/build/components/page-attributes/parent.cjs +1 -0
- package/build/components/page-attributes/parent.cjs.map +2 -2
- package/build/components/post-revisions-preview/revisions-canvas.cjs +17 -4
- package/build/components/post-revisions-preview/revisions-canvas.cjs.map +2 -2
- package/build/components/post-url/panel.cjs +1 -0
- package/build/components/post-url/panel.cjs.map +2 -2
- package/build/components/provider/use-block-editor-settings.cjs +4 -1
- package/build/components/provider/use-block-editor-settings.cjs.map +3 -3
- package/build/components/sidebar/dataform-post-summary.cjs +167 -0
- package/build/components/sidebar/dataform-post-summary.cjs.map +7 -0
- package/build/components/sidebar/post-summary.cjs +11 -0
- package/build/components/sidebar/post-summary.cjs.map +3 -3
- package/build/components/visual-editor/index.cjs +1 -1
- package/build/components/visual-editor/index.cjs.map +2 -2
- package/build/dataviews/store/private-actions.cjs +4 -0
- package/build/dataviews/store/private-actions.cjs.map +2 -2
- package/build/utils/media-upload/on-success.cjs +46 -0
- package/build/utils/media-upload/on-success.cjs.map +7 -0
- package/build-module/components/collab-sidebar/index.mjs +7 -4
- package/build-module/components/collab-sidebar/index.mjs.map +2 -2
- package/build-module/components/collab-sidebar/utils.mjs +13 -15
- package/build-module/components/collab-sidebar/utils.mjs.map +2 -2
- package/build-module/components/collaborators-overlay/avatar-iframe-styles.mjs +120 -0
- package/build-module/components/collaborators-overlay/avatar-iframe-styles.mjs.map +7 -0
- package/build-module/components/collaborators-overlay/collaborator-styles.mjs +25 -1
- package/build-module/components/collaborators-overlay/collaborator-styles.mjs.map +2 -2
- package/build-module/components/collaborators-overlay/overlay-iframe-styles.mjs +124 -0
- package/build-module/components/collaborators-overlay/overlay-iframe-styles.mjs.map +7 -0
- package/build-module/components/collaborators-overlay/overlay.mjs +49 -201
- package/build-module/components/collaborators-overlay/overlay.mjs.map +2 -2
- package/build-module/components/collaborators-overlay/use-block-highlighting.mjs +92 -43
- package/build-module/components/collaborators-overlay/use-block-highlighting.mjs.map +2 -2
- package/build-module/components/collaborators-overlay/use-debounced-recompute.mjs +24 -0
- package/build-module/components/collaborators-overlay/use-debounced-recompute.mjs.map +7 -0
- package/build-module/components/collaborators-overlay/use-render-cursors.mjs +50 -51
- package/build-module/components/collaborators-overlay/use-render-cursors.mjs.map +2 -2
- package/build-module/components/collaborators-presence/avatar/component.mjs +90 -0
- package/build-module/components/collaborators-presence/avatar/component.mjs.map +7 -0
- package/build-module/components/collaborators-presence/avatar/index.mjs +6 -0
- package/build-module/components/collaborators-presence/avatar/index.mjs.map +7 -0
- package/build-module/components/collaborators-presence/avatar/types.mjs +1 -0
- package/build-module/components/collaborators-presence/avatar/types.mjs.map +7 -0
- package/build-module/components/collaborators-presence/avatar/use-image-loading-status.mjs +19 -0
- package/build-module/components/collaborators-presence/avatar/use-image-loading-status.mjs.map +7 -0
- package/build-module/components/collaborators-presence/avatar-group/component.mjs +47 -0
- package/build-module/components/collaborators-presence/avatar-group/component.mjs.map +7 -0
- package/build-module/components/collaborators-presence/avatar-group/index.mjs +6 -0
- package/build-module/components/collaborators-presence/avatar-group/index.mjs.map +7 -0
- package/build-module/components/collaborators-presence/avatar-group/types.mjs +1 -0
- package/build-module/components/collaborators-presence/avatar-group/types.mjs.map +7 -0
- package/build-module/components/collaborators-presence/index.mjs +7 -9
- package/build-module/components/collaborators-presence/index.mjs.map +2 -2
- package/build-module/components/collaborators-presence/list.mjs +11 -22
- package/build-module/components/collaborators-presence/list.mjs.map +2 -2
- package/build-module/components/entities-saved-states/hooks/use-is-dirty.mjs +14 -5
- package/build-module/components/entities-saved-states/hooks/use-is-dirty.mjs.map +2 -2
- package/build-module/components/global-styles/index.mjs +15 -24
- package/build-module/components/global-styles/index.mjs.map +2 -2
- package/build-module/components/global-styles-sidebar/index.mjs +6 -3
- package/build-module/components/global-styles-sidebar/index.mjs.map +2 -2
- package/build-module/components/page-attributes/parent.mjs +1 -0
- package/build-module/components/page-attributes/parent.mjs.map +2 -2
- package/build-module/components/post-revisions-preview/revisions-canvas.mjs +17 -4
- package/build-module/components/post-revisions-preview/revisions-canvas.mjs.map +2 -2
- package/build-module/components/post-url/panel.mjs +1 -0
- package/build-module/components/post-url/panel.mjs.map +2 -2
- package/build-module/components/provider/use-block-editor-settings.mjs +4 -1
- package/build-module/components/provider/use-block-editor-settings.mjs.map +2 -2
- package/build-module/components/sidebar/dataform-post-summary.mjs +136 -0
- package/build-module/components/sidebar/dataform-post-summary.mjs.map +7 -0
- package/build-module/components/sidebar/post-summary.mjs +11 -0
- package/build-module/components/sidebar/post-summary.mjs.map +2 -2
- package/build-module/components/visual-editor/index.mjs +1 -1
- package/build-module/components/visual-editor/index.mjs.map +2 -2
- package/build-module/dataviews/store/private-actions.mjs +8 -1
- package/build-module/dataviews/store/private-actions.mjs.map +2 -2
- package/build-module/utils/media-upload/on-success.mjs +25 -0
- package/build-module/utils/media-upload/on-success.mjs.map +7 -0
- package/build-style/style-rtl.css +876 -137
- package/build-style/style.css +876 -137
- package/build-types/components/collab-sidebar/index.d.ts.map +1 -1
- package/build-types/components/collab-sidebar/utils.d.ts.map +1 -1
- package/build-types/components/collaborators-overlay/avatar-iframe-styles.d.ts +11 -0
- package/build-types/components/collaborators-overlay/avatar-iframe-styles.d.ts.map +1 -0
- package/build-types/components/collaborators-overlay/collaborator-styles.d.ts +17 -2
- package/build-types/components/collaborators-overlay/collaborator-styles.d.ts.map +1 -1
- package/build-types/components/collaborators-overlay/overlay-iframe-styles.d.ts +6 -0
- package/build-types/components/collaborators-overlay/overlay-iframe-styles.d.ts.map +1 -0
- package/build-types/components/collaborators-overlay/overlay.d.ts.map +1 -1
- package/build-types/components/collaborators-overlay/use-block-highlighting.d.ts +21 -5
- package/build-types/components/collaborators-overlay/use-block-highlighting.d.ts.map +1 -1
- package/build-types/components/collaborators-overlay/use-debounced-recompute.d.ts +10 -0
- package/build-types/components/collaborators-overlay/use-debounced-recompute.d.ts.map +1 -0
- package/build-types/components/collaborators-overlay/use-render-cursors.d.ts +2 -1
- package/build-types/components/collaborators-overlay/use-render-cursors.d.ts.map +1 -1
- package/build-types/components/collaborators-presence/avatar/component.d.ts +7 -0
- package/build-types/components/collaborators-presence/avatar/component.d.ts.map +1 -0
- package/build-types/components/collaborators-presence/avatar/index.d.ts +3 -0
- package/build-types/components/collaborators-presence/avatar/index.d.ts.map +1 -0
- package/build-types/components/collaborators-presence/avatar/types.d.ts +66 -0
- package/build-types/components/collaborators-presence/avatar/types.d.ts.map +1 -0
- package/build-types/components/collaborators-presence/avatar/use-image-loading-status.d.ts +17 -0
- package/build-types/components/collaborators-presence/avatar/use-image-loading-status.d.ts.map +1 -0
- package/build-types/components/collaborators-presence/avatar-group/component.d.ts +7 -0
- package/build-types/components/collaborators-presence/avatar-group/component.d.ts.map +1 -0
- package/build-types/components/collaborators-presence/avatar-group/index.d.ts +3 -0
- package/build-types/components/collaborators-presence/avatar-group/index.d.ts.map +1 -0
- package/build-types/components/collaborators-presence/avatar-group/types.d.ts +14 -0
- package/build-types/components/collaborators-presence/avatar-group/types.d.ts.map +1 -0
- package/build-types/components/collaborators-presence/index.d.ts.map +1 -1
- package/build-types/components/collaborators-presence/list.d.ts.map +1 -1
- package/build-types/components/entities-saved-states/hooks/use-is-dirty.d.ts.map +1 -1
- package/build-types/components/global-styles/index.d.ts +2 -1
- package/build-types/components/global-styles/index.d.ts.map +1 -1
- package/build-types/components/global-styles-sidebar/index.d.ts.map +1 -1
- package/build-types/components/page-attributes/parent.d.ts.map +1 -1
- package/build-types/components/post-author/hook.d.ts +1 -1
- package/build-types/components/post-revisions-preview/revisions-canvas.d.ts.map +1 -1
- package/build-types/components/provider/use-block-editor-settings.d.ts.map +1 -1
- package/build-types/components/sidebar/dataform-post-summary.d.ts +4 -0
- package/build-types/components/sidebar/dataform-post-summary.d.ts.map +1 -0
- package/build-types/components/sidebar/post-summary.d.ts.map +1 -1
- package/build-types/dataviews/store/private-actions.d.ts.map +1 -1
- package/build-types/utils/media-upload/on-success.d.ts +9 -0
- package/build-types/utils/media-upload/on-success.d.ts.map +1 -0
- package/package.json +45 -44
- package/src/components/collab-sidebar/index.js +7 -4
- package/src/components/collab-sidebar/utils.js +9 -10
- package/src/components/collaborators-overlay/avatar-iframe-styles.ts +126 -0
- package/src/components/collaborators-overlay/collaborator-styles.ts +43 -2
- package/src/components/collaborators-overlay/overlay-iframe-styles.ts +125 -0
- package/src/components/collaborators-overlay/overlay.tsx +54 -207
- package/src/components/collaborators-overlay/use-block-highlighting.ts +147 -64
- package/src/components/collaborators-overlay/use-debounced-recompute.ts +32 -0
- package/src/components/collaborators-overlay/use-render-cursors.ts +72 -66
- package/src/components/collaborators-presence/avatar/component.tsx +123 -0
- package/src/components/collaborators-presence/avatar/index.ts +2 -0
- package/src/components/collaborators-presence/avatar/styles.scss +168 -0
- package/src/components/collaborators-presence/avatar/test/index.tsx +389 -0
- package/src/components/collaborators-presence/avatar/types.ts +66 -0
- package/src/components/collaborators-presence/avatar/use-image-loading-status.ts +36 -0
- package/src/components/collaborators-presence/avatar-group/component.tsx +55 -0
- package/src/components/collaborators-presence/avatar-group/index.ts +2 -0
- package/src/components/collaborators-presence/avatar-group/styles.scss +33 -0
- package/src/components/collaborators-presence/avatar-group/test/index.tsx +139 -0
- package/src/components/collaborators-presence/avatar-group/types.ts +13 -0
- package/src/components/collaborators-presence/index.tsx +4 -6
- package/src/components/collaborators-presence/list.tsx +7 -17
- package/src/components/collaborators-presence/styles/collaborators-list.scss +26 -19
- package/src/components/collaborators-presence/styles/collaborators-presence.scss +6 -2
- package/src/components/entities-saved-states/hooks/use-is-dirty.js +14 -5
- package/src/components/global-styles/index.js +20 -27
- package/src/components/global-styles-sidebar/index.js +3 -0
- package/src/components/page-attributes/parent.js +1 -0
- package/src/components/post-publish-panel/test/__snapshots__/index.js.snap +2 -2
- package/src/components/post-revisions-preview/revisions-canvas.js +15 -6
- package/src/components/post-url/panel.js +1 -0
- package/src/components/post-url/style.scss +5 -0
- package/src/components/provider/use-block-editor-settings.js +5 -0
- package/src/components/sidebar/dataform-post-summary.js +149 -0
- package/src/components/sidebar/post-summary.js +15 -0
- package/src/components/visual-editor/index.js +1 -1
- package/src/dataviews/store/private-actions.ts +14 -0
- package/src/style.scss +3 -0
- package/src/utils/media-upload/on-success.js +34 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
@use "@wordpress/base-styles/colors" as *;
|
|
2
|
+
@use "@wordpress/base-styles/variables" as *;
|
|
3
|
+
|
|
4
|
+
// Accent color for the avatar. Uses the admin theme color with a hardcoded
|
|
5
|
+
// fallback matching the default WordPress admin blue.
|
|
6
|
+
$-accent-color: var(--wp-admin-theme-color, #3858e9);
|
|
7
|
+
|
|
8
|
+
.editor-avatar {
|
|
9
|
+
position: relative;
|
|
10
|
+
display: inline-flex;
|
|
11
|
+
align-items: center;
|
|
12
|
+
border-radius: $radius-full;
|
|
13
|
+
overflow: hidden;
|
|
14
|
+
overflow: clip;
|
|
15
|
+
flex-shrink: 0;
|
|
16
|
+
box-shadow: 0 0 0 var(--wp-admin-border-width-focus) $white, $elevation-x-small;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.editor-avatar__image {
|
|
20
|
+
box-sizing: border-box;
|
|
21
|
+
position: relative;
|
|
22
|
+
width: $button-size-compact;
|
|
23
|
+
height: $button-size-compact;
|
|
24
|
+
border-radius: $radius-full;
|
|
25
|
+
border: 0;
|
|
26
|
+
background-color: $-accent-color;
|
|
27
|
+
overflow: hidden;
|
|
28
|
+
overflow: clip;
|
|
29
|
+
flex-shrink: 0;
|
|
30
|
+
font-size: 0;
|
|
31
|
+
color: $white;
|
|
32
|
+
|
|
33
|
+
.is-small > & {
|
|
34
|
+
width: $button-size-small;
|
|
35
|
+
height: $button-size-small;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.has-avatar-border-color > & {
|
|
39
|
+
border: var(--wp-admin-border-width-focus) solid var(--editor-avatar-outline-color);
|
|
40
|
+
box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) $white;
|
|
41
|
+
background-clip: padding-box;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// The <img> element, absolutely positioned to fill the __image container.
|
|
46
|
+
// Starts invisible; becomes visible when the image loads (has-src class).
|
|
47
|
+
.editor-avatar__img {
|
|
48
|
+
position: absolute;
|
|
49
|
+
inset: 0;
|
|
50
|
+
width: 100%;
|
|
51
|
+
height: 100%;
|
|
52
|
+
object-fit: cover;
|
|
53
|
+
border-radius: inherit;
|
|
54
|
+
opacity: 0;
|
|
55
|
+
|
|
56
|
+
.has-src > .editor-avatar__image > & {
|
|
57
|
+
opacity: 1;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Initials fallback: show name characters when no image has loaded.
|
|
62
|
+
.editor-avatar:not(.has-src) > .editor-avatar__image {
|
|
63
|
+
display: flex;
|
|
64
|
+
align-items: center;
|
|
65
|
+
justify-content: center;
|
|
66
|
+
font-size: $font-size-x-small;
|
|
67
|
+
font-weight: $font-weight-medium;
|
|
68
|
+
border: 0;
|
|
69
|
+
box-shadow: none;
|
|
70
|
+
background-clip: border-box;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.editor-avatar:not(.has-src).has-avatar-border-color > .editor-avatar__image {
|
|
74
|
+
background-color: var(--editor-avatar-outline-color);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.editor-avatar__name {
|
|
78
|
+
font-size: $font-size-medium;
|
|
79
|
+
font-weight: $font-weight-medium;
|
|
80
|
+
line-height: $font-line-height-small;
|
|
81
|
+
color: var(--editor-avatar-name-color, $white);
|
|
82
|
+
min-width: 0;
|
|
83
|
+
padding-bottom: calc($grid-unit-05 / 2);
|
|
84
|
+
overflow: hidden;
|
|
85
|
+
opacity: 0;
|
|
86
|
+
white-space: nowrap;
|
|
87
|
+
transition: opacity 0.15s cubic-bezier(0.15, 0, 0.15, 1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Badge mode: use grid so the name column animates from 0 to natural width.
|
|
91
|
+
// Spacing is on the container (column-gap + padding-inline-end) so it
|
|
92
|
+
// transitions alongside grid-template-columns instead of causing a bump.
|
|
93
|
+
.editor-avatar.is-badge {
|
|
94
|
+
display: inline-grid;
|
|
95
|
+
grid-template-columns: min-content 0fr;
|
|
96
|
+
column-gap: 0;
|
|
97
|
+
padding-inline-end: 0;
|
|
98
|
+
background-color: $-accent-color;
|
|
99
|
+
transition:
|
|
100
|
+
grid-template-columns 0.3s cubic-bezier(0.15, 0, 0.15, 1),
|
|
101
|
+
column-gap 0.3s cubic-bezier(0.15, 0, 0.15, 1),
|
|
102
|
+
padding-inline-end 0.3s cubic-bezier(0.15, 0, 0.15, 1);
|
|
103
|
+
|
|
104
|
+
&:hover {
|
|
105
|
+
grid-template-columns: min-content 1fr;
|
|
106
|
+
column-gap: $grid-unit-05;
|
|
107
|
+
padding-inline-end: $grid-unit-10;
|
|
108
|
+
transition-timing-function: cubic-bezier(0.85, 0, 0.85, 1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
&:hover .editor-avatar__name {
|
|
112
|
+
opacity: 1;
|
|
113
|
+
transition-timing-function: cubic-bezier(0.85, 0, 0.85, 1);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.editor-avatar.is-badge.has-avatar-border-color {
|
|
118
|
+
background-color: var(--editor-avatar-outline-color);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Dimmed: neutral gray background at 50% opacity. The status indicator is a
|
|
122
|
+
// sibling (outside __image) so it stays at full opacity.
|
|
123
|
+
.editor-avatar.is-dimmed > .editor-avatar__image {
|
|
124
|
+
opacity: 0.5;
|
|
125
|
+
background-color: $gray-700;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.editor-avatar.is-dimmed.has-avatar-border-color > .editor-avatar__image {
|
|
129
|
+
border-color: $gray-700;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Dimmed images: desaturate via CSS filter instead of mix-blend-mode.
|
|
133
|
+
.editor-avatar.is-dimmed .editor-avatar__img {
|
|
134
|
+
filter: grayscale(1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Status indicator: positioned over the image area, outside __image so it
|
|
138
|
+
// is unaffected by the dimmed opacity.
|
|
139
|
+
.editor-avatar__status-indicator {
|
|
140
|
+
position: absolute;
|
|
141
|
+
top: 0;
|
|
142
|
+
left: 0;
|
|
143
|
+
width: $button-size-compact;
|
|
144
|
+
height: $button-size-compact;
|
|
145
|
+
display: flex;
|
|
146
|
+
align-items: center;
|
|
147
|
+
justify-content: center;
|
|
148
|
+
z-index: 1;
|
|
149
|
+
color: $gray-900;
|
|
150
|
+
fill: $gray-900;
|
|
151
|
+
|
|
152
|
+
.is-small > & {
|
|
153
|
+
width: $button-size-small;
|
|
154
|
+
height: $button-size-small;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
svg {
|
|
158
|
+
width: 75%;
|
|
159
|
+
height: 75%;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
@media (prefers-reduced-motion: reduce) {
|
|
164
|
+
.editor-avatar.is-badge,
|
|
165
|
+
.editor-avatar__name {
|
|
166
|
+
transition: none;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Internal dependencies
|
|
8
|
+
*/
|
|
9
|
+
import Avatar from '..';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* In JSDOM, `<img>` elements never fire `load` or `error` events on their
|
|
13
|
+
* own. We simulate them using `fireEvent` on the `<img>` element, which we
|
|
14
|
+
* locate via `getByAltText('')` (the `<img>` has `alt=""`).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
describe( 'Avatar', () => {
|
|
18
|
+
it( 'should render with default props', () => {
|
|
19
|
+
render( <Avatar data-testid="avatar" /> );
|
|
20
|
+
const avatar = screen.getByTestId( 'avatar' );
|
|
21
|
+
expect( avatar ).toBeInTheDocument();
|
|
22
|
+
expect( avatar.tagName ).toBe( 'DIV' );
|
|
23
|
+
expect( avatar ).toHaveClass( 'editor-avatar' );
|
|
24
|
+
} );
|
|
25
|
+
|
|
26
|
+
it( 'should set the accessible name from the name prop', () => {
|
|
27
|
+
render( <Avatar name="Jane Doe" /> );
|
|
28
|
+
const avatar = screen.getByRole( 'img', { name: 'Jane Doe' } );
|
|
29
|
+
expect( avatar ).toBeInTheDocument();
|
|
30
|
+
} );
|
|
31
|
+
|
|
32
|
+
it( 'should not set role or aria-label without a name', () => {
|
|
33
|
+
render( <Avatar data-testid="avatar" /> );
|
|
34
|
+
const avatar = screen.getByTestId( 'avatar' );
|
|
35
|
+
expect( avatar ).not.toHaveAttribute( 'role' );
|
|
36
|
+
expect( avatar ).not.toHaveAttribute( 'aria-label' );
|
|
37
|
+
} );
|
|
38
|
+
|
|
39
|
+
it( 'should render an img element when src is provided', () => {
|
|
40
|
+
render(
|
|
41
|
+
<Avatar
|
|
42
|
+
data-testid="avatar"
|
|
43
|
+
name="Jane Doe"
|
|
44
|
+
src="https://example.com/avatar.jpg"
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
// The <img> should be in the DOM (hidden until loaded).
|
|
48
|
+
const img = screen.getByAltText( '' );
|
|
49
|
+
expect( img.tagName ).toBe( 'IMG' );
|
|
50
|
+
expect( img ).toHaveAttribute(
|
|
51
|
+
'src',
|
|
52
|
+
'https://example.com/avatar.jpg'
|
|
53
|
+
);
|
|
54
|
+
} );
|
|
55
|
+
|
|
56
|
+
it( 'should apply has-src class only after image loads', () => {
|
|
57
|
+
render(
|
|
58
|
+
<Avatar
|
|
59
|
+
data-testid="avatar"
|
|
60
|
+
name="Jane Doe"
|
|
61
|
+
src="https://example.com/avatar.jpg"
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
const avatar = screen.getByTestId( 'avatar' );
|
|
65
|
+
// Before load fires, has-src should not be set.
|
|
66
|
+
expect( avatar ).not.toHaveClass( 'has-src' );
|
|
67
|
+
|
|
68
|
+
// Simulate image load.
|
|
69
|
+
fireEvent.load( screen.getByAltText( '' ) );
|
|
70
|
+
expect( avatar ).toHaveClass( 'has-src' );
|
|
71
|
+
} );
|
|
72
|
+
|
|
73
|
+
it( 'should apply is-small class for small size', () => {
|
|
74
|
+
render( <Avatar data-testid="avatar" size="small" /> );
|
|
75
|
+
const avatar = screen.getByTestId( 'avatar' );
|
|
76
|
+
expect( avatar ).toHaveClass( 'is-small' );
|
|
77
|
+
} );
|
|
78
|
+
|
|
79
|
+
it( 'should not apply is-small class for default size', () => {
|
|
80
|
+
render( <Avatar data-testid="avatar" /> );
|
|
81
|
+
const avatar = screen.getByTestId( 'avatar' );
|
|
82
|
+
expect( avatar ).not.toHaveClass( 'is-small' );
|
|
83
|
+
} );
|
|
84
|
+
|
|
85
|
+
it( 'should apply border color when provided', () => {
|
|
86
|
+
render( <Avatar data-testid="avatar" borderColor="#3858e9" /> );
|
|
87
|
+
const avatar = screen.getByTestId( 'avatar' );
|
|
88
|
+
expect( avatar ).toHaveClass( 'has-avatar-border-color' );
|
|
89
|
+
expect(
|
|
90
|
+
avatar.style.getPropertyValue( '--editor-avatar-outline-color' )
|
|
91
|
+
).toBe( '#3858e9' );
|
|
92
|
+
} );
|
|
93
|
+
|
|
94
|
+
it( 'should set name color custom property when borderColor is provided', () => {
|
|
95
|
+
render( <Avatar data-testid="avatar" borderColor="#3858e9" /> );
|
|
96
|
+
const avatar = screen.getByTestId( 'avatar' );
|
|
97
|
+
expect(
|
|
98
|
+
avatar.style.getPropertyValue( '--editor-avatar-name-color' )
|
|
99
|
+
).toBeTruthy();
|
|
100
|
+
} );
|
|
101
|
+
|
|
102
|
+
it( 'should not have has-src class when src is not provided', () => {
|
|
103
|
+
render( <Avatar data-testid="avatar" /> );
|
|
104
|
+
const avatar = screen.getByTestId( 'avatar' );
|
|
105
|
+
expect( avatar ).not.toHaveClass( 'has-src' );
|
|
106
|
+
} );
|
|
107
|
+
|
|
108
|
+
it( 'should combine custom className with default class', () => {
|
|
109
|
+
render( <Avatar data-testid="avatar" className="custom" /> );
|
|
110
|
+
const avatar = screen.getByTestId( 'avatar' );
|
|
111
|
+
expect( avatar ).toHaveClass( 'editor-avatar' );
|
|
112
|
+
expect( avatar ).toHaveClass( 'custom' );
|
|
113
|
+
} );
|
|
114
|
+
|
|
115
|
+
it( 'should pass through additional HTML attributes', () => {
|
|
116
|
+
render( <Avatar data-testid="avatar" data-custom="value" /> );
|
|
117
|
+
const avatar = screen.getByTestId( 'avatar' );
|
|
118
|
+
expect( avatar ).toHaveAttribute( 'data-custom', 'value' );
|
|
119
|
+
} );
|
|
120
|
+
|
|
121
|
+
it( 'should merge style prop with custom properties', () => {
|
|
122
|
+
render(
|
|
123
|
+
<Avatar
|
|
124
|
+
data-testid="avatar"
|
|
125
|
+
borderColor="#3858e9"
|
|
126
|
+
style={ { left: '10px' } }
|
|
127
|
+
/>
|
|
128
|
+
);
|
|
129
|
+
const avatar = screen.getByTestId( 'avatar' );
|
|
130
|
+
expect( avatar ).toHaveStyle( { left: '10px' } );
|
|
131
|
+
expect(
|
|
132
|
+
avatar.style.getPropertyValue( '--editor-avatar-outline-color' )
|
|
133
|
+
).toBe( '#3858e9' );
|
|
134
|
+
} );
|
|
135
|
+
|
|
136
|
+
describe( 'variant: badge', () => {
|
|
137
|
+
it( 'should not show badge by default', () => {
|
|
138
|
+
render( <Avatar data-testid="avatar" name="Zoraya" /> );
|
|
139
|
+
const avatar = screen.getByTestId( 'avatar' );
|
|
140
|
+
expect( avatar ).not.toHaveClass( 'is-badge' );
|
|
141
|
+
expect( screen.queryByText( 'Zoraya' ) ).not.toBeInTheDocument();
|
|
142
|
+
} );
|
|
143
|
+
|
|
144
|
+
it( 'should render name span with badge variant', () => {
|
|
145
|
+
render(
|
|
146
|
+
<Avatar data-testid="avatar" name="Zoraya" variant="badge" />
|
|
147
|
+
);
|
|
148
|
+
const avatar = screen.getByTestId( 'avatar' );
|
|
149
|
+
expect( avatar ).toHaveClass( 'is-badge' );
|
|
150
|
+
expect( screen.getByText( 'Zoraya' ) ).toBeInTheDocument();
|
|
151
|
+
} );
|
|
152
|
+
|
|
153
|
+
it( 'should render name span with borderColor too', () => {
|
|
154
|
+
render(
|
|
155
|
+
<Avatar
|
|
156
|
+
data-testid="avatar"
|
|
157
|
+
name="Zoraya"
|
|
158
|
+
borderColor="#3d5eef"
|
|
159
|
+
variant="badge"
|
|
160
|
+
/>
|
|
161
|
+
);
|
|
162
|
+
const avatar = screen.getByTestId( 'avatar' );
|
|
163
|
+
expect( avatar ).toHaveClass( 'is-badge' );
|
|
164
|
+
expect( screen.getByText( 'Zoraya' ) ).toBeInTheDocument();
|
|
165
|
+
} );
|
|
166
|
+
|
|
167
|
+
it( 'should not show badge when name is missing', () => {
|
|
168
|
+
render( <Avatar data-testid="avatar" variant="badge" /> );
|
|
169
|
+
const avatar = screen.getByTestId( 'avatar' );
|
|
170
|
+
expect( avatar ).not.toHaveClass( 'is-badge' );
|
|
171
|
+
} );
|
|
172
|
+
|
|
173
|
+
it( 'should still set aria-label even when badge is visible', () => {
|
|
174
|
+
render( <Avatar name="Zoraya" variant="badge" /> );
|
|
175
|
+
const avatar = screen.getByRole( 'img', { name: 'Zoraya' } );
|
|
176
|
+
expect( avatar ).toBeInTheDocument();
|
|
177
|
+
} );
|
|
178
|
+
} );
|
|
179
|
+
|
|
180
|
+
describe( 'label', () => {
|
|
181
|
+
it( 'should show label text instead of name in the badge', () => {
|
|
182
|
+
render( <Avatar name="Jane Doe" label="You" variant="badge" /> );
|
|
183
|
+
expect( screen.getByText( 'You' ) ).toBeInTheDocument();
|
|
184
|
+
expect( screen.queryByText( 'Jane Doe' ) ).not.toBeInTheDocument();
|
|
185
|
+
} );
|
|
186
|
+
|
|
187
|
+
it( 'should keep aria-label as name when label is provided', () => {
|
|
188
|
+
render( <Avatar name="Jane Doe" label="You" variant="badge" /> );
|
|
189
|
+
const avatar = screen.getByRole( 'img', { name: 'Jane Doe' } );
|
|
190
|
+
expect( avatar ).toBeInTheDocument();
|
|
191
|
+
} );
|
|
192
|
+
|
|
193
|
+
it( 'should wrap in tooltip when label differs from name', () => {
|
|
194
|
+
render( <Avatar name="Jane Doe" label="You" variant="badge" /> );
|
|
195
|
+
const avatar = screen.getByRole( 'img', { name: 'Jane Doe' } );
|
|
196
|
+
// The Tooltip's Ariakit.TooltipAnchor makes the element
|
|
197
|
+
// focusable so the tooltip can be triggered via keyboard.
|
|
198
|
+
expect( avatar ).toHaveAttribute( 'tabindex', '0' );
|
|
199
|
+
} );
|
|
200
|
+
} );
|
|
201
|
+
|
|
202
|
+
describe( 'dimmed', () => {
|
|
203
|
+
it( 'should apply is-dimmed class when dimmed', () => {
|
|
204
|
+
render( <Avatar data-testid="avatar" dimmed /> );
|
|
205
|
+
const avatar = screen.getByTestId( 'avatar' );
|
|
206
|
+
expect( avatar ).toHaveClass( 'is-dimmed' );
|
|
207
|
+
} );
|
|
208
|
+
|
|
209
|
+
it( 'should not apply is-dimmed class by default', () => {
|
|
210
|
+
render( <Avatar data-testid="avatar" /> );
|
|
211
|
+
const avatar = screen.getByTestId( 'avatar' );
|
|
212
|
+
expect( avatar ).not.toHaveClass( 'is-dimmed' );
|
|
213
|
+
} );
|
|
214
|
+
|
|
215
|
+
it( 'should render statusIndicator when dimmed', () => {
|
|
216
|
+
render(
|
|
217
|
+
<Avatar
|
|
218
|
+
data-testid="avatar"
|
|
219
|
+
dimmed
|
|
220
|
+
statusIndicator={ <span>icon</span> }
|
|
221
|
+
/>
|
|
222
|
+
);
|
|
223
|
+
expect( screen.getByText( 'icon' ) ).toBeInTheDocument();
|
|
224
|
+
} );
|
|
225
|
+
|
|
226
|
+
it( 'should not render statusIndicator when not dimmed', () => {
|
|
227
|
+
render(
|
|
228
|
+
<Avatar
|
|
229
|
+
data-testid="avatar"
|
|
230
|
+
statusIndicator={ <span>icon</span> }
|
|
231
|
+
/>
|
|
232
|
+
);
|
|
233
|
+
expect( screen.queryByText( 'icon' ) ).not.toBeInTheDocument();
|
|
234
|
+
} );
|
|
235
|
+
|
|
236
|
+
it( 'should apply has-src class when dimmed after image loads', () => {
|
|
237
|
+
render(
|
|
238
|
+
<Avatar
|
|
239
|
+
data-testid="avatar"
|
|
240
|
+
src="https://example.com/avatar.jpg"
|
|
241
|
+
dimmed
|
|
242
|
+
/>
|
|
243
|
+
);
|
|
244
|
+
fireEvent.load( screen.getByAltText( '' ) );
|
|
245
|
+
const avatar = screen.getByTestId( 'avatar' );
|
|
246
|
+
expect( avatar ).toHaveClass( 'has-src' );
|
|
247
|
+
expect( avatar ).toHaveClass( 'is-dimmed' );
|
|
248
|
+
} );
|
|
249
|
+
} );
|
|
250
|
+
|
|
251
|
+
describe( 'initials', () => {
|
|
252
|
+
it( 'should show initials when no src is provided', () => {
|
|
253
|
+
render( <Avatar name="Tanner Robinson" /> );
|
|
254
|
+
expect( screen.getByText( 'TR' ) ).toBeInTheDocument();
|
|
255
|
+
} );
|
|
256
|
+
|
|
257
|
+
it( 'should show single initial for single-word name', () => {
|
|
258
|
+
render( <Avatar name="Zoraya" /> );
|
|
259
|
+
expect( screen.getByText( 'Z' ) ).toBeInTheDocument();
|
|
260
|
+
} );
|
|
261
|
+
|
|
262
|
+
it( 'should limit initials to two characters', () => {
|
|
263
|
+
render( <Avatar name="Jane Marie Doe" /> );
|
|
264
|
+
expect( screen.getByText( 'JM' ) ).toBeInTheDocument();
|
|
265
|
+
} );
|
|
266
|
+
|
|
267
|
+
it( 'should uppercase initials', () => {
|
|
268
|
+
render( <Avatar name="jane doe" /> );
|
|
269
|
+
expect( screen.getByText( 'JD' ) ).toBeInTheDocument();
|
|
270
|
+
} );
|
|
271
|
+
|
|
272
|
+
it( 'should not show initials after image loads', () => {
|
|
273
|
+
render(
|
|
274
|
+
<Avatar
|
|
275
|
+
name="Tanner Robinson"
|
|
276
|
+
src="https://example.com/avatar.jpg"
|
|
277
|
+
/>
|
|
278
|
+
);
|
|
279
|
+
fireEvent.load( screen.getByAltText( '' ) );
|
|
280
|
+
expect( screen.queryByText( 'TR' ) ).not.toBeInTheDocument();
|
|
281
|
+
} );
|
|
282
|
+
|
|
283
|
+
it( 'should not render initials when name is not provided', () => {
|
|
284
|
+
render( <Avatar data-testid="avatar" /> );
|
|
285
|
+
const avatar = screen.getByTestId( 'avatar' );
|
|
286
|
+
// Without a name, the image span should be empty (no initials).
|
|
287
|
+
expect( avatar ).not.toHaveTextContent( /.+/ );
|
|
288
|
+
} );
|
|
289
|
+
} );
|
|
290
|
+
|
|
291
|
+
describe( 'image loading', () => {
|
|
292
|
+
it( 'should reset to loading state when src changes', () => {
|
|
293
|
+
const { rerender } = render(
|
|
294
|
+
<Avatar
|
|
295
|
+
data-testid="avatar"
|
|
296
|
+
name="Jane Doe"
|
|
297
|
+
src="https://example.com/a.jpg"
|
|
298
|
+
/>
|
|
299
|
+
);
|
|
300
|
+
fireEvent.load( screen.getByAltText( '' ) );
|
|
301
|
+
expect( screen.getByTestId( 'avatar' ) ).toHaveClass( 'has-src' );
|
|
302
|
+
|
|
303
|
+
rerender(
|
|
304
|
+
<Avatar
|
|
305
|
+
data-testid="avatar"
|
|
306
|
+
name="Jane Doe"
|
|
307
|
+
src="https://example.com/b.jpg"
|
|
308
|
+
/>
|
|
309
|
+
);
|
|
310
|
+
// New src should reset to loading — initials visible again.
|
|
311
|
+
expect( screen.getByTestId( 'avatar' ) ).not.toHaveClass(
|
|
312
|
+
'has-src'
|
|
313
|
+
);
|
|
314
|
+
expect( screen.getByText( 'JD' ) ).toBeInTheDocument();
|
|
315
|
+
} );
|
|
316
|
+
|
|
317
|
+
it( 'should show initials while image is loading', () => {
|
|
318
|
+
render(
|
|
319
|
+
<Avatar
|
|
320
|
+
data-testid="avatar"
|
|
321
|
+
name="Jane Doe"
|
|
322
|
+
src="https://example.com/avatar.jpg"
|
|
323
|
+
/>
|
|
324
|
+
);
|
|
325
|
+
const avatar = screen.getByTestId( 'avatar' );
|
|
326
|
+
// Before load event, initials should show.
|
|
327
|
+
expect( avatar ).not.toHaveClass( 'has-src' );
|
|
328
|
+
expect( screen.getByText( 'JD' ) ).toBeInTheDocument();
|
|
329
|
+
} );
|
|
330
|
+
|
|
331
|
+
it( 'should show image after successful load', () => {
|
|
332
|
+
render(
|
|
333
|
+
<Avatar
|
|
334
|
+
data-testid="avatar"
|
|
335
|
+
name="Jane Doe"
|
|
336
|
+
src="https://example.com/avatar.jpg"
|
|
337
|
+
/>
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
fireEvent.load( screen.getByAltText( '' ) );
|
|
341
|
+
|
|
342
|
+
const avatar = screen.getByTestId( 'avatar' );
|
|
343
|
+
expect( avatar ).toHaveClass( 'has-src' );
|
|
344
|
+
expect( screen.queryByText( 'JD' ) ).not.toBeInTheDocument();
|
|
345
|
+
} );
|
|
346
|
+
|
|
347
|
+
it( 'should fall back to initials when image fails to load', () => {
|
|
348
|
+
render(
|
|
349
|
+
<Avatar
|
|
350
|
+
data-testid="avatar"
|
|
351
|
+
name="Jane Doe"
|
|
352
|
+
src="https://example.com/bad.jpg"
|
|
353
|
+
/>
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
fireEvent.error( screen.getByAltText( '' ) );
|
|
357
|
+
|
|
358
|
+
const avatar = screen.getByTestId( 'avatar' );
|
|
359
|
+
expect( avatar ).not.toHaveClass( 'has-src' );
|
|
360
|
+
expect( screen.getByText( 'JD' ) ).toBeInTheDocument();
|
|
361
|
+
} );
|
|
362
|
+
|
|
363
|
+
it( 'should not render img element when no src is provided', () => {
|
|
364
|
+
render( <Avatar data-testid="avatar" name="Jane Doe" /> );
|
|
365
|
+
expect( screen.queryByAltText( '' ) ).not.toBeInTheDocument();
|
|
366
|
+
} );
|
|
367
|
+
} );
|
|
368
|
+
|
|
369
|
+
describe( 'tooltip', () => {
|
|
370
|
+
it( 'should wrap in tooltip when name is provided without badge', () => {
|
|
371
|
+
render( <Avatar name="Jane Doe" /> );
|
|
372
|
+
const avatar = screen.getByRole( 'img', { name: 'Jane Doe' } );
|
|
373
|
+
expect( avatar ).toHaveAttribute( 'tabindex', '0' );
|
|
374
|
+
} );
|
|
375
|
+
|
|
376
|
+
it( 'should not wrap in tooltip for badge without label', () => {
|
|
377
|
+
render( <Avatar name="Jane Doe" variant="badge" /> );
|
|
378
|
+
const avatar = screen.getByRole( 'img', { name: 'Jane Doe' } );
|
|
379
|
+
// Badge shows the name visibly, so no tooltip needed.
|
|
380
|
+
expect( avatar ).not.toHaveAttribute( 'tabindex' );
|
|
381
|
+
} );
|
|
382
|
+
|
|
383
|
+
it( 'should not wrap in tooltip when name is not provided', () => {
|
|
384
|
+
render( <Avatar data-testid="avatar" /> );
|
|
385
|
+
const avatar = screen.getByTestId( 'avatar' );
|
|
386
|
+
expect( avatar ).not.toHaveAttribute( 'tabindex' );
|
|
387
|
+
} );
|
|
388
|
+
} );
|
|
389
|
+
} );
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { IconType } from '@wordpress/components';
|
|
2
|
+
|
|
3
|
+
export type AvatarProps = {
|
|
4
|
+
/**
|
|
5
|
+
* URL of the avatar image.
|
|
6
|
+
*
|
|
7
|
+
* When not provided, initials derived from `name` are shown.
|
|
8
|
+
*/
|
|
9
|
+
src?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Name of the user. Used as an accessible label and to generate
|
|
12
|
+
* initials when no image is provided.
|
|
13
|
+
*/
|
|
14
|
+
name?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Visible text shown in the hover badge. When not provided, `name`
|
|
17
|
+
* is used instead. Use this to provide contextual labels like "You"
|
|
18
|
+
* without affecting the accessible name or initials.
|
|
19
|
+
*/
|
|
20
|
+
label?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Specifies the avatar's visual style treatment.
|
|
23
|
+
*
|
|
24
|
+
* - `'badge'`: Displays a hover-expand pill that reveals the user's
|
|
25
|
+
* name (or `label`) on hover. Requires `name` to be set.
|
|
26
|
+
*
|
|
27
|
+
* Leave undefined for the default circular avatar.
|
|
28
|
+
*/
|
|
29
|
+
variant?: 'badge';
|
|
30
|
+
/**
|
|
31
|
+
* Size of the avatar.
|
|
32
|
+
*
|
|
33
|
+
* - `'default'`: For standalone avatars and list items where the
|
|
34
|
+
* avatar is a primary visual element (e.g. collaborator lists,
|
|
35
|
+
* user profiles).
|
|
36
|
+
* - `'small'`: For inline or compact contexts where space is
|
|
37
|
+
* limited (e.g. cursor labels, toolbars, badges alongside text).
|
|
38
|
+
*
|
|
39
|
+
* @default 'default'
|
|
40
|
+
*/
|
|
41
|
+
size?: 'default' | 'small';
|
|
42
|
+
/**
|
|
43
|
+
* CSS color value for an accent border ring around the avatar.
|
|
44
|
+
*
|
|
45
|
+
* When not provided, no border is rendered and the hover badge
|
|
46
|
+
* uses the admin theme color as its background.
|
|
47
|
+
*/
|
|
48
|
+
borderColor?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Whether to dim the avatar to indicate an inactive or away state.
|
|
51
|
+
* When true, images are desaturated and faded, and initials are
|
|
52
|
+
* reduced in opacity.
|
|
53
|
+
*
|
|
54
|
+
* @default false
|
|
55
|
+
*/
|
|
56
|
+
dimmed?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* An icon or custom component rendered as a centered overlay on the
|
|
59
|
+
* avatar image. Only visible when `dimmed` is true.
|
|
60
|
+
*
|
|
61
|
+
* Accepts any value supported by the `Icon` component: an icon from
|
|
62
|
+
* `@wordpress/icons`, a Dashicon name string, a component, or a
|
|
63
|
+
* JSX element.
|
|
64
|
+
*/
|
|
65
|
+
statusIndicator?: IconType | null;
|
|
66
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { useCallback, useState } from '@wordpress/element';
|
|
5
|
+
|
|
6
|
+
export type ImageLoadingStatus = 'idle' | 'loading' | 'loaded' | 'error';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Tracks the loading status of an image URL. Returns the current status and
|
|
10
|
+
* `onLoad`/`onError` callbacks to attach to the `<img>` element.
|
|
11
|
+
*
|
|
12
|
+
* Unlike a side-channel `new Image()` preloader, this hook relies on the
|
|
13
|
+
* native `<img>` element's own events, which avoids cross-browser issues
|
|
14
|
+
* with Safari's privacy features blocking programmatic image requests.
|
|
15
|
+
*
|
|
16
|
+
* @param src - The image URL. When falsy, status is `'idle'`.
|
|
17
|
+
*/
|
|
18
|
+
export function useImageLoadingStatus( src?: string ) {
|
|
19
|
+
const [ prevSrc, setPrevSrc ] = useState( src );
|
|
20
|
+
const [ status, setStatus ] = useState< ImageLoadingStatus >(
|
|
21
|
+
src ? 'loading' : 'idle'
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
// Synchronous reset when src changes — runs during render, not after
|
|
25
|
+
// commit, so a cached image's `load` event cannot sneak in before
|
|
26
|
+
// the reset and get overwritten.
|
|
27
|
+
if ( prevSrc !== src ) {
|
|
28
|
+
setPrevSrc( src );
|
|
29
|
+
setStatus( src ? 'loading' : 'idle' );
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const handleLoad = useCallback( () => setStatus( 'loaded' ), [] );
|
|
33
|
+
const handleError = useCallback( () => setStatus( 'error' ), [] );
|
|
34
|
+
|
|
35
|
+
return { status, handleLoad, handleError };
|
|
36
|
+
}
|