newspack-components 4.2.0 → 4.3.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/dist/cjs/action-card/index.js +21 -12
- package/dist/cjs/action-card/style.scss +25 -2
- package/dist/cjs/autocomplete-tokenfield/index.js +2 -2
- package/dist/cjs/autocomplete-with-latest-posts/index.js +289 -0
- package/dist/cjs/autocomplete-with-latest-posts/style.scss +150 -0
- package/dist/cjs/badge/index.js +32 -0
- package/dist/cjs/badge/style.scss +36 -0
- package/dist/cjs/button/index.js +2 -0
- package/dist/cjs/card/core-card.js +141 -0
- package/dist/cjs/card/index.js +33 -0
- package/dist/cjs/card/style-core.scss +227 -0
- package/dist/cjs/card/style.scss +2 -2
- package/dist/cjs/card-settings-group/index.js +49 -0
- package/dist/cjs/card-settings-group/style.scss +33 -0
- package/dist/cjs/card-sortable-list/index.js +390 -0
- package/dist/cjs/card-sortable-list/style.scss +48 -0
- package/dist/cjs/confirm-dialog/index.js +59 -0
- package/dist/cjs/divider/index.js +42 -0
- package/dist/cjs/divider/style.scss +51 -0
- package/dist/cjs/form-token-field/style.scss +0 -5
- package/dist/cjs/grid/index.js +2 -1
- package/dist/cjs/grid/style.scss +63 -9
- package/dist/cjs/index.js +42 -0
- package/dist/cjs/modal/index.js +11 -4
- package/dist/cjs/modal/style.scss +43 -13
- package/dist/cjs/newspack-icon/style.scss +5 -3
- package/dist/cjs/section-header/index.js +76 -17
- package/dist/cjs/section-header/style.scss +109 -4
- package/dist/cjs/style.scss +2 -0
- package/dist/cjs/waiting/index.js +3 -1
- package/dist/cjs/waiting/style.scss +4 -0
- package/dist/cjs/web-preview/index.js +98 -39
- package/dist/cjs/web-preview/style.scss +20 -53
- package/dist/cjs/with-wizard/index.js +1 -1
- package/dist/cjs/with-wizard/style.scss +24 -6
- package/dist/cjs/with-wizard-screen/style.scss +39 -10
- package/dist/cjs/wizard/components/WizardSnackbar.js +70 -0
- package/dist/cjs/wizard/components/style.scss +48 -0
- package/dist/cjs/wizard/index.js +97 -16
- package/dist/cjs/wizard/store/index.js +46 -0
- package/dist/esm/action-card/index.js +21 -12
- package/dist/esm/action-card/style.scss +25 -2
- package/dist/esm/autocomplete-tokenfield/index.js +2 -2
- package/dist/esm/autocomplete-with-latest-posts/index.js +281 -0
- package/dist/esm/autocomplete-with-latest-posts/style.scss +150 -0
- package/dist/esm/badge/index.js +24 -0
- package/dist/esm/badge/style.scss +36 -0
- package/dist/esm/button/index.js +2 -0
- package/dist/esm/card/core-card.js +133 -0
- package/dist/esm/card/index.js +33 -0
- package/dist/esm/card/style-core.scss +227 -0
- package/dist/esm/card/style.scss +2 -2
- package/dist/esm/card-settings-group/index.js +42 -0
- package/dist/esm/card-settings-group/style.scss +33 -0
- package/dist/esm/card-sortable-list/index.js +381 -0
- package/dist/esm/card-sortable-list/style.scss +48 -0
- package/dist/esm/confirm-dialog/index.js +49 -0
- package/dist/esm/divider/index.js +34 -0
- package/dist/esm/divider/style.scss +51 -0
- package/dist/esm/form-token-field/style.scss +0 -5
- package/dist/esm/grid/index.js +2 -1
- package/dist/esm/grid/style.scss +63 -9
- package/dist/esm/index.js +6 -0
- package/dist/esm/modal/index.js +11 -4
- package/dist/esm/modal/style.scss +43 -13
- package/dist/esm/newspack-icon/style.scss +5 -3
- package/dist/esm/section-header/index.js +75 -18
- package/dist/esm/section-header/style.scss +109 -4
- package/dist/esm/style.scss +2 -0
- package/dist/esm/waiting/index.js +3 -1
- package/dist/esm/waiting/style.scss +4 -0
- package/dist/esm/web-preview/index.js +101 -42
- package/dist/esm/web-preview/style.scss +20 -53
- package/dist/esm/with-wizard/index.js +1 -1
- package/dist/esm/with-wizard/style.scss +24 -6
- package/dist/esm/with-wizard-screen/style.scss +39 -10
- package/dist/esm/wizard/components/WizardSnackbar.js +64 -0
- package/dist/esm/wizard/components/style.scss +48 -0
- package/dist/esm/wizard/index.js +101 -20
- package/dist/esm/wizard/store/index.js +46 -0
- package/package.json +5 -6
- package/shared/author-context.js +26 -0
- package/shared/hooks/use-coauthors.js +220 -0
- package/shared/hooks/use-coauthors.test.js +624 -0
- package/shared/hooks/use-custom-byline.js +47 -0
- package/shared/hooks/use-custom-byline.test.js +133 -0
- package/shared/scss/_mixins.scss +46 -0
- package/src/action-card/action-card.d.ts +7 -3
- package/src/action-card/index.js +24 -11
- package/src/action-card/style.scss +25 -2
- package/src/autocomplete-tokenfield/index.js +2 -2
- package/src/autocomplete-with-latest-posts/index.js +290 -0
- package/src/autocomplete-with-latest-posts/style.scss +150 -0
- package/src/badge/index.tsx +24 -0
- package/src/badge/style.scss +36 -0
- package/src/button/index.tsx +3 -2
- package/src/card/core-card.js +147 -0
- package/src/card/index.js +39 -1
- package/src/card/style-core.scss +227 -0
- package/src/card/style.scss +2 -2
- package/src/card-settings-group/README.md +43 -0
- package/src/card-settings-group/index.tsx +53 -0
- package/src/card-settings-group/style.scss +33 -0
- package/src/card-sortable-list/README.md +51 -0
- package/src/card-sortable-list/index.tsx +412 -0
- package/src/card-sortable-list/style.scss +48 -0
- package/src/confirm-dialog/index.tsx +64 -0
- package/src/divider/README.md +25 -0
- package/src/divider/index.js +31 -0
- package/src/divider/style.scss +51 -0
- package/src/form-token-field/style.scss +0 -5
- package/src/grid/index.tsx +2 -1
- package/src/grid/style.scss +63 -9
- package/src/index.js +6 -0
- package/src/modal/index.js +12 -3
- package/src/modal/style.scss +43 -13
- package/src/newspack-icon/style.scss +5 -3
- package/src/section-header/index.js +72 -13
- package/src/section-header/style.scss +109 -4
- package/src/style.scss +2 -0
- package/src/waiting/index.js +2 -1
- package/src/waiting/style.scss +4 -0
- package/src/web-preview/index.js +90 -35
- package/src/web-preview/style.scss +20 -53
- package/src/with-wizard/index.js +1 -1
- package/src/with-wizard/style.scss +24 -6
- package/src/with-wizard-screen/style.scss +39 -10
- package/src/wizard/components/WizardSnackbar.js +58 -0
- package/src/wizard/components/style.scss +48 -0
- package/src/wizard/index.js +108 -14
- package/src/wizard/store/index.js +26 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Card using WP Core component
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
@use "~@wordpress/base-styles/colors" as wp-colors;
|
|
6
|
+
@use "../../../colors/colors.module" as colors;
|
|
7
|
+
|
|
8
|
+
.newspack-card--core,
|
|
9
|
+
.newspack-wizard .newspack-card--core {
|
|
10
|
+
background: white;
|
|
11
|
+
position: relative;
|
|
12
|
+
transition: background-color 125ms ease-in-out, box-shadow 125ms ease-in-out, color 125ms ease-in-out;
|
|
13
|
+
svg {
|
|
14
|
+
transition: fill 125ms ease-in-out;
|
|
15
|
+
}
|
|
16
|
+
&__icon {
|
|
17
|
+
border-radius: 50%;
|
|
18
|
+
display: grid;
|
|
19
|
+
height: 48px;
|
|
20
|
+
place-items: center;
|
|
21
|
+
width: 48px;
|
|
22
|
+
svg {
|
|
23
|
+
fill: var(--wp-admin-theme-color);
|
|
24
|
+
height: 40px;
|
|
25
|
+
width: 40px;
|
|
26
|
+
}
|
|
27
|
+
.newspack-card--core__has-icon-background-color & {
|
|
28
|
+
background: var(--wp-admin-theme-color-lighter-10);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
&__is-small {
|
|
32
|
+
h1,
|
|
33
|
+
h2,
|
|
34
|
+
h3,
|
|
35
|
+
h4,
|
|
36
|
+
h5,
|
|
37
|
+
h6 {
|
|
38
|
+
font-size: 14px;
|
|
39
|
+
font-weight: 500;
|
|
40
|
+
}
|
|
41
|
+
.newspack-card--core__icon {
|
|
42
|
+
height: 40px;
|
|
43
|
+
width: 40px;
|
|
44
|
+
svg {
|
|
45
|
+
height: 24px;
|
|
46
|
+
width: 24px;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
&__no-margin .components-card__body {
|
|
51
|
+
padding: 0 !important;
|
|
52
|
+
}
|
|
53
|
+
&__header {
|
|
54
|
+
color: wp-colors.$gray-700;
|
|
55
|
+
column-gap: 16px;
|
|
56
|
+
position: relative;
|
|
57
|
+
.newspack-card--core__action {
|
|
58
|
+
margin: 0;
|
|
59
|
+
label {
|
|
60
|
+
border: 0;
|
|
61
|
+
clip-path: inset(50%);
|
|
62
|
+
height: 1px;
|
|
63
|
+
margin: -1px;
|
|
64
|
+
overflow: hidden;
|
|
65
|
+
padding: 0;
|
|
66
|
+
position: absolute !important;
|
|
67
|
+
width: 1px;
|
|
68
|
+
word-wrap: normal !important;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
.components-dropdown-menu {
|
|
72
|
+
aspect-ratio: 1;
|
|
73
|
+
border-left: 1px solid wp-colors.$gray-200;
|
|
74
|
+
display: grid;
|
|
75
|
+
height: 100%;
|
|
76
|
+
padding: 4px;
|
|
77
|
+
place-items: center;
|
|
78
|
+
position: absolute;
|
|
79
|
+
right: 0;
|
|
80
|
+
top: 0;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
&__header--is-draggable {
|
|
84
|
+
.newspack-card--core__header-content {
|
|
85
|
+
padding-left: 65px;
|
|
86
|
+
}
|
|
87
|
+
.newspack-card--core__header__draggable-controls {
|
|
88
|
+
align-items: center;
|
|
89
|
+
border-right: 1px solid wp-colors.$gray-200;
|
|
90
|
+
display: flex;
|
|
91
|
+
height: 100%;
|
|
92
|
+
justify-content: space-between;
|
|
93
|
+
left: 0;
|
|
94
|
+
padding: 0 8px;
|
|
95
|
+
position: absolute;
|
|
96
|
+
top: 0;
|
|
97
|
+
}
|
|
98
|
+
.newspack-card--core__header__draggable-controls__drag-handle {
|
|
99
|
+
cursor: grab;
|
|
100
|
+
display: grid;
|
|
101
|
+
place-items: center;
|
|
102
|
+
}
|
|
103
|
+
.newspack-card--core__header__draggable-controls__move-buttons {
|
|
104
|
+
display: flex;
|
|
105
|
+
flex-direction: column;
|
|
106
|
+
.components-button.has-icon {
|
|
107
|
+
height: 16px;
|
|
108
|
+
padding: 0;
|
|
109
|
+
width: 100%;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
button.newspack-card--core__header {
|
|
114
|
+
appearance: none;
|
|
115
|
+
background-color: white;
|
|
116
|
+
border: none;
|
|
117
|
+
cursor: pointer;
|
|
118
|
+
text-align: left;
|
|
119
|
+
}
|
|
120
|
+
&__header-content {
|
|
121
|
+
display: grid;
|
|
122
|
+
flex: 1;
|
|
123
|
+
grid-gap: 8px;
|
|
124
|
+
h1,
|
|
125
|
+
h2,
|
|
126
|
+
h3,
|
|
127
|
+
h4,
|
|
128
|
+
h5,
|
|
129
|
+
h6,
|
|
130
|
+
p {
|
|
131
|
+
margin-bottom: 0;
|
|
132
|
+
margin-top: 0;
|
|
133
|
+
transition: color 125ms ease-in-out;
|
|
134
|
+
}
|
|
135
|
+
h1,
|
|
136
|
+
h2,
|
|
137
|
+
h3,
|
|
138
|
+
h4,
|
|
139
|
+
h5,
|
|
140
|
+
h6 {
|
|
141
|
+
align-items: center;
|
|
142
|
+
color: wp-colors.$gray-900;
|
|
143
|
+
font-weight: 500;
|
|
144
|
+
display: flex;
|
|
145
|
+
gap: 8px;
|
|
146
|
+
.newspack-badge {
|
|
147
|
+
font-weight: 400;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
&__is-active {
|
|
152
|
+
border-color: var(--wp-admin-theme-color);
|
|
153
|
+
box-shadow: var(--wp-admin-theme-color) 0 0 0 1px;
|
|
154
|
+
button.newspack-card--core__header {
|
|
155
|
+
background-color: rgba( var(--wp-admin-theme-color--rgb), 0.04 );
|
|
156
|
+
color: var(--wp-admin-theme-color);
|
|
157
|
+
h1,
|
|
158
|
+
h2,
|
|
159
|
+
h3,
|
|
160
|
+
h4,
|
|
161
|
+
h5,
|
|
162
|
+
h6 {
|
|
163
|
+
color: var(--wp-admin-theme-color);
|
|
164
|
+
}
|
|
165
|
+
svg {
|
|
166
|
+
fill: var(--wp-admin-theme-color);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
&.newspack-card--core__has-children {
|
|
171
|
+
button.newspack-card--core__header {
|
|
172
|
+
border-bottom-left-radius: 0;
|
|
173
|
+
border-bottom-right-radius: 0;
|
|
174
|
+
}
|
|
175
|
+
.newspack-card--core__body {
|
|
176
|
+
border-top: 1px solid var(--wp-admin-theme-color);
|
|
177
|
+
}
|
|
178
|
+
.components-card__divider {
|
|
179
|
+
background: wp-colors.$gray-100;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
&__buttons-card {
|
|
184
|
+
display: block;
|
|
185
|
+
text-decoration: none;
|
|
186
|
+
&:active,
|
|
187
|
+
&:hover {
|
|
188
|
+
border-color: var(--wp-admin-theme-color);
|
|
189
|
+
box-shadow: var(--wp-admin-theme-color) 0 0 0 1px;
|
|
190
|
+
.newspack-card--core__header {
|
|
191
|
+
color: var(--wp-admin-theme-color);
|
|
192
|
+
h1,
|
|
193
|
+
h2,
|
|
194
|
+
h3,
|
|
195
|
+
h4,
|
|
196
|
+
h5,
|
|
197
|
+
h6 {
|
|
198
|
+
color: var(--wp-admin-theme-color);
|
|
199
|
+
}
|
|
200
|
+
svg {
|
|
201
|
+
fill: var(--wp-admin-theme-color);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
hr.components-card__divider {
|
|
207
|
+
margin: 0;
|
|
208
|
+
}
|
|
209
|
+
&__is-narrow {
|
|
210
|
+
margin-left: auto;
|
|
211
|
+
margin-right: auto;
|
|
212
|
+
@media screen and ( min-width: 608px ) {
|
|
213
|
+
max-width: 576px;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
&__body {
|
|
217
|
+
> div:not(.components-card__body):not(.components-card__media):not(.components-card__divider) {
|
|
218
|
+
padding: 16px 24px;
|
|
219
|
+
.newspack-card--core__is-small & {
|
|
220
|
+
padding: 16px;
|
|
221
|
+
}
|
|
222
|
+
.newspack-card--core__no-margin & {
|
|
223
|
+
padding: 0;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
package/dist/esm/card/style.scss
CHANGED
|
@@ -133,11 +133,11 @@
|
|
|
133
133
|
|
|
134
134
|
// Thematic Break
|
|
135
135
|
|
|
136
|
-
hr {
|
|
136
|
+
hr:not(.newspack-divider) {
|
|
137
137
|
background: wp-colors.$gray-300;
|
|
138
138
|
border: 0;
|
|
139
139
|
height: 1px;
|
|
140
|
-
margin:
|
|
140
|
+
margin: 32px 0;
|
|
141
141
|
|
|
142
142
|
@media screen and ( min-width: 744px ) {
|
|
143
143
|
margin: 64px 0;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Card - Settings group component.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Internal dependencies
|
|
7
|
+
*/
|
|
8
|
+
import { Card } from '../';
|
|
9
|
+
import './style.scss';
|
|
10
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
11
|
+
const CardSettingsGroup = ({
|
|
12
|
+
actionType = 'chevron',
|
|
13
|
+
children,
|
|
14
|
+
icon = null,
|
|
15
|
+
title = '',
|
|
16
|
+
description = '',
|
|
17
|
+
isActive = false,
|
|
18
|
+
onEnable = () => {}
|
|
19
|
+
}) => {
|
|
20
|
+
return /*#__PURE__*/_jsx(Card, {
|
|
21
|
+
className: "newspack-card--core--settings-group",
|
|
22
|
+
actionType: actionType,
|
|
23
|
+
isSmall: true,
|
|
24
|
+
__experimentalCoreCard: true,
|
|
25
|
+
__experimentalCoreProps: {
|
|
26
|
+
header: /*#__PURE__*/_jsxs(_Fragment, {
|
|
27
|
+
children: [/*#__PURE__*/_jsx("h3", {
|
|
28
|
+
children: title
|
|
29
|
+
}), description && /*#__PURE__*/_jsx("p", {
|
|
30
|
+
children: description
|
|
31
|
+
})]
|
|
32
|
+
}),
|
|
33
|
+
onHeaderClick: onEnable,
|
|
34
|
+
icon,
|
|
35
|
+
iconBackgroundColor: true,
|
|
36
|
+
isActive,
|
|
37
|
+
title
|
|
38
|
+
},
|
|
39
|
+
children: isActive && children
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
export default CardSettingsGroup;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
.newspack-card--core--settings-group {
|
|
2
|
+
.components-card__body {
|
|
3
|
+
> .newspack-notice {
|
|
4
|
+
margin: 16px 0;
|
|
5
|
+
}
|
|
6
|
+
> .components-base-control {
|
|
7
|
+
&:first-child {
|
|
8
|
+
margin-top: 0;
|
|
9
|
+
}
|
|
10
|
+
+ .components-base-control,
|
|
11
|
+
+ .newspack-form-token-field {
|
|
12
|
+
margin-top: 16px;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** ToggleControl switch should appear after the label. */
|
|
17
|
+
> .components-toggle-control {
|
|
18
|
+
.components-h-stack {
|
|
19
|
+
.components-form-toggle {
|
|
20
|
+
order: 2;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
.components-toggle-control__help {
|
|
24
|
+
margin-inline-start: auto;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
.newspack-grid:last-child,
|
|
28
|
+
.components-base-control:last-child,
|
|
29
|
+
p:last-child {
|
|
30
|
+
margin-bottom: 0;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Card - Sortable list component.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* WordPress dependencies.
|
|
7
|
+
*/
|
|
8
|
+
import { Disabled, Draggable, __experimentalVStack as VStack } from '@wordpress/components'; // eslint-disable-line @wordpress/no-unsafe-wp-apis
|
|
9
|
+
import { useEffect, useLayoutEffect, useRef, useState } from '@wordpress/element';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Internal dependencies
|
|
13
|
+
*/
|
|
14
|
+
import { Badge, Card } from '../';
|
|
15
|
+
import './style.scss';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* External dependencies
|
|
19
|
+
*/
|
|
20
|
+
import classNames from 'classnames';
|
|
21
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
22
|
+
const DROP_ANIMATION_DURATION = 400; // ms — must match $drop-duration in style.scss
|
|
23
|
+
const BUTTON_MOVE_DURATION = 200; // ms — must match $shift-duration in style.scss
|
|
24
|
+
|
|
25
|
+
const CardSortableList = ({
|
|
26
|
+
disabled = false,
|
|
27
|
+
items = [],
|
|
28
|
+
onDragCallback = () => {}
|
|
29
|
+
}) => {
|
|
30
|
+
const [sortedItems, setSortedItems] = useState(items);
|
|
31
|
+
const [draggingIndex, setDraggingIndex] = useState(null);
|
|
32
|
+
const [hoverIndex, setHoverIndex] = useState(null);
|
|
33
|
+
const [droppedIndex, setDroppedIndex] = useState(null);
|
|
34
|
+
const [measurements, setMeasurements] = useState(null);
|
|
35
|
+
const listRef = useRef(null);
|
|
36
|
+
const itemRefs = useRef([]);
|
|
37
|
+
const dropAnimationTimer = useRef(null);
|
|
38
|
+
const buttonMoveTimer = useRef(null);
|
|
39
|
+
const buttonFlipRef = useRef(null);
|
|
40
|
+
const [buttonMoveId, setButtonMoveId] = useState(0);
|
|
41
|
+
const documentDragOverRef = useRef(null);
|
|
42
|
+
|
|
43
|
+
// Keep sortedItems in sync when the items prop changes externally (e.g. after a save).
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
setSortedItems(items);
|
|
46
|
+
}, [items]);
|
|
47
|
+
|
|
48
|
+
// Clean up any pending animation timer on unmount.
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
return () => {
|
|
51
|
+
if (dropAnimationTimer.current) {
|
|
52
|
+
clearTimeout(dropAnimationTimer.current);
|
|
53
|
+
}
|
|
54
|
+
if (buttonMoveTimer.current) {
|
|
55
|
+
clearTimeout(buttonMoveTimer.current);
|
|
56
|
+
}
|
|
57
|
+
if (documentDragOverRef.current) {
|
|
58
|
+
document.removeEventListener('dragover', documentDragOverRef.current);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}, []);
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Handle a chevron-button move. Records item positions before the reorder so
|
|
65
|
+
* the FLIP animation can play after React commits the new DOM order.
|
|
66
|
+
*/
|
|
67
|
+
const handleButtonMove = (fromIndex, toIndex) => {
|
|
68
|
+
if (toIndex < 0 || toIndex >= sortedItems.length) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const fromEl = itemRefs.current[fromIndex];
|
|
72
|
+
const toEl = itemRefs.current[toIndex];
|
|
73
|
+
if (!fromEl || !toEl) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const stride = Math.abs(toEl.getBoundingClientRect().top - fromEl.getBoundingClientRect().top);
|
|
77
|
+
const direction = toIndex > fromIndex ? 1 : -1;
|
|
78
|
+
buttonFlipRef.current = {
|
|
79
|
+
fromIndex,
|
|
80
|
+
toIndex,
|
|
81
|
+
stride,
|
|
82
|
+
direction
|
|
83
|
+
};
|
|
84
|
+
const reordered = [...sortedItems];
|
|
85
|
+
const [moved] = reordered.splice(fromIndex, 1);
|
|
86
|
+
reordered.splice(toIndex, 0, moved);
|
|
87
|
+
setSortedItems(reordered);
|
|
88
|
+
setButtonMoveId(id => id + 1);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* FLIP Invert+Play step: runs synchronously after React commits the reordered
|
|
93
|
+
* DOM. Animate each swapped item from its previous position to its new one
|
|
94
|
+
* using inline CSS transitions (no CSS classes, to avoid creating a new
|
|
95
|
+
* containing block that would break position:fixed on the Draggable clone).
|
|
96
|
+
*/
|
|
97
|
+
useLayoutEffect(() => {
|
|
98
|
+
const flipData = buttonFlipRef.current;
|
|
99
|
+
if (!flipData) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
buttonFlipRef.current = null;
|
|
103
|
+
const {
|
|
104
|
+
fromIndex,
|
|
105
|
+
toIndex,
|
|
106
|
+
stride,
|
|
107
|
+
direction
|
|
108
|
+
} = flipData;
|
|
109
|
+
const startAnimation = (el, startY) => {
|
|
110
|
+
if (!el) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// Snap to the starting position with no transition.
|
|
114
|
+
el.style.transition = 'none';
|
|
115
|
+
el.style.transform = `translateY(${startY}px)`;
|
|
116
|
+
// Force a reflow so the browser registers the starting position
|
|
117
|
+
// before we switch to the animated transition.
|
|
118
|
+
el.getBoundingClientRect();
|
|
119
|
+
// Animate from startY back to 0 (its new natural position).
|
|
120
|
+
el.style.transition = `transform ${BUTTON_MOVE_DURATION}ms ease-out`;
|
|
121
|
+
el.style.transform = '';
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Moved item is now at toIndex: animate it in from its old position.
|
|
125
|
+
startAnimation(itemRefs.current[toIndex], -direction * stride);
|
|
126
|
+
// Displaced item is now at fromIndex: animate it in from its old position.
|
|
127
|
+
startAnimation(itemRefs.current[fromIndex], direction * stride);
|
|
128
|
+
if (buttonMoveTimer.current) {
|
|
129
|
+
clearTimeout(buttonMoveTimer.current);
|
|
130
|
+
}
|
|
131
|
+
buttonMoveTimer.current = setTimeout(() => {
|
|
132
|
+
// Clear inline animation styles before calling the external callback so
|
|
133
|
+
// the subsequent React re-render doesn't fight a live animation.
|
|
134
|
+
[fromIndex, toIndex].forEach(i => {
|
|
135
|
+
const el = itemRefs.current[i];
|
|
136
|
+
if (el) {
|
|
137
|
+
el.style.transition = '';
|
|
138
|
+
el.style.transform = '';
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Move focus to the matching chevron button on the card at toIndex.
|
|
143
|
+
// Focus before onDragCallback so a parent rerender can't steal focus.
|
|
144
|
+
const targetEl = itemRefs.current[toIndex];
|
|
145
|
+
if (targetEl) {
|
|
146
|
+
const moveButtons = targetEl.querySelectorAll('.newspack-card--core__header__draggable-controls__move-buttons button');
|
|
147
|
+
// direction > 0 = moved down → prefer the down button (index 1).
|
|
148
|
+
// direction < 0 = moved up → prefer the up button (index 0).
|
|
149
|
+
const preferred = direction > 0 ? moveButtons[1] : moveButtons[0];
|
|
150
|
+
const fallback = direction > 0 ? moveButtons[0] : moveButtons[1];
|
|
151
|
+
if (preferred && !preferred.disabled) {
|
|
152
|
+
preferred.focus();
|
|
153
|
+
} else if (fallback && !fallback.disabled) {
|
|
154
|
+
fallback.focus();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
onDragCallback(fromIndex, toIndex);
|
|
158
|
+
buttonMoveTimer.current = null;
|
|
159
|
+
}, BUTTON_MOVE_DURATION);
|
|
160
|
+
}, [buttonMoveId]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
161
|
+
|
|
162
|
+
const handleDragStart = index => {
|
|
163
|
+
// Cancel any in-progress button-move animation so no item wrapper keeps
|
|
164
|
+
// an inline transform. A transformed ancestor breaks position:fixed on
|
|
165
|
+
// the Draggable clone (CSS spec: fixed positioning is relative to the
|
|
166
|
+
// nearest ancestor with a transform/filter/perspective).
|
|
167
|
+
if (buttonMoveTimer.current) {
|
|
168
|
+
clearTimeout(buttonMoveTimer.current);
|
|
169
|
+
buttonMoveTimer.current = null;
|
|
170
|
+
itemRefs.current.forEach(el => {
|
|
171
|
+
if (el) {
|
|
172
|
+
el.style.transition = '';
|
|
173
|
+
el.style.transform = '';
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
const listEl = listRef.current;
|
|
178
|
+
if (listEl) {
|
|
179
|
+
var _rects$index$height;
|
|
180
|
+
const itemEls = itemRefs.current;
|
|
181
|
+
const rects = itemEls.map(el => el?.getBoundingClientRect());
|
|
182
|
+
|
|
183
|
+
// Stride = item height + gap to next item (difference between consecutive tops).
|
|
184
|
+
const itemStrides = rects.map((rect, i) => {
|
|
185
|
+
if (!rect) {
|
|
186
|
+
return 0;
|
|
187
|
+
}
|
|
188
|
+
const nextRect = rects[i + 1];
|
|
189
|
+
return nextRect ? nextRect.top - rect.top : rect.height;
|
|
190
|
+
});
|
|
191
|
+
setMeasurements({
|
|
192
|
+
lockedHeight: listEl.getBoundingClientRect().height,
|
|
193
|
+
sourceHeight: (_rects$index$height = rects[index]?.height) !== null && _rects$index$height !== void 0 ? _rects$index$height : 0,
|
|
194
|
+
itemStrides,
|
|
195
|
+
itemTops: rects.map(rect => {
|
|
196
|
+
var _rect$top;
|
|
197
|
+
return (_rect$top = rect?.top) !== null && _rect$top !== void 0 ? _rect$top : 0;
|
|
198
|
+
})
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
// Make the entire document a valid drop target so the browser skips
|
|
202
|
+
// its snap-back animation when the cursor is released outside the list.
|
|
203
|
+
const preventSnapback = e => e.preventDefault();
|
|
204
|
+
document.addEventListener('dragover', preventSnapback);
|
|
205
|
+
documentDragOverRef.current = preventSnapback;
|
|
206
|
+
setDraggingIndex(index);
|
|
207
|
+
setDroppedIndex(null);
|
|
208
|
+
};
|
|
209
|
+
const clearDragState = () => {
|
|
210
|
+
if (documentDragOverRef.current) {
|
|
211
|
+
document.removeEventListener('dragover', documentDragOverRef.current);
|
|
212
|
+
documentDragOverRef.current = null;
|
|
213
|
+
}
|
|
214
|
+
setDraggingIndex(null);
|
|
215
|
+
setHoverIndex(null);
|
|
216
|
+
setMeasurements(null);
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Determine the drop target index from cursor coordinates and the item
|
|
221
|
+
* top positions snapshotted at drag start. Mirrors the midpoint logic
|
|
222
|
+
* previously used in getDropIndex but works without a live event target.
|
|
223
|
+
*/
|
|
224
|
+
const getDropIndexFromCursor = (clientY, m, sourceIndex) => {
|
|
225
|
+
const {
|
|
226
|
+
itemTops,
|
|
227
|
+
itemStrides,
|
|
228
|
+
sourceHeight
|
|
229
|
+
} = m;
|
|
230
|
+
for (let i = 0; i < itemTops.length; i++) {
|
|
231
|
+
const movingDown = sourceIndex > i;
|
|
232
|
+
const midpoint = itemTops[i] + (movingDown ? itemStrides[i] : itemStrides[i] - sourceHeight);
|
|
233
|
+
if (clientY < midpoint) {
|
|
234
|
+
return i;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return itemTops.length; // below all items
|
|
238
|
+
};
|
|
239
|
+
const handleDragEnd = event => {
|
|
240
|
+
// Take a local copy of measurements before clearDragState nulls it.
|
|
241
|
+
const m = measurements;
|
|
242
|
+
const sourceIndex = draggingIndex;
|
|
243
|
+
clearDragState();
|
|
244
|
+
if (m === null || sourceIndex === null) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const dropIndex = getDropIndexFromCursor(event.clientY, m, sourceIndex);
|
|
248
|
+
|
|
249
|
+
// Compute the destination index in the post-removal array.
|
|
250
|
+
// After splicing out the dragged item, indices above it shift down by one.
|
|
251
|
+
const insertIndex = dropIndex > sourceIndex ? dropIndex - 1 : dropIndex;
|
|
252
|
+
if (insertIndex === sourceIndex) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const reordered = [...sortedItems];
|
|
256
|
+
const [moved] = reordered.splice(sourceIndex, 1);
|
|
257
|
+
reordered.splice(insertIndex, 0, moved);
|
|
258
|
+
|
|
259
|
+
// First render: show items in their new order with no animation.
|
|
260
|
+
// The dragging CSS classes are already cleared by clearDragState() above,
|
|
261
|
+
// so no transitions will fire on this paint.
|
|
262
|
+
setSortedItems(reordered);
|
|
263
|
+
|
|
264
|
+
// Next frame: trigger the drop animation on the newly-positioned item,
|
|
265
|
+
// then call onDragCallback after the animation completes.
|
|
266
|
+
requestAnimationFrame(() => {
|
|
267
|
+
setDroppedIndex(insertIndex);
|
|
268
|
+
dropAnimationTimer.current = setTimeout(() => {
|
|
269
|
+
setDroppedIndex(null);
|
|
270
|
+
onDragCallback(sourceIndex, insertIndex);
|
|
271
|
+
}, DROP_ANIMATION_DURATION);
|
|
272
|
+
});
|
|
273
|
+
};
|
|
274
|
+
const handleDragOver = (event, index) => {
|
|
275
|
+
event.preventDefault();
|
|
276
|
+
if (!measurements) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
280
|
+
const midpoint = rect.top + rect.height / 2;
|
|
281
|
+
setHoverIndex(event.clientY < midpoint ? index : index + 1);
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Compute the translateY for a non-source item at `index` given the current
|
|
286
|
+
* draggingIndex and hoverIndex, so that items visually shift to show the gap
|
|
287
|
+
* while the container's layout height stays fixed.
|
|
288
|
+
*
|
|
289
|
+
* The source item has been visually hidden but still occupies its layout slot.
|
|
290
|
+
* Items between the source and the hover position need to slide by the source
|
|
291
|
+
* item's stride (height + gap) to either fill the vacated source slot or make
|
|
292
|
+
* room for the incoming gap at the hover position.
|
|
293
|
+
*/
|
|
294
|
+
const getTranslateY = index => {
|
|
295
|
+
var _itemStrides$dragging;
|
|
296
|
+
if (draggingIndex === null || hoverIndex === null || !measurements) {
|
|
297
|
+
return 0;
|
|
298
|
+
}
|
|
299
|
+
if (index === draggingIndex) {
|
|
300
|
+
return 0;
|
|
301
|
+
}
|
|
302
|
+
const {
|
|
303
|
+
itemStrides,
|
|
304
|
+
sourceHeight
|
|
305
|
+
} = measurements;
|
|
306
|
+
const sourceStride = (_itemStrides$dragging = itemStrides[draggingIndex]) !== null && _itemStrides$dragging !== void 0 ? _itemStrides$dragging : sourceHeight;
|
|
307
|
+
if (hoverIndex > draggingIndex) {
|
|
308
|
+
// Dragging downward: items strictly between source and hover position slide up.
|
|
309
|
+
if (index > draggingIndex && index < hoverIndex) {
|
|
310
|
+
return -sourceStride;
|
|
311
|
+
}
|
|
312
|
+
} else if (index >= hoverIndex && index < draggingIndex) {
|
|
313
|
+
// Dragging upward: items between hover position and source slide down.
|
|
314
|
+
return sourceStride;
|
|
315
|
+
}
|
|
316
|
+
return 0;
|
|
317
|
+
};
|
|
318
|
+
const isDragging = draggingIndex !== null;
|
|
319
|
+
return /*#__PURE__*/_jsx(VStack, {
|
|
320
|
+
ref: listRef,
|
|
321
|
+
className: classNames('newspack-card--core--sortable-list', disabled && 'newspack-card--core--sortable-list__is-disabled', isDragging && 'newspack-card--core--sortable-list__is-dragging'),
|
|
322
|
+
style: measurements ? {
|
|
323
|
+
height: measurements.lockedHeight
|
|
324
|
+
} : undefined,
|
|
325
|
+
spacing: 4,
|
|
326
|
+
children: sortedItems.map((item, index) => {
|
|
327
|
+
const translateY = getTranslateY(index);
|
|
328
|
+
return /*#__PURE__*/_jsx(Disabled, {
|
|
329
|
+
isDisabled: disabled,
|
|
330
|
+
children: /*#__PURE__*/_jsx("div", {
|
|
331
|
+
ref: el => {
|
|
332
|
+
itemRefs.current[index] = el;
|
|
333
|
+
},
|
|
334
|
+
className: classNames('newspack-card--core--sortable-list__item', {
|
|
335
|
+
'is-source': draggingIndex === index,
|
|
336
|
+
'is-dropped': droppedIndex === index
|
|
337
|
+
}),
|
|
338
|
+
style: translateY ? {
|
|
339
|
+
transform: `translateY(${translateY}px)`
|
|
340
|
+
} : {
|
|
341
|
+
transition: !isDragging ? 'none' : undefined
|
|
342
|
+
},
|
|
343
|
+
id: `draggable-card-${index}`,
|
|
344
|
+
onDragOver: e => handleDragOver(e, index),
|
|
345
|
+
children: /*#__PURE__*/_jsx(Draggable, {
|
|
346
|
+
transferData: {},
|
|
347
|
+
cloneClassname: "newspack-card--core--sortable-list__item__clone",
|
|
348
|
+
elementId: `draggable-card-${index}`,
|
|
349
|
+
onDragStart: () => handleDragStart(index),
|
|
350
|
+
onDragEnd: handleDragEnd,
|
|
351
|
+
appendToOwnerDocument: true,
|
|
352
|
+
children: ({
|
|
353
|
+
onDraggableStart,
|
|
354
|
+
onDraggableEnd
|
|
355
|
+
}) => /*#__PURE__*/_jsx(Card, {
|
|
356
|
+
isSmall: true,
|
|
357
|
+
draggable: true,
|
|
358
|
+
onDragStart: onDraggableStart,
|
|
359
|
+
onDragEnd: onDraggableEnd,
|
|
360
|
+
__experimentalCoreCard: true,
|
|
361
|
+
__experimentalCoreProps: {
|
|
362
|
+
header: /*#__PURE__*/_jsxs("h3", {
|
|
363
|
+
children: [item.title, /*#__PURE__*/_jsx(Badge, {
|
|
364
|
+
level: item.badgeLevel,
|
|
365
|
+
text: item.badgeText
|
|
366
|
+
})]
|
|
367
|
+
}),
|
|
368
|
+
isDraggable: true,
|
|
369
|
+
isFirstTarget: index === 0,
|
|
370
|
+
isLastTarget: index === sortedItems.length - 1,
|
|
371
|
+
dragIndex: index,
|
|
372
|
+
onDragCallback: handleButtonMove
|
|
373
|
+
}
|
|
374
|
+
})
|
|
375
|
+
})
|
|
376
|
+
})
|
|
377
|
+
}, item.id);
|
|
378
|
+
})
|
|
379
|
+
});
|
|
380
|
+
};
|
|
381
|
+
export default CardSortableList;
|