@isi-ui7/bos7-shared 0.3.11 → 0.4.1

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.
@@ -33,7 +33,7 @@ import {
33
33
  import type { Ui7FormDensity, Ui7FormWidth } from "./style-contract";
34
34
  import { useBosSharedI18n } from "./i18n";
35
35
  import type { FormField, FormMode, FormSection } from "./form-types";
36
- import { toLocalISODate } from "./form-renderer-utils";
36
+ import { toLocalISODate, fromLocalISODate } from "./form-renderer-utils";
37
37
  import { DetailModalField } from "./detail-modal-field";
38
38
 
39
39
  export type SchemaFormRendererProps<TData extends Record<string, unknown>> = {
@@ -64,7 +64,14 @@ function asString(value: unknown): string {
64
64
  return String(value);
65
65
  }
66
66
 
67
- /** Wraps a field label with an optional required asterisk for Carbon's labelText prop. */
67
+ // Wraps a field label with an optional required asterisk for Carbon's `labelText` prop.
68
+ // The asterisk is `aria-hidden` on purpose — it's decoration for sighted users.
69
+ // The actual screen-reader signal is `aria-required`, forwarded separately onto
70
+ // each control below. Forwarded as `aria-required`, NOT native `required`: native
71
+ // `required` adds a second, browser-native validation system with its own
72
+ // message/tooltip on top of `runFieldValidation`'s in-app enforcement of
73
+ // `validation.required` — two error systems for one field. `aria-required` only
74
+ // announces, doesn't enforce, which is exactly the gap that was missing.
68
75
  function mkLabel(label: ReactNode, required?: boolean) {
69
76
  return (
70
77
  <>
@@ -225,6 +232,7 @@ function NumericField({
225
232
  invalid,
226
233
  invalidText,
227
234
  placeholder,
235
+ required,
228
236
  onChange,
229
237
  }: {
230
238
  id: string;
@@ -246,6 +254,8 @@ function NumericField({
246
254
  invalid?: boolean;
247
255
  invalidText?: string;
248
256
  placeholder?: string;
257
+ /** Forwarded as `aria-required` on the underlying `TextInput` — see `mkLabel` for why not native `required`. */
258
+ required?: boolean;
249
259
  onChange: (v: number) => void;
250
260
  }) {
251
261
  const [raw, setRaw] = useState<string | null>(null);
@@ -378,6 +388,7 @@ function NumericField({
378
388
  invalid={invalid}
379
389
  invalidText={invalidText}
380
390
  placeholder={placeholder}
391
+ aria-required={required ? true : undefined}
381
392
  />
382
393
  );
383
394
 
@@ -836,7 +847,12 @@ export function SchemaFormRenderer<TData extends Record<string, unknown>>({
836
847
  // DISIMPAN tetap ISO. Dua hal berbeda: yang satu untuk
837
848
  // dibaca manusia, yang satu kontrak dgn backend.
838
849
  dateFormat="d-m-Y"
839
- value={asString(rawValue)}
850
+ // `value` HARUS `Date`, bukan string ISO — string ISO
851
+ // di sini di-parse ULANG flatpickr seolah format
852
+ // tampilan (d-m-Y), menghasilkan tanggal lain sama
853
+ // sekali pada setiap re-render (laporan DG-00,
854
+ // agent7 2026-08-31, kena SEMUA field tanggal).
855
+ value={fromLocalISODate(asString(rawValue))}
840
856
  onChange={([date]) =>
841
857
  setValue(field.key, toLocalISODate(date) as TData[keyof TData])
842
858
  }
@@ -849,6 +865,7 @@ export function SchemaFormRenderer<TData extends Record<string, unknown>>({
849
865
  disabled={disabled}
850
866
  invalid={Boolean(invalidText)}
851
867
  invalidText={invalidText}
868
+ aria-required={effectiveRequired ? true : undefined}
852
869
  />
853
870
  </DatePicker>
854
871
  {field.helperText ? (
@@ -872,6 +889,7 @@ export function SchemaFormRenderer<TData extends Record<string, unknown>>({
872
889
  disabled={disabled}
873
890
  invalid={Boolean(invalidText)}
874
891
  invalidText={invalidText}
892
+ aria-required={effectiveRequired ? true : undefined}
875
893
  >
876
894
  {(field.options ?? []).map((opt) => (
877
895
  <SelectItem key={String(opt.value)} value={opt.value} text={opt.label} />
@@ -944,6 +962,7 @@ export function SchemaFormRenderer<TData extends Record<string, unknown>>({
944
962
  invalid={Boolean(effectiveInvalidText)}
945
963
  invalidText={effectiveInvalidText}
946
964
  placeholder={field.placeholder}
965
+ required={effectiveRequired}
947
966
  onChange={(v) => setValue(field.key, v as TData[keyof TData])}
948
967
  />
949
968
  </div>,
@@ -962,6 +981,7 @@ export function SchemaFormRenderer<TData extends Record<string, unknown>>({
962
981
  setValue(field.key, Boolean(checked) as TData[keyof TData])
963
982
  }
964
983
  disabled={disabled}
984
+ aria-required={effectiveRequired ? true : undefined}
965
985
  />
966
986
  </div>,
967
987
  ];
@@ -999,6 +1019,7 @@ export function SchemaFormRenderer<TData extends Record<string, unknown>>({
999
1019
  )
1000
1020
  }
1001
1021
  disabled={disabled}
1022
+ aria-required={effectiveRequired ? true : undefined}
1002
1023
  />
1003
1024
  </div>
1004
1025
  </FieldShell>
@@ -1023,6 +1044,7 @@ export function SchemaFormRenderer<TData extends Record<string, unknown>>({
1023
1044
  maxLength={field.maxLength}
1024
1045
  invalid={Boolean(invalidText)}
1025
1046
  invalidText={invalidText}
1047
+ aria-required={effectiveRequired ? true : undefined}
1026
1048
  />
1027
1049
  </div>,
1028
1050
  ];
@@ -1049,6 +1071,7 @@ export function SchemaFormRenderer<TData extends Record<string, unknown>>({
1049
1071
  readOnly={effectiveReadonly}
1050
1072
  invalid={Boolean(invalidText)}
1051
1073
  invalidText={invalidText}
1074
+ aria-required={effectiveRequired ? true : undefined}
1052
1075
  />
1053
1076
  </div>,
1054
1077
  ];
@@ -1076,6 +1099,7 @@ export function SchemaFormRenderer<TData extends Record<string, unknown>>({
1076
1099
  invalid={Boolean(invalidText)}
1077
1100
  invalidText={invalidText}
1078
1101
  placeholder={field.placeholder}
1102
+ aria-required={effectiveRequired ? true : undefined}
1079
1103
  />
1080
1104
  </div>,
1081
1105
  ];
@@ -1099,6 +1123,7 @@ export function SchemaFormRenderer<TData extends Record<string, unknown>>({
1099
1123
  invalid={Boolean(invalidText)}
1100
1124
  invalidText={invalidText}
1101
1125
  placeholder={field.placeholder}
1126
+ aria-required={effectiveRequired ? true : undefined}
1102
1127
  />
1103
1128
  </div>,
1104
1129
  ];
@@ -20,3 +20,17 @@ if (typeof Element !== "undefined" && !Element.prototype.scrollIntoView) {
20
20
  /* no-op: jsdom nol punya viewport untuk digulir */
21
21
  };
22
22
  }
23
+
24
+ // Sama alasannya, komponen beda: jsdom nol mengimplementasikan `ResizeObserver`.
25
+ // Carbon `TextArea` memanggilnya (auto-resize) saat mount — tanpa tambalan ini,
26
+ // SETIAP uji yang merender `TextArea` (langsung atau lewat `SchemaFormRenderer`)
27
+ // melempar `ReferenceError` di layout-effect, di luar rantai promise uji — kelas
28
+ // kegagalan yang sama: ringkasan `it()` bisa tetap hijau sementara exit code
29
+ // menolak. No-op sengaja: yang diuji adalah apa yang terender, bukan resize.
30
+ if (typeof globalThis.ResizeObserver === "undefined") {
31
+ globalThis.ResizeObserver = class ResizeObserver {
32
+ observe() {}
33
+ unobserve() {}
34
+ disconnect() {}
35
+ } as unknown as typeof ResizeObserver;
36
+ }