@siratik/wp-component-library 0.1.1 → 0.1.2
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/LICENSE +5 -0
- package/README.md +43 -5
- package/components/attachment-image/README.md +118 -0
- package/components/attachment-image/index.js +105 -0
- package/components/attributes-panel/README.md +92 -0
- package/components/attributes-panel/index.js +324 -0
- package/components/badge/README.md +61 -0
- package/components/badge/index.js +105 -0
- package/components/block-appender/README.md +158 -0
- package/components/block-appender/index.js +135 -0
- package/components/block-extension/index.js +214 -0
- package/components/business-hours/SpecialHoursControl.js +211 -0
- package/components/business-hours/WeeklyHoursControl.js +419 -0
- package/components/business-hours/index.js +8 -0
- package/components/button/README.md +172 -0
- package/components/button/index.js +84 -0
- package/components/captcha-placeholder/CloudflareTurnstile.js +111 -0
- package/components/captcha-placeholder/GoogleRecaptcha.js +132 -0
- package/components/captcha-placeholder/HCaptcha.js +99 -0
- package/components/captcha-placeholder/README.md +228 -0
- package/components/captcha-placeholder/index.js +39 -0
- package/components/captcha-placeholder/styles.js +33 -0
- package/components/geocoder/AddressEntryControl.js +390 -0
- package/components/geocoder/GeocoderSearchControl.js +353 -0
- package/components/geocoder/countryOptions.js +66 -0
- package/components/geocoder/geocoder.js +400 -0
- package/components/geocoder/index.js +16 -0
- package/components/icon-picker/Icon.js +53 -0
- package/components/icon-picker/IconPicker.js +70 -0
- package/components/icon-picker/IconPickerModal.js +474 -0
- package/components/icon-picker/IconPickerToolbarButton.js +60 -0
- package/components/icon-picker/InlineIconPicker.js +92 -0
- package/components/icon-picker/api.js +181 -0
- package/components/icon-picker/config-bootstrap.js +108 -0
- package/components/icon-picker/hooks.js +157 -0
- package/components/icon-picker/index.js +39 -0
- package/components/icon-picker/load-script.js +33 -0
- package/components/icon-picker/loaders.js +79 -0
- package/components/icon-picker/store.js +223 -0
- package/components/icon-picker/styles.js +232 -0
- package/components/icon-picker/utils.js +138 -0
- package/components/layout/README.md +268 -0
- package/components/link-tools-panel/README.md +49 -0
- package/components/link-tools-panel/index.js +198 -0
- package/components/link-tools-panel/styles.js +90 -0
- package/components/media/README.md +288 -0
- package/components/media/SettingsMediaUpload.js +187 -0
- package/components/media/editor-media-upload.js +65 -0
- package/components/media/image-controls.js +196 -0
- package/components/media/index.js +13 -0
- package/components/media/inspector-media-upload.js +290 -0
- package/components/media/toolbar-media-upload.js +53 -0
- package/components/media/utils/aspect-ratios.js +118 -0
- package/components/meta/MetaPanel.js +75 -0
- package/components/meta/README.md +46 -0
- package/components/meta/index.js +6 -0
- package/components/meta/meta-gated-editor.js +282 -0
- package/components/query/README.md +340 -0
- package/components/query/cardTemplatePartPanel.js +69 -0
- package/components/query/cardTemplatePartSelect.js +165 -0
- package/components/query/cardTemplatePartUtils.js +119 -0
- package/components/query/columnCountControl.js +63 -0
- package/components/query/displayTypeSelect.js +52 -0
- package/components/query/examples.js +166 -0
- package/components/query/hideCurrentPostControl.js +47 -0
- package/components/query/index.js +20 -0
- package/components/query/manualPostSelector.js +125 -0
- package/components/query/manualTermSelector.js +141 -0
- package/components/query/orderBySelect.js +95 -0
- package/components/query/postTypeSelect.js +67 -0
- package/components/query/postsPerPageControl.js +51 -0
- package/components/query/selectionModeControl.js +61 -0
- package/components/query/taxonomySelect.js +139 -0
- package/components/query/utils.js +75 -0
- package/components/repeater/README.md +159 -0
- package/components/repeater/index.js +829 -0
- package/components/segmented-bar/README.md +30 -0
- package/components/segmented-bar/index.js +68 -0
- package/components/sortable-select/README.md +153 -0
- package/components/sortable-select/custom-sortable-token.js +127 -0
- package/components/sortable-select/index.js +693 -0
- package/components/style-picker/README.md +36 -0
- package/components/style-picker/index.js +92 -0
- package/components/variable-field/README.md +299 -0
- package/components/variable-field/index.js +592 -0
- package/components/variable-field/value-to-chip-html.js +38 -0
- package/components/variable-inserter/README.md +68 -0
- package/components/variable-inserter/index.js +168 -0
- package/constants/statusColors.js +1 -1
- package/editor.js +16 -0
- package/index.js +127 -0
- package/package.json +21 -13
- package/utils/kitaris-localize.js +128 -0
- package/utils/slug-aware-color-setter.js +27 -0
- package/build/index.asset.php +0 -20
- package/build/index.js +0 -1242
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Weekly hours editor: one row per day (closed, 24 hours, or open–close
|
|
3
|
+
* times), plus optional Open 24/7 and 24-hour format toggles. Controlled:
|
|
4
|
+
* `hours` uses the organization hours shape and `onHoursChange` receives the
|
|
5
|
+
* whole next object.
|
|
6
|
+
*
|
|
7
|
+
* @package Siratik/WP-Component-Library
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import styled from "@emotion/styled";
|
|
11
|
+
import {
|
|
12
|
+
Flex,
|
|
13
|
+
FlexItem,
|
|
14
|
+
PanelRow,
|
|
15
|
+
TimePicker,
|
|
16
|
+
ToggleControl,
|
|
17
|
+
} from "@wordpress/components";
|
|
18
|
+
import { Fragment, useEffect, useState } from "@wordpress/element";
|
|
19
|
+
import { __ } from "@wordpress/i18n";
|
|
20
|
+
|
|
21
|
+
const StyledWeeklyHours = styled.div`
|
|
22
|
+
.business-hours {
|
|
23
|
+
align-items: center;
|
|
24
|
+
position: relative;
|
|
25
|
+
padding-bottom: 0.75rem;
|
|
26
|
+
padding-top: 0.75rem;
|
|
27
|
+
|
|
28
|
+
&:before {
|
|
29
|
+
content: "";
|
|
30
|
+
border-top: 1px dashed var(--wpds-color-stroke-surface-neutral, rgb(217, 219, 222));
|
|
31
|
+
position: absolute;
|
|
32
|
+
width: 100%;
|
|
33
|
+
top: -5px;
|
|
34
|
+
left: 0;
|
|
35
|
+
right: 0;
|
|
36
|
+
margin: 0 auto;
|
|
37
|
+
}
|
|
38
|
+
&:first-of-type {
|
|
39
|
+
&:before {
|
|
40
|
+
display: none;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
&:last-of-type {
|
|
45
|
+
&:after {
|
|
46
|
+
content: "";
|
|
47
|
+
border-bottom: 1px dashed var(--wpds-color-stroke-surface-neutral, rgb(217, 219, 222));
|
|
48
|
+
position: absolute;
|
|
49
|
+
width: 100%;
|
|
50
|
+
bottom: -5px;
|
|
51
|
+
left: 0;
|
|
52
|
+
right: 0;
|
|
53
|
+
margin: 0 auto;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
&__time-group {
|
|
58
|
+
display: flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
justify-content: flex-end;
|
|
61
|
+
margin: 0 !important;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
&__day {
|
|
65
|
+
min-width: 80px;
|
|
66
|
+
font-weight: 600;
|
|
67
|
+
margin-bottom: 0;
|
|
68
|
+
|
|
69
|
+
@media (max-width: 768px) {
|
|
70
|
+
min-width: 60px;
|
|
71
|
+
font-size: 0.875rem;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
&__time {
|
|
76
|
+
width: 80px;
|
|
77
|
+
|
|
78
|
+
@media (max-width: 768px) {
|
|
79
|
+
width: auto;
|
|
80
|
+
min-width: 70px;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
&__am-pm {
|
|
85
|
+
width: 60px;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
&__dash {
|
|
89
|
+
padding-left: 0.5rem;
|
|
90
|
+
padding-right: 0.5rem;
|
|
91
|
+
|
|
92
|
+
@media (max-width: 768px) {
|
|
93
|
+
padding-left: 0.25rem;
|
|
94
|
+
padding-right: 0.25rem;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.components-toggle-control {
|
|
99
|
+
padding-left: 1rem;
|
|
100
|
+
|
|
101
|
+
@media (max-width: 768px) {
|
|
102
|
+
padding-left: 0.5rem;
|
|
103
|
+
|
|
104
|
+
.components-form-toggle__label {
|
|
105
|
+
font-size: 0.875rem;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
&__toggle-closed,
|
|
111
|
+
&__toggle-24hrs {
|
|
112
|
+
@media (max-width: 480px) {
|
|
113
|
+
min-width: auto !important;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.components-flex {
|
|
118
|
+
@media (max-width: 576px) {
|
|
119
|
+
flex-wrap: wrap;
|
|
120
|
+
gap: 0.5rem !important;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.components-flex-item {
|
|
125
|
+
@media (max-width: 576px) {
|
|
126
|
+
&:first-of-type {
|
|
127
|
+
width: 100%;
|
|
128
|
+
margin-bottom: 0.5rem;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
&:last-child {
|
|
133
|
+
@media (max-width: 480px) {
|
|
134
|
+
width: 100%;
|
|
135
|
+
min-width: auto !important;
|
|
136
|
+
|
|
137
|
+
.components-flex {
|
|
138
|
+
justify-content: flex-start !important;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.components-datetime__time-input {
|
|
145
|
+
@media (max-width: 480px) {
|
|
146
|
+
font-size: 0.875rem;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
`;
|
|
151
|
+
|
|
152
|
+
const DAYS_OF_WEEK = [
|
|
153
|
+
"monday",
|
|
154
|
+
"tuesday",
|
|
155
|
+
"wednesday",
|
|
156
|
+
"thursday",
|
|
157
|
+
"friday",
|
|
158
|
+
"saturday",
|
|
159
|
+
"sunday",
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Individual Day Hours Component
|
|
164
|
+
*/
|
|
165
|
+
function DayHours({ day, dayHours, updateHours, is24HourFormat = false }) {
|
|
166
|
+
const capitalizedDay = day.charAt(0).toUpperCase() + day.slice(1);
|
|
167
|
+
|
|
168
|
+
// Convert 24-hour time string to time object for TimePicker
|
|
169
|
+
const timeStringToTimeObject = (timeString) => {
|
|
170
|
+
if (!timeString) {
|
|
171
|
+
return { hours: 9, minutes: 0 };
|
|
172
|
+
}
|
|
173
|
+
const [hours, minutes] = timeString.split(":");
|
|
174
|
+
return {
|
|
175
|
+
hours: parseInt(hours),
|
|
176
|
+
minutes: parseInt(minutes),
|
|
177
|
+
};
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Convert TimePicker time object to 24-hour format string
|
|
181
|
+
const timeObjectToTimeString = (timeObject) => {
|
|
182
|
+
if (
|
|
183
|
+
!timeObject ||
|
|
184
|
+
typeof timeObject.hours !== "number" ||
|
|
185
|
+
typeof timeObject.minutes !== "number"
|
|
186
|
+
) {
|
|
187
|
+
return "09:00";
|
|
188
|
+
}
|
|
189
|
+
const hours = timeObject.hours.toString().padStart(2, "0");
|
|
190
|
+
const minutes = timeObject.minutes.toString().padStart(2, "0");
|
|
191
|
+
return `${hours}:${minutes}`;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// Get current times - always use 24-hour format for storage
|
|
195
|
+
const currentOpenTime = dayHours.open_time || "09:00";
|
|
196
|
+
const currentCloseTime = dayHours.close_time || "17:00";
|
|
197
|
+
|
|
198
|
+
// State for TimePicker (always 24-hour format internally)
|
|
199
|
+
const [openTime, setOpenTime] = useState(() =>
|
|
200
|
+
timeStringToTimeObject(currentOpenTime),
|
|
201
|
+
);
|
|
202
|
+
const [closeTime, setCloseTime] = useState(() =>
|
|
203
|
+
timeStringToTimeObject(currentCloseTime),
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
// Update state when props change
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
setOpenTime(timeStringToTimeObject(dayHours.open_time || "09:00"));
|
|
209
|
+
setCloseTime(timeStringToTimeObject(dayHours.close_time || "17:00"));
|
|
210
|
+
}, [dayHours.open_time, dayHours.close_time]);
|
|
211
|
+
|
|
212
|
+
const handleOpenTimeChange = (newTimeObject) => {
|
|
213
|
+
setOpenTime(newTimeObject);
|
|
214
|
+
const timeString = timeObjectToTimeString(newTimeObject);
|
|
215
|
+
updateHours(day, "open_time", timeString);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const handleCloseTimeChange = (newTimeObject) => {
|
|
219
|
+
setCloseTime(newTimeObject);
|
|
220
|
+
const timeString = timeObjectToTimeString(newTimeObject);
|
|
221
|
+
updateHours(day, "close_time", timeString);
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
let dayStatus;
|
|
225
|
+
if (dayHours.closed) {
|
|
226
|
+
dayStatus = (
|
|
227
|
+
<Flex justify="flex-end">
|
|
228
|
+
<span
|
|
229
|
+
style={{
|
|
230
|
+
color: "#757575",
|
|
231
|
+
fontStyle: "italic",
|
|
232
|
+
}}
|
|
233
|
+
>
|
|
234
|
+
{__("Closed all day", "wp-component-library")}
|
|
235
|
+
</span>
|
|
236
|
+
</Flex>
|
|
237
|
+
);
|
|
238
|
+
} else if (dayHours.open_24_hours) {
|
|
239
|
+
dayStatus = (
|
|
240
|
+
<Flex justify="flex-end">
|
|
241
|
+
<span
|
|
242
|
+
style={{
|
|
243
|
+
color: "#757575",
|
|
244
|
+
}}
|
|
245
|
+
>
|
|
246
|
+
{__("Open 24 hours", "wp-component-library")}
|
|
247
|
+
</span>
|
|
248
|
+
</Flex>
|
|
249
|
+
);
|
|
250
|
+
} else {
|
|
251
|
+
dayStatus = (
|
|
252
|
+
<Flex justify="flex-end" align="center" gap="2">
|
|
253
|
+
<TimePicker.TimeInput
|
|
254
|
+
key={`${day}-open-${is24HourFormat}`}
|
|
255
|
+
className="business-hours__time business-hours__time--open"
|
|
256
|
+
value={openTime}
|
|
257
|
+
onChange={handleOpenTimeChange}
|
|
258
|
+
is12Hour={!is24HourFormat}
|
|
259
|
+
/>
|
|
260
|
+
<span className="business-hours__dash">–</span>
|
|
261
|
+
<TimePicker.TimeInput
|
|
262
|
+
key={`${day}-close-${is24HourFormat}`}
|
|
263
|
+
className="business-hours__time business-hours__time--close"
|
|
264
|
+
value={closeTime}
|
|
265
|
+
onChange={handleCloseTimeChange}
|
|
266
|
+
is12Hour={!is24HourFormat}
|
|
267
|
+
/>
|
|
268
|
+
</Flex>
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return (
|
|
273
|
+
<PanelRow className="business-hours">
|
|
274
|
+
<Flex align="center" gap="3" style={{ width: "100%" }}>
|
|
275
|
+
<FlexItem style={{ minWidth: "80px" }}>
|
|
276
|
+
<p className="business-hours__day">{capitalizedDay}</p>
|
|
277
|
+
</FlexItem>
|
|
278
|
+
|
|
279
|
+
<FlexItem
|
|
280
|
+
style={{ minWidth: "60px" }}
|
|
281
|
+
className="business-hours__toggle-closed"
|
|
282
|
+
>
|
|
283
|
+
<ToggleControl
|
|
284
|
+
__nextHasNoMarginBottom
|
|
285
|
+
className="business-hours__closed"
|
|
286
|
+
label={__("Closed", "wp-component-library")}
|
|
287
|
+
checked={dayHours.closed ?? false}
|
|
288
|
+
onChange={(newValue) =>
|
|
289
|
+
updateHours(day, "closed", newValue)
|
|
290
|
+
}
|
|
291
|
+
/>
|
|
292
|
+
</FlexItem>
|
|
293
|
+
|
|
294
|
+
<FlexItem
|
|
295
|
+
style={{ minWidth: "60px" }}
|
|
296
|
+
className="business-hours__toggle-24hrs"
|
|
297
|
+
>
|
|
298
|
+
{!dayHours.closed && (
|
|
299
|
+
<ToggleControl
|
|
300
|
+
__nextHasNoMarginBottom
|
|
301
|
+
className="business-hours__AlwaysOpen"
|
|
302
|
+
label={__("24 Hrs", "wp-component-library")}
|
|
303
|
+
checked={dayHours.open_24_hours ?? false}
|
|
304
|
+
onChange={(newValue) =>
|
|
305
|
+
updateHours(day, "open_24_hours", newValue)
|
|
306
|
+
}
|
|
307
|
+
/>
|
|
308
|
+
)}
|
|
309
|
+
</FlexItem>
|
|
310
|
+
|
|
311
|
+
<FlexItem style={{ flex: 1, minWidth: "200px" }}>
|
|
312
|
+
{dayStatus}
|
|
313
|
+
</FlexItem>
|
|
314
|
+
</Flex>
|
|
315
|
+
</PanelRow>
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Weekly schedule UI + optional branch-only toggles (24/7, time format).
|
|
321
|
+
* Use with controlled `hours` / `onHoursChange` for primary hours body or extra locations.
|
|
322
|
+
*
|
|
323
|
+
* @param {Object} props
|
|
324
|
+
* @param {Object} props.hours Hours blob (same shape as organization `hours`).
|
|
325
|
+
* @param {Function} props.onHoursChange Full hours object updater.
|
|
326
|
+
* @param {boolean} props.includeSecondaryToggles Show Open 24/7 + 24h format (extra locations modal).
|
|
327
|
+
*/
|
|
328
|
+
export function WeeklyHoursControl({
|
|
329
|
+
hours,
|
|
330
|
+
onHoursChange,
|
|
331
|
+
includeSecondaryToggles = false,
|
|
332
|
+
className = "",
|
|
333
|
+
}) {
|
|
334
|
+
const businessHours = hours || {};
|
|
335
|
+
const alwaysOpen = businessHours.always_open ?? false;
|
|
336
|
+
const is24HourFormat = businessHours.is_24_hour_format ?? false;
|
|
337
|
+
|
|
338
|
+
const updateHours = (day, field, value) => {
|
|
339
|
+
onHoursChange({
|
|
340
|
+
...businessHours,
|
|
341
|
+
[day]: {
|
|
342
|
+
...(businessHours?.[day] || {}),
|
|
343
|
+
[field]: value,
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const updateGlobalSetting = (field, value) => {
|
|
349
|
+
onHoursChange({
|
|
350
|
+
...businessHours,
|
|
351
|
+
[field]: value,
|
|
352
|
+
});
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
return (
|
|
356
|
+
<StyledWeeklyHours className={`weekly-hours-control business-hours-schedule-form ${className}`.trim()}>
|
|
357
|
+
{includeSecondaryToggles ? (
|
|
358
|
+
<Flex
|
|
359
|
+
gap="2rem"
|
|
360
|
+
align="center"
|
|
361
|
+
wrap
|
|
362
|
+
style={{ marginBottom: "1rem" }}
|
|
363
|
+
>
|
|
364
|
+
<ToggleControl
|
|
365
|
+
__nextHasNoMarginBottom
|
|
366
|
+
label={__("Open 24/7", "wp-component-library")}
|
|
367
|
+
checked={alwaysOpen}
|
|
368
|
+
onChange={(newValue) =>
|
|
369
|
+
updateGlobalSetting("always_open", newValue)
|
|
370
|
+
}
|
|
371
|
+
/>
|
|
372
|
+
<ToggleControl
|
|
373
|
+
__nextHasNoMarginBottom
|
|
374
|
+
label={__("24-hour format", "wp-component-library")}
|
|
375
|
+
checked={is24HourFormat}
|
|
376
|
+
onChange={(newValue) =>
|
|
377
|
+
updateGlobalSetting(
|
|
378
|
+
"is_24_hour_format",
|
|
379
|
+
newValue,
|
|
380
|
+
)
|
|
381
|
+
}
|
|
382
|
+
/>
|
|
383
|
+
</Flex>
|
|
384
|
+
) : null}
|
|
385
|
+
|
|
386
|
+
{alwaysOpen ? (
|
|
387
|
+
<div
|
|
388
|
+
style={{
|
|
389
|
+
textAlign: "center",
|
|
390
|
+
padding: "2rem",
|
|
391
|
+
color: "#757575",
|
|
392
|
+
}}
|
|
393
|
+
>
|
|
394
|
+
<p>
|
|
395
|
+
{__(
|
|
396
|
+
"Hours are set to open 24/7 all days.",
|
|
397
|
+
"wp-component-library",
|
|
398
|
+
)}
|
|
399
|
+
</p>
|
|
400
|
+
</div>
|
|
401
|
+
) : (
|
|
402
|
+
<Fragment>
|
|
403
|
+
{DAYS_OF_WEEK.map((day) => (
|
|
404
|
+
<Fragment key={day}>
|
|
405
|
+
<DayHours
|
|
406
|
+
day={day}
|
|
407
|
+
dayHours={businessHours?.[day] || {}}
|
|
408
|
+
updateHours={updateHours}
|
|
409
|
+
is24HourFormat={is24HourFormat}
|
|
410
|
+
/>
|
|
411
|
+
</Fragment>
|
|
412
|
+
))}
|
|
413
|
+
</Fragment>
|
|
414
|
+
)}
|
|
415
|
+
</StyledWeeklyHours>
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export default WeeklyHoursControl;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Button Component
|
|
2
|
+
|
|
3
|
+
A frontend button component for rendering styled buttons with accessibility features. This component provides a consistent button implementation with support for different sizes, styles, and screen reader text.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Multiple button styles and sizes
|
|
8
|
+
- Screen reader support for accessibility
|
|
9
|
+
- BEM-style CSS class generation
|
|
10
|
+
- Flexible wrapper attributes
|
|
11
|
+
- Link target support for external links
|
|
12
|
+
- Consistent styling system
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
```jsx
|
|
17
|
+
import { Button } from '@siratik/wp-component-library/components/button';
|
|
18
|
+
|
|
19
|
+
// Basic button
|
|
20
|
+
<Button
|
|
21
|
+
text="Click Me"
|
|
22
|
+
link="/about"
|
|
23
|
+
/>
|
|
24
|
+
|
|
25
|
+
// Styled button with size variant
|
|
26
|
+
<Button
|
|
27
|
+
text="Get Started"
|
|
28
|
+
link="/signup"
|
|
29
|
+
style="primary"
|
|
30
|
+
size="large"
|
|
31
|
+
className="cta-button"
|
|
32
|
+
/>
|
|
33
|
+
|
|
34
|
+
// External link with screen reader text
|
|
35
|
+
<Button
|
|
36
|
+
text="View Report"
|
|
37
|
+
link="https://example.com/report"
|
|
38
|
+
target="_blank"
|
|
39
|
+
screenReader="Opens in new window"
|
|
40
|
+
/>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Props
|
|
44
|
+
|
|
45
|
+
| Prop | Type | Default | Description |
|
|
46
|
+
|------|------|---------|-------------|
|
|
47
|
+
| `className` | `string` | - | Additional CSS classes for the wrapper |
|
|
48
|
+
| `size` | `string` | `"default"` | Button size variant (small, default, large, etc.) |
|
|
49
|
+
| `style` | `string` | `"default"` | Button style variant (default, primary, secondary, etc.) |
|
|
50
|
+
| `text` | `string` | `"Button Text"` | Button display text |
|
|
51
|
+
| `link` | `string` | `"#"` | Button URL/href |
|
|
52
|
+
| `target` | `string` | - | Link target attribute (_blank, _self, etc.) |
|
|
53
|
+
| `screenReader` | `string` | - | Additional text for screen readers |
|
|
54
|
+
| `wrapperAttributes` | `object` | - | Additional HTML attributes for wrapper |
|
|
55
|
+
|
|
56
|
+
## Button Styles
|
|
57
|
+
|
|
58
|
+
The component supports various style variants through the `style` prop:
|
|
59
|
+
- `default` - Standard button appearance
|
|
60
|
+
- `primary` - Primary action button
|
|
61
|
+
- `secondary` - Secondary action button
|
|
62
|
+
- Custom styles can be added via CSS
|
|
63
|
+
|
|
64
|
+
## Button Sizes
|
|
65
|
+
|
|
66
|
+
Available size variants through the `size` prop:
|
|
67
|
+
- `small` - Compact button
|
|
68
|
+
- `default` - Standard size
|
|
69
|
+
- `large` - Larger button
|
|
70
|
+
- Custom sizes can be defined via CSS
|
|
71
|
+
|
|
72
|
+
## CSS Classes
|
|
73
|
+
|
|
74
|
+
The component generates BEM-style classes:
|
|
75
|
+
|
|
76
|
+
### Wrapper Classes
|
|
77
|
+
- Base: `wp-block-kitaris-button`
|
|
78
|
+
- Style modifier: `is-style-{style}`
|
|
79
|
+
- Size modifier: `is-size-{size}`
|
|
80
|
+
- Custom classes: Added via `className` prop
|
|
81
|
+
|
|
82
|
+
### Inner Elements
|
|
83
|
+
- Link text: `wp-block-kitaris-button__text`
|
|
84
|
+
- Screen reader text: `screen-reader-text`
|
|
85
|
+
|
|
86
|
+
## Accessibility
|
|
87
|
+
|
|
88
|
+
### Screen Reader Support
|
|
89
|
+
Additional context for screen readers can be provided:
|
|
90
|
+
```jsx
|
|
91
|
+
<Button
|
|
92
|
+
text="Download"
|
|
93
|
+
link="/file.pdf"
|
|
94
|
+
screenReader="PDF document, 2.5MB"
|
|
95
|
+
/>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Target Attribute
|
|
99
|
+
When using `target="_blank"`, consider adding screen reader text:
|
|
100
|
+
```jsx
|
|
101
|
+
<Button
|
|
102
|
+
text="External Site"
|
|
103
|
+
link="https://example.com"
|
|
104
|
+
target="_blank"
|
|
105
|
+
screenReader="Opens in new window"
|
|
106
|
+
/>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Examples
|
|
110
|
+
|
|
111
|
+
### Call-to-Action Button
|
|
112
|
+
```jsx
|
|
113
|
+
<Button
|
|
114
|
+
text="Start Free Trial"
|
|
115
|
+
link="/trial"
|
|
116
|
+
style="primary"
|
|
117
|
+
size="large"
|
|
118
|
+
className="hero-cta"
|
|
119
|
+
/>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Download Button
|
|
123
|
+
```jsx
|
|
124
|
+
<Button
|
|
125
|
+
text="Download Guide"
|
|
126
|
+
link="/downloads/guide.pdf"
|
|
127
|
+
style="secondary"
|
|
128
|
+
screenReader="PDF, 5MB"
|
|
129
|
+
wrapperAttributes={{
|
|
130
|
+
'data-track': 'download',
|
|
131
|
+
'data-file': 'guide.pdf'
|
|
132
|
+
}}
|
|
133
|
+
/>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Navigation Button
|
|
137
|
+
```jsx
|
|
138
|
+
<Button
|
|
139
|
+
text="Next Page"
|
|
140
|
+
link="/page/2"
|
|
141
|
+
size="small"
|
|
142
|
+
className="pagination-button"
|
|
143
|
+
/>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### External Link Button
|
|
147
|
+
```jsx
|
|
148
|
+
<Button
|
|
149
|
+
text="Visit Partner Site"
|
|
150
|
+
link="https://partner.com"
|
|
151
|
+
target="_blank"
|
|
152
|
+
style="outline"
|
|
153
|
+
screenReader="Opens partner.com in new window"
|
|
154
|
+
/>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Styling
|
|
158
|
+
|
|
159
|
+
The component is designed to work with your theme's button styles. You can customize appearance by:
|
|
160
|
+
|
|
161
|
+
1. Using predefined style and size variants
|
|
162
|
+
2. Adding custom CSS classes via `className`
|
|
163
|
+
3. Defining CSS rules for the generated BEM classes
|
|
164
|
+
4. Using wrapper attributes for data attributes or additional HTML attributes
|
|
165
|
+
|
|
166
|
+
## Notes
|
|
167
|
+
|
|
168
|
+
- This is a presentational component that renders button-like elements using spans
|
|
169
|
+
- For actual form submissions or interactive buttons, use the WordPress Button component
|
|
170
|
+
- The component maintains consistent structure for reliable styling
|
|
171
|
+
- Screen reader text is properly hidden visually but remains accessible
|
|
172
|
+
- All props are optional with sensible defaults
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** Variants, as on kitaris/button. */
|
|
2
|
+
const VARIANTS = ["primary", "secondary", "tertiary", "text-link"];
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Style and variant classes, as siratik-utility's Button::render() writes
|
|
6
|
+
* them. `style` is the block style ("" or "fill", "outline"); the older form,
|
|
7
|
+
* a variant passed as `style` ("default", "secondary", "tertiary",
|
|
8
|
+
* "text-link"), still works.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} style Block style, or a legacy variant.
|
|
11
|
+
* @param {string} variant Variant.
|
|
12
|
+
* @return {string} Classes.
|
|
13
|
+
*/
|
|
14
|
+
const styleAndVariantClasses = (style, variant) => {
|
|
15
|
+
let nextStyle = style || "";
|
|
16
|
+
let nextVariant = variant || "";
|
|
17
|
+
|
|
18
|
+
if (nextStyle === "default" || VARIANTS.includes(nextStyle)) {
|
|
19
|
+
if (!nextVariant && nextStyle !== "default") {
|
|
20
|
+
nextVariant = nextStyle;
|
|
21
|
+
}
|
|
22
|
+
nextStyle = "";
|
|
23
|
+
}
|
|
24
|
+
if (!VARIANTS.includes(nextVariant)) {
|
|
25
|
+
nextVariant = "primary";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return [
|
|
29
|
+
nextStyle && nextStyle !== "fill" ? `is-style-${nextStyle}` : "",
|
|
30
|
+
`is-variant-${nextVariant}`,
|
|
31
|
+
]
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.join(" ");
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Render a button component.
|
|
38
|
+
*
|
|
39
|
+
* @since 0.1.0
|
|
40
|
+
*
|
|
41
|
+
* @param {Object} props Component properties.
|
|
42
|
+
* @param {string} props.className Optional. Additional class for the button.
|
|
43
|
+
* @param {string} props.style Optional. Block style: "" (Fill) or "outline".
|
|
44
|
+
* Legacy: a variant name.
|
|
45
|
+
* @param {string} props.variant Optional. "primary" (default), "secondary",
|
|
46
|
+
* "tertiary" or "text-link".
|
|
47
|
+
* @param {string} props.text Button text. Defaults to 'Button Text'.
|
|
48
|
+
* @param {string} props.link Button link. Defaults to '#'.
|
|
49
|
+
* @param {string|null} props.target Optional. Link target.
|
|
50
|
+
* @param {string|null} props.screenReader Optional. Screen reader text.
|
|
51
|
+
* @returns {WPElement} Button element to render.
|
|
52
|
+
*/
|
|
53
|
+
function ButtonFrontend({
|
|
54
|
+
className = null,
|
|
55
|
+
size = "default",
|
|
56
|
+
style = "",
|
|
57
|
+
variant = "",
|
|
58
|
+
text = "Button Text",
|
|
59
|
+
screenReader = null,
|
|
60
|
+
}) {
|
|
61
|
+
// Add screen reader text
|
|
62
|
+
const screenReaderElement = screenReader ? (
|
|
63
|
+
<span className="screen-reader-only">{screenReader}</span>
|
|
64
|
+
) : null;
|
|
65
|
+
|
|
66
|
+
// Add classes
|
|
67
|
+
const wrapperClass = className
|
|
68
|
+
? `${className}__button is-size-${size}`
|
|
69
|
+
: `wp-block-kitaris-button ${styleAndVariantClasses(style, variant)} is-size-${size}`;
|
|
70
|
+
const linkClass = className
|
|
71
|
+
? `wp-block-kitaris-button__link wp-block-kitaris-button__text`
|
|
72
|
+
: "wp-block-kitaris-button__text";
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<span className={wrapperClass}>
|
|
76
|
+
<span className={linkClass}>
|
|
77
|
+
{text}
|
|
78
|
+
{screenReaderElement}
|
|
79
|
+
</span>
|
|
80
|
+
</span>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export { ButtonFrontend };
|