@bookinglab/booking-ui-react 1.12.0 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,16 +1,132 @@
1
- import { forwardRef, useId, useState, useCallback, useImperativeHandle, useEffect, useRef } from 'react';
1
+ import React, { forwardRef, useId, useState, useCallback, useImperativeHandle, useEffect, useRef } from 'react';
2
2
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
3
3
 
4
4
  var __defProp = Object.defineProperty;
5
5
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
6
6
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
7
7
  var cx = (...classes) => classes.filter(Boolean).join(" ");
8
+ function DateInputGroup({
9
+ question,
10
+ value,
11
+ onChange,
12
+ inputId,
13
+ hasError,
14
+ errorId,
15
+ fieldsetCls,
16
+ legendCls,
17
+ groupCls,
18
+ itemCls,
19
+ sublabelCls,
20
+ dateInputCls,
21
+ helpTextAbove,
22
+ helpTextBelow,
23
+ renderError
24
+ }) {
25
+ const parseValue = (raw) => {
26
+ const m = raw.match(/^(\d{4})-(\d{2})-(\d{2})$/);
27
+ if (!m) return { day: "", month: "", year: "" };
28
+ return { year: m[1], month: String(Number(m[2])), day: String(Number(m[3])) };
29
+ };
30
+ const [parts, setParts] = useState(() => parseValue(value));
31
+ const lastEmittedRef = React.useRef(value);
32
+ useEffect(() => {
33
+ if (value !== lastEmittedRef.current) {
34
+ setParts(parseValue(value));
35
+ lastEmittedRef.current = value;
36
+ }
37
+ }, [value]);
38
+ const buildValue = (d, m, y) => {
39
+ if (!/^\d{1,2}$/.test(d) || !/^\d{1,2}$/.test(m) || !/^\d{4}$/.test(y)) return "";
40
+ const dn = Number(d), mn = Number(m), yn = Number(y);
41
+ const date = new Date(yn, mn - 1, dn);
42
+ if (date.getFullYear() !== yn || date.getMonth() !== mn - 1 || date.getDate() !== dn) return "";
43
+ return `${String(yn).padStart(4, "0")}-${String(mn).padStart(2, "0")}-${String(dn).padStart(2, "0")}`;
44
+ };
45
+ const handleSubChange = (part, v) => {
46
+ const sanitized = v.replace(/[^\d]/g, "");
47
+ const max = part === "year" ? 4 : 2;
48
+ const next = { ...parts, [part]: sanitized.slice(0, max) };
49
+ setParts(next);
50
+ const combined = buildValue(next.day, next.month, next.year);
51
+ lastEmittedRef.current = combined;
52
+ onChange(combined);
53
+ };
54
+ const dayId = `${inputId}-day`;
55
+ const monthId = `${inputId}-month`;
56
+ const yearId = `${inputId}-year`;
57
+ return /* @__PURE__ */ jsxs("fieldset", { className: fieldsetCls, "aria-describedby": hasError ? errorId : void 0, children: [
58
+ /* @__PURE__ */ jsxs("legend", { className: legendCls, children: [
59
+ question.name,
60
+ question.required && /* @__PURE__ */ jsx("span", { className: "text-red-500 ml-1", "aria-hidden": "true", children: "*" })
61
+ ] }),
62
+ helpTextAbove,
63
+ /* @__PURE__ */ jsxs("div", { className: groupCls, role: "group", children: [
64
+ /* @__PURE__ */ jsxs("div", { className: itemCls, children: [
65
+ /* @__PURE__ */ jsx("label", { htmlFor: dayId, className: sublabelCls, children: "Day" }),
66
+ /* @__PURE__ */ jsx(
67
+ "input",
68
+ {
69
+ id: dayId,
70
+ type: "text",
71
+ inputMode: "numeric",
72
+ pattern: "[0-9]*",
73
+ maxLength: 2,
74
+ value: parts.day,
75
+ onChange: (e) => handleSubChange("day", e.target.value),
76
+ className: cx(dateInputCls, "w-16"),
77
+ "aria-invalid": hasError ? true : void 0,
78
+ "aria-required": question.required || void 0
79
+ }
80
+ )
81
+ ] }),
82
+ /* @__PURE__ */ jsxs("div", { className: itemCls, children: [
83
+ /* @__PURE__ */ jsx("label", { htmlFor: monthId, className: sublabelCls, children: "Month" }),
84
+ /* @__PURE__ */ jsx(
85
+ "input",
86
+ {
87
+ id: monthId,
88
+ type: "text",
89
+ inputMode: "numeric",
90
+ pattern: "[0-9]*",
91
+ maxLength: 2,
92
+ value: parts.month,
93
+ onChange: (e) => handleSubChange("month", e.target.value),
94
+ className: cx(dateInputCls, "w-16"),
95
+ "aria-invalid": hasError ? true : void 0,
96
+ "aria-required": question.required || void 0
97
+ }
98
+ )
99
+ ] }),
100
+ /* @__PURE__ */ jsxs("div", { className: itemCls, children: [
101
+ /* @__PURE__ */ jsx("label", { htmlFor: yearId, className: sublabelCls, children: "Year" }),
102
+ /* @__PURE__ */ jsx(
103
+ "input",
104
+ {
105
+ id: yearId,
106
+ type: "text",
107
+ inputMode: "numeric",
108
+ pattern: "[0-9]*",
109
+ maxLength: 4,
110
+ value: parts.year,
111
+ onChange: (e) => handleSubChange("year", e.target.value),
112
+ className: cx(dateInputCls, "w-24"),
113
+ "aria-invalid": hasError ? true : void 0,
114
+ "aria-required": question.required || void 0
115
+ }
116
+ )
117
+ ] })
118
+ ] }),
119
+ helpTextBelow,
120
+ renderError()
121
+ ] });
122
+ }
8
123
  function FormField({
9
124
  question,
10
125
  value,
11
126
  error,
12
127
  onChange,
13
- classNames
128
+ classNames,
129
+ displayHelpTextBelowLabel = false
14
130
  }) {
15
131
  const inputId = `question-${question.id}`;
16
132
  const errorId = `${inputId}-error`;
@@ -43,6 +159,8 @@ function FormField({
43
159
  }
44
160
  );
45
161
  };
