kelt-ui-kit-react 2.0.1 → 2.0.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/src/form/Form.tsx CHANGED
@@ -12,6 +12,7 @@ import "./form.css";
12
12
  import {
13
13
  FormInterface,
14
14
  FormMode,
15
+ FormSection,
15
16
  FormValuesInterface,
16
17
  } from "./form.interface";
17
18
  import { Input } from "./input/Input";
@@ -20,6 +21,7 @@ import { TextArea } from "./textArea/TextArea";
20
21
  import { Col } from "../grid/col/Col";
21
22
  import { Row } from "../grid/row/Row";
22
23
  import { TypeInputEnum } from "./form.enum";
24
+ import { AddressSearch } from "../addressSearch/AddressSearch";
23
25
 
24
26
  const euroFormatter = new Intl.NumberFormat("fr-FR", {
25
27
  style: "currency",
@@ -48,7 +50,8 @@ export type FormProps<
48
50
  hideSubmit?: boolean;
49
51
  autoComplete?: string;
50
52
  children?: React.ReactNode;
51
- initialForm: FormInterface[];
53
+ initialForm?: FormInterface[];
54
+ sections?: FormSection[];
52
55
  disabledSubmit?: boolean;
53
56
  mode?: FormMode;
54
57
  onSubmit?: (values: FormValuesInterface<T>) => void;
@@ -59,6 +62,7 @@ export const DynamicForm = forwardRef(
59
62
  <T extends { [key: string]: string | number | boolean | Date | string[] }>(
60
63
  {
61
64
  initialForm,
65
+ sections,
62
66
  title,
63
67
  onSubmit,
64
68
  id,
@@ -79,11 +83,17 @@ export const DynamicForm = forwardRef(
79
83
  setBtnDisabledSubmit?: (disabled: boolean) => void;
80
84
  }>
81
85
  ) => {
86
+ const formFields = sections
87
+ ? sections.reduce<FormInterface[]>(
88
+ (fields, section) => fields.concat(section.fields),
89
+ [],
90
+ )
91
+ : (initialForm ?? []);
82
92
  const [btnDisabledSubmit, setBtnDisabledSubmit] = useState(disabledSubmit);
83
93
  const [values, setValues] = useState<FormValuesInterface<T>>(() => {
84
94
  const initialValues: FormValuesInterface<T> =
85
95
  {} as FormValuesInterface<T>;
86
- initialForm.forEach((field) => {
96
+ formFields.forEach((field) => {
87
97
  (initialValues as any)[field.name] = field.value ?? "";
88
98
  });
89
99
  return initialValues;
@@ -164,7 +174,7 @@ export const DynamicForm = forwardRef(
164
174
  setValues(() => {
165
175
  const resetValues: FormValuesInterface<T> =
166
176
  {} as FormValuesInterface<T>;
167
- initialForm.forEach((field) => {
177
+ formFields.forEach((field) => {
168
178
  (resetValues as any)[field.name] = field.value ?? "";
169
179
  });
170
180
  return resetValues;
@@ -179,7 +189,7 @@ export const DynamicForm = forwardRef(
179
189
  }, []);
180
190
 
181
191
  const focusInitialElement = () => {
182
- const focusedField = initialForm.find((field) => field.focus);
192
+ const focusedField = formFields.find((field) => field.focus);
183
193
  if (focusedField && inputRefs.current[focusedField.name]) {
184
194
  setTimeout(() => {
185
195
  inputRefs.current[focusedField.name]?.select();
@@ -198,6 +208,17 @@ export const DynamicForm = forwardRef(
198
208
  }));
199
209
  };
200
210
 
211
+ const updateValues = useCallback(
212
+ (updatedValues: FormValuesInterface<T>) => {
213
+ setValues((previousValues) => {
214
+ const nextValues = { ...previousValues, ...updatedValues };
215
+ onChange?.(nextValues);
216
+ return nextValues;
217
+ });
218
+ },
219
+ [onChange],
220
+ );
221
+
201
222
  useImperativeHandle(ref, () => ({
202
223
  resetForm,
203
224
  clearInput,
@@ -207,8 +228,6 @@ export const DynamicForm = forwardRef(
207
228
  setBtnDisabledSubmit,
208
229
  }));
209
230
 
210
- const gridEnabled = initialForm.some((field) => field.col !== undefined);
211
- const FieldsContainer = gridEnabled ? Row : React.Fragment;
212
231
  const getReadValue = (field: FormInterface): React.ReactNode => {
213
232
  const value = values[field.name];
214
233
 
@@ -253,15 +272,13 @@ export const DynamicForm = forwardRef(
253
272
  : String(value);
254
273
  };
255
274
 
256
- return (
257
- <form
258
- autoComplete={autoComplete ?? "off"}
259
- onSubmit={handleSubmit}
260
- className={className ?? ""}
261
- >
262
- <div className="form-content">
263
- <FieldsContainer>
264
- {initialForm.map((v, key) => {
275
+ const renderFields = (fields: FormInterface[]) => {
276
+ const gridEnabled = fields.some((field) => field.col !== undefined);
277
+ const FieldsContainer = gridEnabled ? Row : React.Fragment;
278
+
279
+ return (
280
+ <FieldsContainer>
281
+ {fields.map((v, key) => {
265
282
  const required = v.onRequired
266
283
  ? v.onRequired(v.value)
267
284
  : v.required ?? false;
@@ -270,102 +287,150 @@ export const DynamicForm = forwardRef(
270
287
  : v.disabled ?? false;
271
288
 
272
289
  return (
273
- <FormFieldContainer key={key} field={v} gridEnabled={gridEnabled}>
290
+ <FormFieldContainer
291
+ key={v.name || key}
292
+ field={v}
293
+ gridEnabled={gridEnabled}
294
+ >
274
295
  <div
275
- className={`d-flex flex-column ${
276
- v.className ?? ""
277
- } form-group form-group-${v.type}`}
296
+ className={`d-flex flex-column ${v.className ?? ""
297
+ } form-group form-group-${v.type}`}
278
298
  >
279
- {v.label && <label>{v.label}</label>}
280
- {v.icon && <Icon classIcon={v.icon} />}
299
+ {v.label && <label>{v.label}</label>}
300
+ {v.icon && <Icon classIcon={v.icon} />}
281
301
 
282
- {mode === "read" ? (
283
- <div
284
- className={`form-read-value ${
285
- v.type === TypeInputEnum.TEXTAREA
302
+ {mode === "read" ? (
303
+ <div
304
+ className={`form-read-value ${v.type === TypeInputEnum.TEXTAREA
286
305
  ? "form-read-value--textarea"
287
306
  : ""
288
- }`}
289
- >
290
- {getReadValue(v)}
291
- </div>
292
- ) : v.type === "select" ? (
293
- <Select
294
- id={v.id ?? v.name}
295
- label={undefined}
296
- name={v.name}
297
- options={v.options ?? []}
298
- disabled={disabled}
299
- multiple={v.multiple}
300
- value={values[v.name] as any}
301
- required={required}
302
- defaultValue={v.value as string | string[]}
303
- placeholder={v.placeholder}
304
- onChangeMultiple={(newValue) =>
305
- handleChangeSelect(newValue, v.name, v)
306
- }
307
- onChange={(newValue) =>
308
- handleChangeSelect(newValue, v.name, v)
309
- }
310
- />
311
- ) : v.type === "textarea" ? (
312
- <TextArea
313
- id={v.id ?? v.name}
314
- name={v.name}
315
- className="textarea-input"
316
- placeholder={v.placeholder ?? ""}
317
- disabled={disabled}
318
- required={required}
319
- value={values[v.name] as string}
320
- onChange={(value, name) =>
321
- handleChangeTextArea(name, value, v)
322
- }
323
- />
324
- ) : v.type === TypeInputEnum.PRICE ? (
325
- <InputPrice
326
- id={v.id ?? v.name}
327
- ref={(el) => (inputRefs.current[v.name] = el)}
328
- name={v.name}
329
- placeholder={v.placeholder ?? ""}
330
- autoComplete={v.autoComplete}
331
- tabIndex={0}
332
- disabled={disabled}
333
- autoFocus={v.focus ?? false}
334
- required={required}
335
- value={String(values[v.name] ?? "")}
336
- onChange={(value) =>
337
- handleChangeTextArea(v.name, value, v)
338
- }
339
- />
340
- ) : (
341
- <Input
342
- id={v.id ?? v.name}
343
- ref={(el) => (inputRefs.current[v.name] = el)}
344
- name={v.name}
345
- type={v.type}
346
- placeholder={v.placeholder ?? ""}
347
- autoComplete={v.autoComplete}
348
- tabIndex={0}
349
- disabled={disabled}
350
- autoFocus={v.focus ?? false}
351
- required={required}
352
- step={v.step ?? "0.01"}
353
- value={values[v.name] as string | number}
354
- checked={
355
- v.type === "checkbox" ? !!values[v.name] : undefined
356
- }
357
- onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
358
- handleChangeInput(e, v)
359
- }
360
- onInvalid={v.onInvalid ? v.onInvalid : undefined}
361
- />
362
- )}
307
+ }`}
308
+ >
309
+ {getReadValue(v)}
310
+ </div>
311
+ ) : v.type === TypeInputEnum.ADDRESS ? (
312
+ <AddressSearch
313
+ id={v.id ?? v.name}
314
+ label=""
315
+ placeholder={v.placeholder}
316
+ value={String(values[v.name] ?? "")}
317
+ disabled={disabled}
318
+ minimumQueryLength={v.addressSearch?.minimumQueryLength}
319
+ debounceTime={v.addressSearch?.debounceTime}
320
+ limit={v.addressSearch?.limit}
321
+ provider={v.addressSearch?.provider}
322
+ allowManualEntry
323
+ manualEntryLabel={v.addressSearch?.manualEntryLabel}
324
+ onChange={(value) =>
325
+ updateValues({ [v.name]: value } as FormValuesInterface<T>)
326
+ }
327
+ onSelect={(address) => {
328
+ const mappedValues =
329
+ v.addressSearch?.mapAddress?.(address) ?? {};
330
+ updateValues(mappedValues as FormValuesInterface<T>);
331
+ v.addressSearch?.onSelect?.(address);
332
+ }}
333
+ />
334
+ ) : v.type === "select" ? (
335
+ <Select
336
+ id={v.id ?? v.name}
337
+ label={undefined}
338
+ name={v.name}
339
+ options={v.options ?? []}
340
+ disabled={disabled}
341
+ multiple={v.multiple}
342
+ value={values[v.name] as any}
343
+ required={required}
344
+ defaultValue={v.value as string | string[]}
345
+ placeholder={v.placeholder}
346
+ onChangeMultiple={(newValue) =>
347
+ handleChangeSelect(newValue, v.name, v)
348
+ }
349
+ onChange={(newValue) =>
350
+ handleChangeSelect(newValue, v.name, v)
351
+ }
352
+ />
353
+ ) : v.type === "textarea" ? (
354
+ <TextArea
355
+ id={v.id ?? v.name}
356
+ name={v.name}
357
+ className="textarea-input"
358
+ placeholder={v.placeholder ?? ""}
359
+ disabled={disabled}
360
+ required={required}
361
+ value={values[v.name] as string}
362
+ onChange={(value, name) =>
363
+ handleChangeTextArea(name, value, v)
364
+ }
365
+ />
366
+ ) : v.type === TypeInputEnum.PRICE ? (
367
+ <InputPrice
368
+ id={v.id ?? v.name}
369
+ ref={(el) => (inputRefs.current[v.name] = el)}
370
+ name={v.name}
371
+ placeholder={v.placeholder ?? ""}
372
+ autoComplete={v.autoComplete}
373
+ tabIndex={0}
374
+ disabled={disabled}
375
+ autoFocus={v.focus ?? false}
376
+ required={required}
377
+ value={String(values[v.name] ?? "")}
378
+ onChange={(value) =>
379
+ handleChangeTextArea(v.name, value, v)
380
+ }
381
+ />
382
+ ) : (
383
+ <Input
384
+ id={v.id ?? v.name}
385
+ ref={(el) => (inputRefs.current[v.name] = el)}
386
+ name={v.name}
387
+ type={v.type}
388
+ placeholder={v.placeholder ?? ""}
389
+ autoComplete={v.autoComplete}
390
+ tabIndex={0}
391
+ disabled={disabled}
392
+ autoFocus={v.focus ?? false}
393
+ required={required}
394
+ step={v.step ?? "0.01"}
395
+ value={values[v.name] as string | number}
396
+ checked={
397
+ v.type === "checkbox" ? !!values[v.name] : undefined
398
+ }
399
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
400
+ handleChangeInput(e, v)
401
+ }
402
+ onInvalid={v.onInvalid ? v.onInvalid : undefined}
403
+ />
404
+ )}
363
405
  </div>
364
406
  </FormFieldContainer>
365
407
  );
366
408
  })}
409
+ </FieldsContainer>
410
+ );
411
+ };
412
+
413
+ return (
414
+ <form
415
+ autoComplete={autoComplete ?? "off"}
416
+ onSubmit={handleSubmit}
417
+ className={className ?? ""}
418
+ >
419
+ <div className="form-content">
420
+ {sections
421
+ ? sections.map((section, index) => (
422
+ <section
423
+ className={`form-section ${section.className ?? ""}`}
424
+ key={index}
425
+ >
426
+ {section.title && (
427
+ <h3 className="form-section-title">{section.title}</h3>
428
+ )}
429
+ {renderFields(section.fields)}
430
+ </section>
431
+ ))
432
+ : renderFields(initialForm ?? [])}
367
433
  {children}
368
- </FieldsContainer>
369
434
  </div>
370
435
  <div className="d-flex justify-content-end form-footer">
371
436
  {mode === "edit" && !hideSubmit && (
package/src/form/form.css CHANGED
@@ -20,6 +20,19 @@ input[type="radio"] {
20
20
  }
21
21
 
22
22
  form {
23
+ .form-section+.form-section {
24
+ margin-top: 1.25rem;
25
+ }
26
+
27
+ .form-section-title {
28
+ margin: 0 15px 0.5rem;
29
+ padding-bottom: 0.5rem;
30
+ border-bottom: 1px solid #dedede;
31
+ color: var(--primary-color);
32
+ font-size: 1rem;
33
+ font-weight: 600;
34
+ }
35
+
23
36
  &.form-row {
24
37
  display: flex;
25
38
  flex-direction: row;
@@ -79,7 +92,7 @@ form {
79
92
  border: 1px solid transparent;
80
93
  border-radius: 4px;
81
94
  margin: 0.25rem 0;
82
- padding: 0.5rem 1rem;
95
+ padding: 0;
83
96
  font-size: 0.8rem;
84
97
  line-height: normal;
85
98
  overflow-wrap: anywhere;
@@ -1,4 +1,5 @@
1
1
  export enum TypeInputEnum {
2
+ ADDRESS = "address",
2
3
  TEXT = "text",
3
4
  BUTTON = "button",
4
5
  EMAIL = "email",
@@ -1,5 +1,9 @@
1
1
  import { TypeInputEnum } from "./form.enum";
2
2
  import { GridBreakpoints } from "../grid/grid.interface";
3
+ import {
4
+ GeocodedAddress,
5
+ GeocodingProvider,
6
+ } from "../geocoding/geocoding.interface";
3
7
 
4
8
  export type FormMode = "read" | "edit";
5
9
 
@@ -13,6 +17,12 @@ export interface FormCondition {
13
17
  regex: string;
14
18
  }
15
19
 
20
+ export interface FormSection {
21
+ title?: React.ReactNode;
22
+ fields: FormInterface[];
23
+ className?: string;
24
+ }
25
+
16
26
  export interface FormInterface {
17
27
  value: string | number | boolean | Date;
18
28
  id?: string;
@@ -29,6 +39,17 @@ export interface FormInterface {
29
39
  options?: { value: string; label: string }[]; // <--- pour les <option>
30
40
  className?: string;
31
41
  col?: GridBreakpoints;
42
+ addressSearch?: {
43
+ minimumQueryLength?: number;
44
+ debounceTime?: number;
45
+ limit?: number;
46
+ provider?: GeocodingProvider;
47
+ manualEntryLabel?: string;
48
+ mapAddress?: (
49
+ address: GeocodedAddress,
50
+ ) => Record<string, string | number | boolean | Date | string[]>;
51
+ onSelect?: (address: GeocodedAddress) => void;
52
+ };
32
53
  formatReadValue?: (
33
54
  value: string | number | boolean | Date | string[]
34
55
  ) => React.ReactNode;
@@ -0,0 +1,89 @@
1
+ import {
2
+ GeocodedAddress,
3
+ GeocodingProvider,
4
+ GeocodingSearchOptions,
5
+ } from "./geocoding.interface";
6
+
7
+ const DEFAULT_GEOCODING_URL = "https://data.geopf.fr/geocodage/search";
8
+
9
+ interface GeocodingFeature {
10
+ geometry?: {
11
+ coordinates?: [number, number];
12
+ };
13
+ properties?: {
14
+ id?: string;
15
+ label?: string;
16
+ name?: string;
17
+ postcode?: string;
18
+ city?: string;
19
+ citycode?: string;
20
+ score?: number;
21
+ type?: string;
22
+ };
23
+ }
24
+
25
+ interface GeocodingResponse {
26
+ features?: GeocodingFeature[];
27
+ }
28
+
29
+ export interface GeocodingServiceOptions {
30
+ baseUrl?: string;
31
+ fetcher?: typeof fetch;
32
+ }
33
+
34
+ export class GeocodingService implements GeocodingProvider {
35
+ private readonly baseUrl: string;
36
+ private readonly fetcher: typeof fetch;
37
+
38
+ constructor(options: GeocodingServiceOptions = {}) {
39
+ this.baseUrl = options.baseUrl ?? DEFAULT_GEOCODING_URL;
40
+ this.fetcher = options.fetcher ?? globalThis.fetch.bind(globalThis);
41
+ }
42
+
43
+ async search(
44
+ query: string,
45
+ options: GeocodingSearchOptions = {},
46
+ ): Promise<GeocodedAddress[]> {
47
+ const normalizedQuery = query.trim();
48
+ if (!normalizedQuery) return [];
49
+
50
+ const params = new URLSearchParams({
51
+ q: normalizedQuery,
52
+ limit: String(options.limit ?? 5),
53
+ autocomplete: "1",
54
+ index: "address",
55
+ });
56
+ const response = await this.fetcher(`${this.baseUrl}?${params}`, {
57
+ signal: options.signal,
58
+ });
59
+
60
+ if (!response.ok) {
61
+ throw new Error(`Geocoding request failed (${response.status})`);
62
+ }
63
+
64
+ const data = (await response.json()) as GeocodingResponse;
65
+ return (data.features ?? []).flatMap((feature) => {
66
+ const properties = feature.properties;
67
+ const coordinates = feature.geometry?.coordinates;
68
+ if (!properties?.label || !coordinates) return [];
69
+
70
+ return [
71
+ {
72
+ id: properties.id ?? properties.label,
73
+ label: properties.label,
74
+ addressLine: properties.name ?? properties.label,
75
+ postalCode: properties.postcode ?? "",
76
+ city: properties.city ?? "",
77
+ cityCode: properties.citycode,
78
+ countryCode: "FR",
79
+ longitude: coordinates[0],
80
+ latitude: coordinates[1],
81
+ score: properties.score,
82
+ type: properties.type,
83
+ },
84
+ ];
85
+ });
86
+ }
87
+ }
88
+
89
+ export const geocodingService = new GeocodingService();
@@ -0,0 +1,25 @@
1
+ export interface GeocodedAddress {
2
+ id: string;
3
+ label: string;
4
+ addressLine: string;
5
+ postalCode: string;
6
+ city: string;
7
+ cityCode?: string;
8
+ countryCode: string;
9
+ longitude: number;
10
+ latitude: number;
11
+ score?: number;
12
+ type?: string;
13
+ }
14
+
15
+ export interface GeocodingSearchOptions {
16
+ limit?: number;
17
+ signal?: AbortSignal;
18
+ }
19
+
20
+ export interface GeocodingProvider {
21
+ search(
22
+ query: string,
23
+ options?: GeocodingSearchOptions,
24
+ ): Promise<GeocodedAddress[]>;
25
+ }
@@ -1,7 +1,7 @@
1
1
  .grid-row {
2
2
  display: flex;
3
3
  flex-wrap: wrap;
4
- margin: 0 -15px;
4
+
5
5
  &.scroll {
6
6
  overflow-x: auto;
7
7
  flex-wrap: nowrap;
@@ -15,4 +15,4 @@
15
15
  flex-wrap: wrap;
16
16
  }
17
17
  }
18
- }
18
+ }
package/src/index.ts CHANGED
@@ -20,6 +20,8 @@ export { useToken } from "./_core/hooks/useToken";
20
20
  export { useValidate } from "./_core/hooks/useValidate";
21
21
  export { WidgetSupport } from "./_core/hooks/widgetSupport";
22
22
  export { ButtonAction } from "./action/ButtonAction";
23
+ export { AddressSearch } from "./addressSearch/AddressSearch";
24
+ export type { AddressSearchProps } from "./addressSearch/AddressSearch";
23
25
  export { Badge } from "./badge/Badge";
24
26
  export { Button } from "./button/Button";
25
27
  export { ButtonActions } from "./button/buttonActions/ButtonActions";
@@ -33,12 +35,21 @@ export { Expands } from "./expands/Expands";
33
35
  export { FilAriane } from "./filAriane/FilAriane";
34
36
  export { DynamicForm } from "./form/Form";
35
37
  export { TypeInputEnum } from "./form/form.enum";
36
- export type { FormMode } from "./form/form.interface";
38
+ export type { FormMode, FormSection } from "./form/form.interface";
37
39
  export { Input } from "./form/input/Input";
38
40
  export { InputNumber } from "./form/inputNumber/InputNumber";
39
41
  export { InputPrice } from "./form/inputPrice/InputPrice";
40
42
  export { TextArea } from "./form/textArea/TextArea";
41
43
  export { Grid } from "./grid/Grid";
44
+ export {
45
+ GeocodingService,
46
+ geocodingService,
47
+ } from "./geocoding/GeocodingService";
48
+ export type {
49
+ GeocodedAddress,
50
+ GeocodingProvider,
51
+ GeocodingSearchOptions,
52
+ } from "./geocoding/geocoding.interface";
42
53
  export { Header } from "./header/Header";
43
54
  export { Icon } from "./icon/Icon";
44
55
  export { IconSizeEnum } from "./icon/iconSize.enum";