162
+ const helpTextAbove = displayHelpTextBelowLabel ? renderHelpText() : null;
163
+ const helpTextBelow = displayHelpTextBelowLabel ? null : renderHelpText();
46
164
  const renderError = () => {
47
165
  if (!error) return null;
48
166
  return /* @__PURE__ */ jsx("p", { id: errorId, className: classNames?.errorText ?? defaultErrorText, role: "alert", children: error });
@@ -61,6 +179,7 @@ function FormField({
61
179
  case "text_field":
62
180
  return /* @__PURE__ */ jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
63
181
  renderLabel(),
182
+ helpTextAbove,
64
183
  /* @__PURE__ */ jsx(
65
184
  "input",
66
185
  {
@@ -73,12 +192,13 @@ function FormField({
73
192
  ...ariaProps
74
193
  }
75
194
  ),
76
- renderHelpText(),
195
+ helpTextBelow,
77
196
  renderError()
78
197
  ] });
79
198
  case "text_area":
80
199
  return /* @__PURE__ */ jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
81
200
  renderLabel(),
201
+ helpTextAbove,
82
202
  /* @__PURE__ */ jsx(
83
203
  "textarea",
84
204
  {
@@ -91,12 +211,13 @@ function FormField({
91
211
  ...ariaProps
92
212
  }
93
213
  ),
94
- renderHelpText(),
214
+ helpTextBelow,
95
215
  renderError()
96
216
  ] });
97
217
  case "select":
98
218
  return /* @__PURE__ */ jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
99
219
  renderLabel(),
220
+ helpTextAbove,
100
221
  /* @__PURE__ */ jsxs(
101
222
  "select",
102
223
  {
@@ -111,12 +232,13 @@ function FormField({
111
232
  ]
112
233
  }
113
234
  ),
114
- renderHelpText(),
235
+ helpTextBelow,
115
236
  renderError()
116
237
  ] });
117
238
  case "radio":
118
239
  return /* @__PURE__ */ jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
119
240
  renderLabel(),
241
+ helpTextAbove,
120
242
  /* @__PURE__ */ jsx(
121
243
  "div",
122
244
  {
@@ -144,34 +266,10 @@ function FormField({
144
266
  })
145
267
  }
146
268
  ),
147
- renderHelpText(),
269
+ helpTextBelow,
148
270
  renderError()
149
271
  ] });
150
272
  case "date": {
151
- const raw = typeof value === "string" ? value : "";
152
- const match2 = raw.match(/^(\d{0,4})-?(\d{0,2})-?(\d{0,2})$/);
153
- const yearPart = match2?.[1] ?? "";
154
- const monthPart = match2?.[2] ? String(Number(match2[2])) : "";
155
- const dayPart = match2?.[3] ? String(Number(match2[3])) : "";
156
- const dayId = `${inputId}-day`;
157
- const monthId = `${inputId}-month`;
158
- const yearId = `${inputId}-year`;
159
- const buildValue = (d, m, y) => {
160
- const dt = d.trim();
161
- const mt = m.trim();
162
- const yt = y.trim();
163
- if (!dt && !mt && !yt) return "";
164
- if (/^\d{1,2}$/.test(dt) && /^\d{1,2}$/.test(mt) && /^\d{4}$/.test(yt)) {
165
- const dn = Number(dt);
166
- const mn = Number(mt);
167
- const yn = Number(yt);
168
- const date = new Date(yn, mn - 1, dn);
169
- if (date.getFullYear() === yn && date.getMonth() === mn - 1 && date.getDate() === dn) {
170
- return `${String(yn).padStart(4, "0")}-${String(mn).padStart(2, "0")}-${String(dn).padStart(2, "0")}`;
171
- }
172
- }
173
- return "";
174
- };
175
273
  const dateInputBase = classNames?.dateInput ?? classNames?.input ?? defaultInput;
176
274
  const dateInputCls = hasError ? cx(dateInputBase, classNames?.inputError ?? defaultInputError) : dateInputBase;
177
275
  const sublabelCls = classNames?.dateInputLabel ?? "block text-xs font-medium mb-1 text-gray-700";
@@ -179,91 +277,31 @@ function FormField({
179
277
  const groupCls = classNames?.dateInputGroup ?? "flex flex-row gap-3 items-end";
180
278
  const legendCls = classNames?.dateLegend ?? labelClasses;
181
279
  const fieldsetCls = classNames?.dateFieldset ?? (classNames?.fieldWrapper ?? defaultFieldWrapper);
182
- const handleSubChange = (part, v) => {
183
- const sanitized = v.replace(/[^\d]/g, "");
184
- const next = {
185
- day: part === "day" ? sanitized : dayPart,
186
- month: part === "month" ? sanitized : monthPart,
187
- year: part === "year" ? sanitized : yearPart
188
- };
189
- const combined = buildValue(next.day, next.month, next.year);
190
- onChange(combined);
191
- };
192
- return /* @__PURE__ */ jsxs(
193
- "fieldset",
280
+ return /* @__PURE__ */ jsx(
281
+ DateInputGroup,
194
282
  {
195
- className: fieldsetCls,
196
- "aria-describedby": hasError ? errorId : void 0,
197
- children: [
198
- /* @__PURE__ */ jsxs("legend", { className: legendCls, children: [
199
- question.name,
200
- question.required && /* @__PURE__ */ jsx("span", { className: "text-red-500 ml-1", "aria-hidden": "true", children: "*" })
201
- ] }),
202
- /* @__PURE__ */ jsxs("div", { className: groupCls, role: "group", "aria-labelledby": void 0, children: [
203
- /* @__PURE__ */ jsxs("div", { className: itemCls, children: [
204
- /* @__PURE__ */ jsx("label", { htmlFor: dayId, className: sublabelCls, children: "Day" }),
205
- /* @__PURE__ */ jsx(
206
- "input",
207
- {
208
- id: dayId,
209
- type: "text",
210
- inputMode: "numeric",
211
- pattern: "[0-9]*",
212
- maxLength: 2,
213
- value: dayPart,
214
- onChange: (e) => handleSubChange("day", e.target.value),
215
- className: cx(dateInputCls, "w-16"),
216
- "aria-invalid": hasError ? true : void 0,
217
- "aria-required": question.required || void 0
218
- }
219
- )
220
- ] }),
221
- /* @__PURE__ */ jsxs("div", { className: itemCls, children: [
222
- /* @__PURE__ */ jsx("label", { htmlFor: monthId, className: sublabelCls, children: "Month" }),
223
- /* @__PURE__ */ jsx(
224
- "input",
225
- {
226
- id: monthId,
227
- type: "text",
228
- inputMode: "numeric",
229
- pattern: "[0-9]*",
230
- maxLength: 2,
231
- value: monthPart,
232
- onChange: (e) => handleSubChange("month", e.target.value),
233
- className: cx(dateInputCls, "w-16"),
234
- "aria-invalid": hasError ? true : void 0,
235
- "aria-required": question.required || void 0
236
- }
237
- )
238
- ] }),
239
- /* @__PURE__ */ jsxs("div", { className: itemCls, children: [
240
- /* @__PURE__ */ jsx("label", { htmlFor: yearId, className: sublabelCls, children: "Year" }),
241
- /* @__PURE__ */ jsx(
242
- "input",
243
- {
244
- id: yearId,
245
- type: "text",
246
- inputMode: "numeric",
247
- pattern: "[0-9]*",
248
- maxLength: 4,
249
- value: yearPart,
250
- onChange: (e) => handleSubChange("year", e.target.value),
251
- className: cx(dateInputCls, "w-24"),
252
- "aria-invalid": hasError ? true : void 0,
253
- "aria-required": question.required || void 0
254
- }
255
- )
256
- ] })
257
- ] }),
258
- renderHelpText(),
259
- renderError()
260
- ]
283
+ question,
284
+ value: typeof value === "string" ? value : "",
285
+ onChange,
286
+ inputId,
287
+ hasError,
288
+ errorId,
289
+ fieldsetCls,
290
+ legendCls,
291
+ groupCls,
292
+ itemCls,
293
+ sublabelCls,
294
+ dateInputCls,
295
+ helpTextAbove,
296
+ helpTextBelow,
297
+ renderError
261
298
  }
262
299
  );
263
300
  }
264
301
  case "number":
265
302
  return /* @__PURE__ */ jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
266
303
  renderLabel(),
304
+ helpTextAbove,
267
305
  /* @__PURE__ */ jsx(
268
306
  "input",
269
307
  {
@@ -278,11 +316,12 @@ function FormField({
278
316
  ...ariaProps
279
317
  }
280
318
  ),
281
- renderHelpText(),
319
+ helpTextBelow,
282
320
  renderError()
283
321
  ] });
284
322
  case "check":
285
323
  return /* @__PURE__ */ jsxs("div", { className: classNames?.fieldWrapper ?? defaultFieldWrapper, children: [
324
+ helpTextAbove,
286
325
  /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2", children: [
287
326
  /* @__PURE__ */ jsx(
288
327
  "input",
@@ -300,7 +339,7 @@ function FormField({
300
339
  question.required && /* @__PURE__ */ jsx("span", { className: "text-red-500 ml-1", "aria-hidden": "true", children: "*" })
301
340
  ] })
302
341
  ] }),
303
- renderHelpText(),
342
+ helpTextBelow,
304
343
  renderError()
305
344
  ] });
306
345
  default:
@@ -312,6 +351,7 @@ function BookingForm({
312
351
  onSubmit,
313
352
  submitLabel = "Submit",
314
353
  isAdmin = false,
354
+ displayHelpTextBelowLabel = false,
315
355
  className = "",
316
356
  labelClassName,
317
357
  classNames: classNamesProp
@@ -508,7 +548,8 @@ function BookingForm({
508
548
  value: values[question.id],
509
549
  error: touched[question.id] ? errors[question.id] : void 0,
510
550
  onChange: (value) => handleChange(question.id, value),
511
- classNames
551
+ classNames,
552
+ displayHelpTextBelowLabel
512
553
  },
513
554
  question.id
514
555
  )),