@konce-pt/react 0.9.1 → 0.11.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.
Files changed (200) hide show
  1. package/dist/accordion/accordion.js +7 -1
  2. package/dist/accordion/llms.txt +8 -4
  3. package/dist/alert/alert.d.ts +10 -4
  4. package/dist/alert/alert.js +13 -6
  5. package/dist/alert/llms.txt +8 -6
  6. package/dist/app-shell/app-shell.js +28 -2
  7. package/dist/app-shell/llms.txt +3 -0
  8. package/dist/autocomplete/autocomplete.d.ts +7 -2
  9. package/dist/autocomplete/autocomplete.js +46 -9
  10. package/dist/autocomplete/llms.txt +12 -0
  11. package/dist/avatar/avatar.d.ts +4 -2
  12. package/dist/avatar/avatar.js +8 -3
  13. package/dist/avatar/llms.txt +9 -5
  14. package/dist/avatar-group/avatar-group.d.ts +5 -2
  15. package/dist/avatar-group/avatar-group.js +10 -2
  16. package/dist/avatar-group/llms.txt +7 -4
  17. package/dist/badge/badge.d.ts +8 -1
  18. package/dist/badge/badge.js +3 -2
  19. package/dist/badge/llms.txt +11 -5
  20. package/dist/bottom-sheet/bottom-sheet.d.ts +1 -1
  21. package/dist/bottom-sheet/bottom-sheet.js +8 -2
  22. package/dist/bottom-sheet/llms.txt +8 -2
  23. package/dist/breadcrumb/breadcrumb.js +4 -4
  24. package/dist/breadcrumb/llms.txt +12 -5
  25. package/dist/card/card.d.ts +9 -1
  26. package/dist/card/card.js +2 -2
  27. package/dist/card/llms.txt +6 -2
  28. package/dist/carousel/carousel.d.ts +8 -1
  29. package/dist/carousel/carousel.js +9 -2
  30. package/dist/carousel/llms.txt +24 -10
  31. package/dist/checkbox/checkbox.d.ts +6 -1
  32. package/dist/checkbox/checkbox.js +11 -3
  33. package/dist/checkbox/llms.txt +11 -0
  34. package/dist/chip/chip.js +10 -1
  35. package/dist/chip/llms.txt +7 -4
  36. package/dist/chips-input/chips-input.d.ts +7 -1
  37. package/dist/chips-input/chips-input.js +21 -5
  38. package/dist/chips-input/llms.txt +12 -0
  39. package/dist/color-picker/color-picker.d.ts +18 -5
  40. package/dist/color-picker/color-picker.js +48 -14
  41. package/dist/color-picker/llms.txt +14 -0
  42. package/dist/confirm/confirm-container.d.ts +5 -1
  43. package/dist/confirm/confirm-container.js +15 -1
  44. package/dist/confirm/llms.txt +4 -2
  45. package/dist/context-menu/context-menu.d.ts +5 -0
  46. package/dist/context-menu/context-menu.js +48 -4
  47. package/dist/context-menu/llms.txt +11 -4
  48. package/dist/date-range/date-range.d.ts +14 -1
  49. package/dist/date-range/date-range.js +133 -17
  50. package/dist/date-range/llms.txt +26 -3
  51. package/dist/datepicker/datepicker.d.ts +10 -1
  52. package/dist/datepicker/datepicker.js +147 -14
  53. package/dist/datepicker/llms.txt +27 -3
  54. package/dist/dialog/dialog.d.ts +4 -0
  55. package/dist/dialog/dialog.js +7 -1
  56. package/dist/dialog/llms.txt +2 -0
  57. package/dist/drawer/drawer.js +7 -2
  58. package/dist/drawer/llms.txt +10 -5
  59. package/dist/empty/empty.d.ts +6 -1
  60. package/dist/empty/empty.js +2 -2
  61. package/dist/empty/llms.txt +4 -3
  62. package/dist/fab/llms.txt +2 -1
  63. package/dist/file-upload/file-upload.d.ts +5 -0
  64. package/dist/file-upload/file-upload.js +24 -3
  65. package/dist/file-upload/llms.txt +15 -4
  66. package/dist/form-field/form-field-context.d.ts +30 -1
  67. package/dist/form-field/form-field-context.js +27 -0
  68. package/dist/form-field/form-field.js +16 -2
  69. package/dist/form-field/llms.txt +10 -1
  70. package/dist/galleria/galleria.d.ts +6 -1
  71. package/dist/galleria/galleria.js +40 -1
  72. package/dist/galleria/llms.txt +17 -8
  73. package/dist/icon/icon.d.ts +7 -1
  74. package/dist/icon/icon.js +18 -4
  75. package/dist/icon/llms.txt +5 -2
  76. package/dist/image/image.d.ts +5 -0
  77. package/dist/image/image.js +38 -2
  78. package/dist/image/llms.txt +24 -17
  79. package/dist/input/input.d.ts +1 -1
  80. package/dist/input/input.js +4 -6
  81. package/dist/input/llms.txt +5 -0
  82. package/dist/input-mask/input-mask.d.ts +1 -1
  83. package/dist/input-mask/input-mask.js +8 -6
  84. package/dist/input-mask/llms.txt +11 -0
  85. package/dist/input-number/input-number.d.ts +13 -1
  86. package/dist/input-number/input-number.js +49 -9
  87. package/dist/input-number/llms.txt +21 -3
  88. package/dist/input-otp/input-otp.d.ts +7 -2
  89. package/dist/input-otp/input-otp.js +36 -7
  90. package/dist/input-otp/llms.txt +14 -0
  91. package/dist/internal/calendar-focus.d.ts +8 -0
  92. package/dist/internal/calendar-focus.js +38 -0
  93. package/dist/internal/option-nav.d.ts +24 -0
  94. package/dist/internal/option-nav.js +80 -0
  95. package/dist/internal/overlay.d.ts +7 -2
  96. package/dist/internal/overlay.js +4 -2
  97. package/dist/internal/roving.d.ts +9 -0
  98. package/dist/internal/roving.js +43 -0
  99. package/dist/internal/tween.d.ts +17 -0
  100. package/dist/internal/tween.js +40 -0
  101. package/dist/internal/use-modal-focus.d.ts +10 -0
  102. package/dist/internal/use-modal-focus.js +57 -0
  103. package/dist/knob/knob.d.ts +9 -1
  104. package/dist/knob/knob.js +41 -5
  105. package/dist/knob/llms.txt +15 -0
  106. package/dist/listbox/listbox.d.ts +13 -1
  107. package/dist/listbox/listbox.js +89 -6
  108. package/dist/listbox/llms.txt +21 -5
  109. package/dist/megamenu/llms.txt +28 -10
  110. package/dist/megamenu/megamenu.d.ts +10 -1
  111. package/dist/megamenu/megamenu.js +116 -5
  112. package/dist/menu/llms.txt +12 -1
  113. package/dist/menu/menu-keys.d.ts +15 -0
  114. package/dist/menu/menu-keys.js +53 -0
  115. package/dist/menu/menu-rows.js +1 -1
  116. package/dist/menu/menu.js +52 -4
  117. package/dist/menubar/llms.txt +24 -7
  118. package/dist/menubar/menubar.d.ts +8 -1
  119. package/dist/menubar/menubar.js +92 -6
  120. package/dist/meter-group/llms.txt +10 -3
  121. package/dist/meter-group/meter-group.d.ts +9 -2
  122. package/dist/meter-group/meter-group.js +8 -2
  123. package/dist/order-list/llms.txt +11 -3
  124. package/dist/order-list/order-list.d.ts +5 -1
  125. package/dist/order-list/order-list.js +46 -8
  126. package/dist/panel/llms.txt +9 -6
  127. package/dist/panel/panel.js +3 -1
  128. package/dist/password/llms.txt +8 -0
  129. package/dist/password/password.d.ts +6 -1
  130. package/dist/password/password.js +6 -5
  131. package/dist/pick-list/llms.txt +12 -3
  132. package/dist/pick-list/pick-list.d.ts +5 -1
  133. package/dist/pick-list/pick-list.js +52 -5
  134. package/dist/popover/llms.txt +9 -2
  135. package/dist/popover/popover.d.ts +9 -3
  136. package/dist/popover/popover.js +41 -6
  137. package/dist/public-api.d.ts +6 -6
  138. package/dist/public-api.js +1 -1
  139. package/dist/radio-group/llms.txt +11 -0
  140. package/dist/radio-group/radio-group.d.ts +9 -1
  141. package/dist/radio-group/radio-group.js +5 -2
  142. package/dist/rating/llms.txt +18 -5
  143. package/dist/rating/rating.d.ts +21 -3
  144. package/dist/rating/rating.js +87 -12
  145. package/dist/rich-text/llms.txt +20 -0
  146. package/dist/rich-text/rich-text.d.ts +7 -1
  147. package/dist/rich-text/rich-text.js +155 -28
  148. package/dist/scroll-top/llms.txt +5 -1
  149. package/dist/scroll-top/scroll-top.js +3 -1
  150. package/dist/select/llms.txt +21 -7
  151. package/dist/select/select.d.ts +12 -1
  152. package/dist/select/select.js +110 -38
  153. package/dist/sidenav/llms.txt +6 -0
  154. package/dist/skeleton/llms.txt +3 -2
  155. package/dist/skeleton/skeleton.d.ts +1 -1
  156. package/dist/skeleton/skeleton.js +2 -2
  157. package/dist/slider/llms.txt +14 -1
  158. package/dist/slider/slider.d.ts +11 -2
  159. package/dist/slider/slider.js +15 -2
  160. package/dist/speed-dial/llms.txt +6 -2
  161. package/dist/speed-dial/speed-dial.d.ts +1 -1
  162. package/dist/speed-dial/speed-dial.js +14 -3
  163. package/dist/spinner/llms.txt +12 -8
  164. package/dist/spinner/spinner.d.ts +5 -1
  165. package/dist/spinner/spinner.js +12 -2
  166. package/dist/split-button/llms.txt +11 -3
  167. package/dist/split-button/split-button.d.ts +3 -1
  168. package/dist/split-button/split-button.js +43 -3
  169. package/dist/splitter/llms.txt +14 -4
  170. package/dist/splitter/splitter.d.ts +9 -2
  171. package/dist/splitter/splitter.js +15 -2
  172. package/dist/stepper/llms.txt +8 -4
  173. package/dist/stepper/stepper.d.ts +3 -2
  174. package/dist/stepper/stepper.js +7 -2
  175. package/dist/switch/llms.txt +10 -0
  176. package/dist/switch/switch.d.ts +1 -1
  177. package/dist/switch/switch.js +4 -2
  178. package/dist/switch-group/llms.txt +11 -0
  179. package/dist/switch-group/switch-group.d.ts +5 -1
  180. package/dist/switch-group/switch-group.js +7 -2
  181. package/dist/tabs/llms.txt +10 -4
  182. package/dist/tabs/tabs.d.ts +4 -0
  183. package/dist/tabs/tabs.js +21 -2
  184. package/dist/textarea/llms.txt +10 -0
  185. package/dist/textarea/textarea.d.ts +3 -1
  186. package/dist/textarea/textarea.js +13 -8
  187. package/dist/toast/llms.txt +21 -7
  188. package/dist/toast/toast-container.d.ts +6 -1
  189. package/dist/toast/toast-container.js +52 -1
  190. package/dist/toast/toast-store.d.ts +12 -0
  191. package/dist/toast/toast-store.js +46 -7
  192. package/dist/tooltip/llms.txt +10 -3
  193. package/dist/tooltip/tooltip.d.ts +5 -0
  194. package/dist/tooltip/tooltip.js +80 -7
  195. package/dist/tree/llms.txt +20 -8
  196. package/dist/tree/tree-keys.d.ts +27 -0
  197. package/dist/tree/tree-keys.js +39 -0
  198. package/dist/tree/tree.d.ts +12 -5
  199. package/dist/tree/tree.js +75 -29
  200. package/package.json +12 -12
@@ -6,8 +6,14 @@
6
6
  * zbudowany na liście) zgłaszają je przez `useKptFormFieldFilled`.
7
7
  */
8
8
  export interface KptFormFieldContextValue {
9
- /** Id, którym wrapper wiąże `label[for]` — kontrolka powinna nadać je swojemu elementowi. */
9
+ /** Id, którym wrapper wiąże `label[for]` — kontrolka bez własnego `id` nadaje je swojemu elementowi. */
10
10
  controlId: string;
11
+ /** Id elementu etykiety albo `undefined`, gdy wrapper jej nie pokazuje. */
12
+ labelId: string | undefined;
13
+ /** Id widocznego komunikatu (błąd albo podpowiedź) albo `undefined`. */
14
+ describedBy: string | undefined;
15
+ /** Kontrolka z własnym `id` zgłasza je wrapperowi — na nie wskaże `label[for]`. */
16
+ reportControlId: (id: string | null) => void;
11
17
  /** Czy pole jest oznaczone jako nieprawidłowe (niepusty `error` na wrapperze). */
12
18
  invalid: boolean;
13
19
  /** Zgłasza wypełnienie kontrolki, gdy nie da się go odczytać z DOM. */
@@ -21,3 +27,26 @@ export declare function useKptFormFieldContext(): KptFormFieldContextValue | nul
21
27
  * Steruje uniesieniem etykiety w wariantach float.
22
28
  */
23
29
  export declare function useKptFormFieldFilled(filled: boolean): void;
30
+ /** To, czego kontrolka potrzebuje, żeby wrapper ją nazwał i opisał. */
31
+ export interface KptFieldA11y {
32
+ /** Id elementu fokusowalnego: własne `id`, potem `controlId` wrappera, potem wygenerowane. */
33
+ id: string;
34
+ /**
35
+ * Id etykiety wrappera — dla kontrolek na elemencie, którego `label[for]` nie nazywa
36
+ * (`div` z rolą, `contenteditable`). Natywne `input`/`textarea` nazywa już samo `htmlFor`.
37
+ */
38
+ labelledBy: string | undefined;
39
+ /** Id podpowiedzi albo błędu wrappera — do `aria-describedby`. */
40
+ describedBy: string | undefined;
41
+ }
42
+ /**
43
+ * Wiąże kontrolkę z otaczającym `KptFormField`. Poza wrapperem zwraca samo `id` (własne albo
44
+ * wygenerowane) i puste powiązania. Bliźniak `kptFieldA11y` z portu Angulara.
45
+ *
46
+ * @example
47
+ * const field = useKptFieldA11y(id, 'kpt-textarea');
48
+ * <textarea id={field.id} aria-describedby={field.describedBy} />
49
+ */
50
+ export declare function useKptFieldA11y(provided: string | undefined, prefix: string): KptFieldA11y;
51
+ /** Łączy kilka id w wartość `aria-describedby`/`aria-labelledby`; `undefined`, gdy nie ma żadnego. */
52
+ export declare function joinIds(...ids: (string | null | undefined | false)[]): string | undefined;
@@ -1,4 +1,5 @@
1
1
  import { createContext, useContext, useEffect } from 'react';
2
+ import { useKptId } from "../internal/use-id.js";
2
3
  export const KptFormFieldContext = createContext(null);
3
4
  /** Kontekst pola formularza albo `null`, gdy kontrolka stoi poza `KptFormField`. */
4
5
  export function useKptFormFieldContext() {
@@ -15,3 +16,29 @@ export function useKptFormFieldFilled(filled) {
15
16
  reportFilled?.(filled);
16
17
  }, [reportFilled, filled]);
17
18
  }
19
+ /**
20
+ * Wiąże kontrolkę z otaczającym `KptFormField`. Poza wrapperem zwraca samo `id` (własne albo
21
+ * wygenerowane) i puste powiązania. Bliźniak `kptFieldA11y` z portu Angulara.
22
+ *
23
+ * @example
24
+ * const field = useKptFieldA11y(id, 'kpt-textarea');
25
+ * <textarea id={field.id} aria-describedby={field.describedBy} />
26
+ */
27
+ export function useKptFieldA11y(provided, prefix) {
28
+ const context = useKptFormFieldContext();
29
+ const id = useKptId(provided ?? context?.controlId, prefix);
30
+ const report = context?.reportControlId;
31
+ const own = provided ?? null;
32
+ useEffect(() => {
33
+ if (!report || !own)
34
+ return;
35
+ report(own);
36
+ return () => report(null);
37
+ }, [report, own]);
38
+ return { id, labelledBy: context?.labelId, describedBy: context?.describedBy };
39
+ }
40
+ /** Łączy kilka id w wartość `aria-describedby`/`aria-labelledby`; `undefined`, gdy nie ma żadnego. */
41
+ export function joinIds(...ids) {
42
+ const list = ids.filter((id) => !!id);
43
+ return list.length ? list.join(' ') : undefined;
44
+ }
@@ -23,7 +23,14 @@ export function KptFormField({ label = '', hint = '', error = null, required = f
23
23
  const [nativeFilled, setNativeFilled] = useState(false);
24
24
  const [reportedFilled, setReportedFilled] = useState(null);
25
25
  const generatedId = useKptId(undefined, 'kpt-form-field-control');
26
+ const baseId = useKptId(undefined, 'kpt-form-field');
26
27
  const id = controlId || generatedId;
28
+ // Kontrolka z własnym `id` zgłasza je przez kontekst — wtedy `label[for]` wskazuje właśnie je.
29
+ const [reportedId, setReportedId] = useState(null);
30
+ const labelFor = controlId || reportedId || generatedId;
31
+ const labelId = label ? `${baseId}-label` : undefined;
32
+ const messageId = `${baseId}-message`;
33
+ const describedBy = error || hint ? messageId : undefined;
27
34
  /** Odczytuje wartość projektowanego `input`/`textarea` i aktualizuje stan wypełnienia. */
28
35
  const updateFilled = useCallback(() => {
29
36
  const el = hostRef.current?.querySelector('.kpt-form-field__control input, .kpt-form-field__control textarea');
@@ -36,6 +43,13 @@ export function KptFormField({ label = '', hint = '', error = null, required = f
36
43
  // powtarzamy po każdym renderze — to jeden `querySelector`, nie warto tu optymalizować.
37
44
  useEffect(updateFilled);
38
45
  const filled = reportedFilled ?? nativeFilled;
39
- const context = useMemo(() => ({ controlId: id, invalid: !!error, reportFilled: setReportedFilled }), [id, error]);
40
- return (_jsx(KptFormFieldContext.Provider, { value: context, children: _jsx("div", { className: cn('kpt-form-field-host', className), ref: composeRefs(hostRef, ref), onInput: updateFilled, onChange: updateFilled, ...rest, children: _jsxs("div", { className: cn('kpt-form-field', `kpt-form-field--${labelType}`, labelType !== 'fixed' && 'kpt-form-field--float', error && 'kpt-form-field--invalid'), children: [_jsxs("div", { className: cn('kpt-form-field__box', filled && 'is-filled'), children: [label ? (_jsxs("label", { className: "kpt-form-field__label", htmlFor: id || undefined, children: [label, required ? (_jsx("span", { className: "kpt-form-field__required", "aria-hidden": "true", children: "*" })) : null] })) : null, _jsx("div", { className: "kpt-form-field__control", children: children })] }), error ? (_jsx("p", { className: "kpt-form-field__message kpt-form-field__message--error", role: "alert", children: error })) : hint ? (_jsx("p", { className: "kpt-form-field__message kpt-form-field__message--hint", children: hint })) : null] }) }) }));
46
+ const context = useMemo(() => ({
47
+ controlId: id,
48
+ labelId,
49
+ describedBy,
50
+ invalid: !!error,
51
+ reportFilled: setReportedFilled,
52
+ reportControlId: setReportedId,
53
+ }), [id, labelId, describedBy, error]);
54
+ return (_jsx(KptFormFieldContext.Provider, { value: context, children: _jsx("div", { className: cn('kpt-form-field-host', className), ref: composeRefs(hostRef, ref), onInput: updateFilled, onChange: updateFilled, ...rest, children: _jsxs("div", { className: cn('kpt-form-field', `kpt-form-field--${labelType}`, labelType !== 'fixed' && 'kpt-form-field--float', error && 'kpt-form-field--invalid'), children: [_jsxs("div", { className: cn('kpt-form-field__box', filled && 'is-filled'), children: [label ? (_jsxs("label", { className: "kpt-form-field__label", id: labelId, htmlFor: labelFor, children: [label, required ? (_jsx("span", { className: "kpt-form-field__required", "aria-hidden": "true", children: "*" })) : null] })) : null, _jsx("div", { className: "kpt-form-field__control", children: children })] }), error ? (_jsx("p", { className: "kpt-form-field__message kpt-form-field__message--error", role: "alert", id: messageId, children: error })) : hint ? (_jsx("p", { className: "kpt-form-field__message kpt-form-field__message--hint", id: messageId, children: hint })) : null] }) }) }));
41
55
  }
@@ -25,7 +25,16 @@ Import: `import { KptFormField } from '@konce-pt/react';`
25
25
 
26
26
  ## Label wiring
27
27
  The wrapper generates a control id and publishes it through context, so `<KptInput>` inside needs
28
- no configuration — `label[for]` and `input[id]` always match. Pass `controlId` to take over.
28
+ no configuration — `label[for]` and `input[id]` always match. Pass `controlId` to take over; a control
29
+ with its own `id` reports it, and the label follows.
30
+
31
+ The context also carries the label's id and the id of the visible hint or error. Every library
32
+ control uses them through `useKptFieldA11y`: the hint or error lands in `aria-describedby`, and a
33
+ control on a `div` with a role (`KptSelect`, `KptDatepicker`, `KptRadioGroup`, `KptRichText`…) gets
34
+ `aria-labelledby` pointing at the label, because `label[for]` does not name it. A custom control:
35
+
36
+ const field = useKptFieldA11y(id, 'my-control');
37
+ <div role="slider" id={field.id} aria-labelledby={field.labelledBy} aria-describedby={field.describedBy} />
29
38
 
30
39
  ## Float labels
31
40
  `labelType` other than `fixed` turns on the floating label with an animated transition: `over`
@@ -1,4 +1,4 @@
1
- import type { ComponentPropsWithRef } from 'react';
1
+ import { type ComponentPropsWithRef } from 'react';
2
2
  import { type KptI18nOverrides } from '../i18n/i18n-context.tsx';
3
3
  export interface KptGalleriaItem {
4
4
  src: string;
@@ -16,6 +16,11 @@ export interface KptGalleriaProps extends Omit<ComponentPropsWithRef<'div'>, 'on
16
16
  /**
17
17
  * Galleria — duży podgląd obrazu z paskiem miniatur i nawigacją.
18
18
  *
19
+ * Klawiatura: na całej galerii strzałki w lewo/prawo przechodzą do sąsiedniego obrazu (w RTL
20
+ * odwrotnie), `Home`/`End` — do pierwszego/ostatniego; gdy fokus stoi na miniaturze, idzie za
21
+ * aktywną. Miniatura ma nazwę z `alt` obrazu i `aria-current` na aktywnej, a zmiana obrazu jest
22
+ * ogłaszana uprzejmie („Obraz 2 z 4: …”) — dopiero po działaniu użytkownika, nie przy starcie.
23
+ *
19
24
  * @example
20
25
  * <KptGalleria items={photos} activeIndex={index} onActiveIndexChange={setIndex} />
21
26
  */
@@ -1,11 +1,19 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRef, useState } from 'react';
2
3
  import { cn } from "../internal/cn.js";
3
4
  import { useControllableState } from "../internal/use-controllable-state.js";
4
5
  import { useKptMessages } from "../i18n/i18n-context.js";
5
6
  import { KptIcon } from "../icon/icon.js";
7
+ /** Klawisze przechodzące między obrazami: kierunek względem bieżącego albo skrajny indeks. */
8
+ const NAV_KEYS = new Set(['ArrowLeft', 'ArrowRight', 'Home', 'End']);
6
9
  /**
7
10
  * Galleria — duży podgląd obrazu z paskiem miniatur i nawigacją.
8
11
  *
12
+ * Klawiatura: na całej galerii strzałki w lewo/prawo przechodzą do sąsiedniego obrazu (w RTL
13
+ * odwrotnie), `Home`/`End` — do pierwszego/ostatniego; gdy fokus stoi na miniaturze, idzie za
14
+ * aktywną. Miniatura ma nazwę z `alt` obrazu i `aria-current` na aktywnej, a zmiana obrazu jest
15
+ * ogłaszana uprzejmie („Obraz 2 z 4: …”) — dopiero po działaniu użytkownika, nie przy starcie.
16
+ *
9
17
  * @example
10
18
  * <KptGalleria items={photos} activeIndex={index} onActiveIndexChange={setIndex} />
11
19
  */
@@ -16,6 +24,37 @@ export function KptGalleria({ items = [], activeIndex, defaultActiveIndex = 0, o
16
24
  defaultValue: defaultActiveIndex,
17
25
  onChange: onActiveIndexChange,
18
26
  });
27
+ // Ogłoszenie dopiero po działaniu użytkownika — przy starcie region ma milczeć.
28
+ const [userMoved, setUserMoved] = useState(false);
29
+ const thumbsRef = useRef([]);
30
+ const rootRef = useRef(null);
19
31
  const current = items[active] ?? null;
20
- return (_jsx("div", { className: cn('kpt-galleria-host', className), ...rest, children: _jsxs("div", { className: "kpt-galleria", children: [_jsxs("div", { className: "kpt-galleria__stage", children: [_jsx("button", { type: "button", className: "kpt-galleria__nav kpt-galleria__nav--prev", disabled: active === 0, "aria-label": t('common.previous'), onClick: () => setActive(Math.max(active - 1, 0)), children: _jsx(KptIcon, { name: "chevron-left" }) }), current ? _jsx("img", { className: "kpt-galleria__image", src: current.src, alt: current.alt || '' }) : null, _jsx("button", { type: "button", className: "kpt-galleria__nav kpt-galleria__nav--next", disabled: active >= items.length - 1, "aria-label": t('common.next'), onClick: () => setActive(Math.min(active + 1, items.length - 1)), children: _jsx(KptIcon, { name: "chevron-right" }) })] }), showThumbnails ? (_jsx("div", { className: "kpt-galleria__thumbs", children: items.map((item, index) => (_jsx("button", { type: "button", className: cn('kpt-galleria__thumb', index === active && 'is-active'), "aria-label": t('galleria.image', { index: index + 1 }), onClick: () => setActive(index), children: _jsx("img", { src: item.thumbnail || item.src, alt: item.alt || '' }) }, index))) })) : null] }) }));
32
+ const last = items.length - 1;
33
+ const select = (index) => {
34
+ if (last < 0)
35
+ return active;
36
+ const next = Math.min(Math.max(index, 0), last);
37
+ setUserMoved(true);
38
+ setActive(next);
39
+ return next;
40
+ };
41
+ const onKeyDown = (event) => {
42
+ if (!NAV_KEYS.has(event.key))
43
+ return;
44
+ event.preventDefault();
45
+ const rtl = rootRef.current ? getComputedStyle(rootRef.current).direction === 'rtl' : false;
46
+ const step = event.key === 'ArrowRight' ? 1 : -1;
47
+ const target = event.key === 'Home' ? 0 : event.key === 'End' ? last : active + (rtl ? -step : step);
48
+ const focusOnThumb = thumbsRef.current.some((thumb) => thumb === document.activeElement);
49
+ const next = select(target);
50
+ // Fokus na miniaturze idzie za aktywnym obrazem — inaczej strzałka zmieniałaby obraz,
51
+ // a fokus zostawał na poprzedniej miniaturze.
52
+ if (focusOnThumb)
53
+ thumbsRef.current[next]?.focus();
54
+ };
55
+ const position = t('galleria.position', { index: active + 1, total: items.length });
56
+ const announcement = userMoved ? (current?.alt ? `${position}: ${current.alt}` : position) : '';
57
+ return (_jsx("div", { className: cn('kpt-galleria-host', className), ...rest, children: _jsxs("div", { ref: rootRef, className: "kpt-galleria", onKeyDown: onKeyDown, children: [_jsxs("div", { className: "kpt-galleria__stage", children: [_jsx("button", { type: "button", className: "kpt-galleria__nav kpt-galleria__nav--prev", "aria-disabled": active <= 0 || undefined, "aria-label": t('common.previous'), onClick: () => select(active - 1), children: _jsx(KptIcon, { name: "chevron-left" }) }), current ? _jsx("img", { className: "kpt-galleria__image", src: current.src, alt: current.alt || '' }) : null, _jsx("button", { type: "button", className: "kpt-galleria__nav kpt-galleria__nav--next", "aria-disabled": active >= last || undefined, "aria-label": t('common.next'), onClick: () => select(active + 1), children: _jsx(KptIcon, { name: "chevron-right" }) })] }), showThumbnails ? (_jsx("div", { className: "kpt-galleria__thumbs", children: items.map((item, index) => (_jsx("button", { ref: (el) => {
58
+ thumbsRef.current[index] = el;
59
+ }, type: "button", className: cn('kpt-galleria__thumb', index === active && 'is-active'), "aria-current": index === active ? 'true' : undefined, "aria-label": item.alt || t('galleria.image', { index: index + 1 }), onClick: () => select(index), children: _jsx("img", { src: item.thumbnail || item.src, alt: "" }) }, index))) })) : null, _jsx("span", { className: "kpt-galleria__live", "aria-live": "polite", children: announcement })] }) }));
21
60
  }
@@ -7,13 +7,14 @@ Import: `import { KptGalleria, type KptGalleriaItem } from '@konce-pt/react';`
7
7
  <div class="kpt-galleria-host">
8
8
  <div class="kpt-galleria">
9
9
  <div class="kpt-galleria__stage">
10
- <button class="kpt-galleria__nav kpt-galleria__nav--prev">…</button>
11
- <img class="kpt-galleria__image" src="…" alt="…">
12
- <button class="kpt-galleria__nav kpt-galleria__nav--next">…</button>
10
+ <button class="kpt-galleria__nav kpt-galleria__nav--prev" aria-disabled="true" aria-label="Previous">…</button>
11
+ <img class="kpt-galleria__image" src="…" alt="Mountain lake">
12
+ <button class="kpt-galleria__nav kpt-galleria__nav--next" aria-label="Next">…</button>
13
13
  </div>
14
14
  <div class="kpt-galleria__thumbs">
15
- <button class="kpt-galleria__thumb is-active"><img src="…"></button>…
15
+ <button class="kpt-galleria__thumb is-active" aria-current="true" aria-label="Mountain lake"><img src="…" alt=""></button>…
16
16
  </div>
17
+ <span class="kpt-galleria__live" aria-live="polite">Image 2 of 4: Pine forest</span>
17
18
  </div>
18
19
  </div>
19
20
 
@@ -36,11 +37,19 @@ data is still loading shows the stage and the disabled arrows instead of crashin
36
37
  <KptGalleria items={photos} activeIndex={index} onActiveIndexChange={setIndex} showThumbnails={false} />
37
38
 
38
39
  ## i18n
39
- The arrows use `common.previous` / `common.next`, the thumbnails `galleria.image` (`{index}`).
40
+ The arrows use `common.previous` / `common.next`, the thumbnails without `alt` `galleria.image` (`{index}`), the announcement `galleria.position` (`{index}`, `{total}`).
41
+
42
+ ## Keyboard
43
+ Anywhere inside the gallery, `ArrowLeft`/`ArrowRight` move to the neighbouring image (reversed in
44
+ RTL) and `Home`/`End` to the first/last. When focus is on a thumbnail, it follows the active one.
40
45
 
41
46
  ## Tokens
42
- The stage `--kpt-color-muted`, the active thumbnail `--kpt-color-primary`.
47
+ The stage `--kpt-color-muted`; the active thumbnail `--kpt-color-primary`; the arrows `--kpt-color-surface` (85% via `color-mix`, solid on hover) / `--kpt-color-on-surface` with `--kpt-elevation-2`.
43
48
 
44
49
  ## Accessibility
45
- Each thumbnail is a labelled button, and the arrows go dark at the ends; `alt` comes from the item,
46
- so a photo without a description does not get a made-up one.
50
+ The large image carries its `alt`. Each thumbnail is a button named by the item's `alt` (or
51
+ `galleria.image` — “Image 3” — when there is none), with `alt=""` on the thumbnail image so the
52
+ name is not read twice; the active one carries `aria-current`. The arrows use `aria-disabled` at
53
+ the ends instead of `disabled`, so they keep the focus. A change of image is announced politely
54
+ through a hidden live region — `galleria.position` (“Image 2 of 4”) plus the `alt` — only after the
55
+ user acts, never on first render.
@@ -4,14 +4,20 @@ export interface KptIconProps extends ComponentPropsWithRef<'span'> {
4
4
  name: string;
5
5
  /** Rozmiar jako długość CSS (np. '1.25rem'); domyślnie dziedziczy z `font-size`. */
6
6
  size?: string;
7
+ /** Nazwa dla czytnika ekranu — tylko gdy ikona niesie znaczenie bez tekstu obok. */
8
+ label?: string;
7
9
  }
8
10
  /**
9
11
  * Ikona Koncept UI (inline SVG, zestaw Tabler Icons — MIT). Rozmiar dziedziczy z `font-size`
10
12
  * (1em) lub ustaw `size`; kolor z `currentColor`. Ikona musi być w rejestrze — wbudowany
11
13
  * zestaw działa od razu, pełny dodaje `registerKptTablerIcons()` z `@konce-pt/react/icons`.
12
14
  *
15
+ * Domyślnie ikona jest dekoracją (`aria-hidden`). Ikona niosąca znaczenie sama, bez tekstu obok,
16
+ * dostaje `label` — wtedy jest `role="img"` z tą nazwą. W buildzie deweloperskim nieznana nazwa
17
+ * daje jedno ostrzeżenie w konsoli zamiast cichej pustki.
18
+ *
13
19
  * @example
14
20
  * <KptIcon name="menu" />
15
21
  * <KptIcon name="check" size="1.25rem" />
16
22
  */
17
- export declare function KptIcon({ name, size, className, style, ...rest }: Readonly<KptIconProps>): import("react").JSX.Element;
23
+ export declare function KptIcon({ name, size, label, className, style, ...rest }: Readonly<KptIconProps>): import("react").JSX.Element;
package/dist/icon/icon.js CHANGED
@@ -1,23 +1,37 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { renderKptIconSvg } from '@konce-pt/icons';
3
- import { useSyncExternalStore } from 'react';
3
+ import { useEffect, useSyncExternalStore } from 'react';
4
4
  import { cn } from "../internal/cn.js";
5
+ import { isDevBuild } from "../internal/dev.js";
5
6
  import { getKptIcon, getKptIconsVersion, subscribeKptIcons } from "./icon-registry.js";
7
+ /** Nazwy, o których już ostrzegliśmy — jedno ostrzeżenie na nazwę, nie na każdą instancję. */
8
+ const warned = new Set();
6
9
  /**
7
10
  * Ikona Koncept UI (inline SVG, zestaw Tabler Icons — MIT). Rozmiar dziedziczy z `font-size`
8
11
  * (1em) lub ustaw `size`; kolor z `currentColor`. Ikona musi być w rejestrze — wbudowany
9
12
  * zestaw działa od razu, pełny dodaje `registerKptTablerIcons()` z `@konce-pt/react/icons`.
10
13
  *
14
+ * Domyślnie ikona jest dekoracją (`aria-hidden`). Ikona niosąca znaczenie sama, bez tekstu obok,
15
+ * dostaje `label` — wtedy jest `role="img"` z tą nazwą. W buildzie deweloperskim nieznana nazwa
16
+ * daje jedno ostrzeżenie w konsoli zamiast cichej pustki.
17
+ *
11
18
  * @example
12
19
  * <KptIcon name="menu" />
13
20
  * <KptIcon name="check" size="1.25rem" />
14
21
  */
15
- export function KptIcon({ name, size = '', className, style, ...rest }) {
22
+ export function KptIcon({ name, size = '', label = '', className, style, ...rest }) {
16
23
  // Rejestr bywa uzupełniany po pierwszym renderze (leniwy import zestawu Tabler),
17
24
  // więc komponent słucha jego wersji zamiast czytać go raz.
18
25
  useSyncExternalStore(subscribeKptIcons, getKptIconsVersion, getKptIconsVersion);
19
26
  // Treść pochodzi z zaufanego rejestru i przeszła sanitizację przy rejestracji —
20
27
  // to ten sam kontrakt, co `innerHTML` w porcie Angulara.
21
- const html = renderKptIconSvg(getKptIcon(name));
22
- return (_jsx("span", { className: cn('kpt-icon-host', className), "aria-hidden": "true", style: size ? { fontSize: size, ...style } : style, dangerouslySetInnerHTML: { __html: html }, ...rest }));
28
+ const svg = getKptIcon(name);
29
+ const html = renderKptIconSvg(svg);
30
+ useEffect(() => {
31
+ if (svg || !isDevBuild() || warned.has(name))
32
+ return;
33
+ warned.add(name);
34
+ console.warn(`KptIcon: ikony „${name}” nie ma (jeszcze) w rejestrze — sprawdź nazwę albo zarejestruj ją (registerKptIcons / registerKptTablerIcons).`);
35
+ }, [svg, name]);
36
+ return (_jsx("span", { className: cn('kpt-icon-host', className), "aria-hidden": label ? undefined : true, role: label ? 'img' : undefined, "aria-label": label || undefined, style: size ? { fontSize: size, ...style } : style, dangerouslySetInnerHTML: { __html: html }, ...rest }));
23
37
  }
@@ -12,6 +12,7 @@ Import: `import { KptIcon } from '@konce-pt/react';`
12
12
  ## Props
13
13
  - `name`: string — the icon name from the registry (required)
14
14
  - `size`: string — a CSS length (e.g. '1.25rem'); by default inherited from `font-size`
15
+ - `label`: string — a name for screen readers; with it the icon is `role="img"`, without it `aria-hidden`
15
16
  - `className`, `ref` and every other `<span>` prop go to the host
16
17
 
17
18
  ## The registry
@@ -62,5 +63,7 @@ CMS, an API or an upload.
62
63
  <KptButton><KptIcon name="phone" /> Call</KptButton>
63
64
 
64
65
  ## Accessibility
65
- The host carries `aria-hidden="true"` — an icon is decoration. Put the accessible name on the
66
- control around it (`aria-label` on the button), not on the icon.
66
+ By default an icon is `aria-hidden` — decorative, which is right next to a text label or inside
67
+ an icon button with its own name. An icon that carries meaning on its own gets `label`: it becomes
68
+ `role="img"` with that `aria-label`. In development builds an unknown name logs one console
69
+ warning per name instead of rendering silently empty.
@@ -11,6 +11,11 @@ export interface KptImageProps extends Omit<ComponentPropsWithRef<'div'>, 'onCli
11
11
  /**
12
12
  * Obraz z opcjonalnym podglądem pełnoekranowym.
13
13
  *
14
+ * Z `preview` miniatura jest przyciskiem (osiągalnym z klawiatury), a podgląd otwiera się na
15
+ * natywnym `<dialog>` przez `showModal()`: przeglądarka daje pułapkę fokusu, `Escape`, bezwładną
16
+ * stronę i górną warstwę. Na czas podglądu blokujemy przewijanie dokumentu, a po zamknięciu
17
+ * fokus wraca na miniaturę.
18
+ *
14
19
  * @example
15
20
  * <KptImage src="/foto.jpg" alt="Zdjęcie" preview width="240px" />
16
21
  */
@@ -1,16 +1,52 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from 'react';
2
+ import { useEffect, useRef, useState } from 'react';
3
3
  import { cn } from "../internal/cn.js";
4
4
  import { useKptMessages } from "../i18n/i18n-context.js";
5
5
  import { KptIcon } from "../icon/icon.js";
6
6
  /**
7
7
  * Obraz z opcjonalnym podglądem pełnoekranowym.
8
8
  *
9
+ * Z `preview` miniatura jest przyciskiem (osiągalnym z klawiatury), a podgląd otwiera się na
10
+ * natywnym `<dialog>` przez `showModal()`: przeglądarka daje pułapkę fokusu, `Escape`, bezwładną
11
+ * stronę i górną warstwę. Na czas podglądu blokujemy przewijanie dokumentu, a po zamknięciu
12
+ * fokus wraca na miniaturę.
13
+ *
9
14
  * @example
10
15
  * <KptImage src="/foto.jpg" alt="Zdjęcie" preview width="240px" />
11
16
  */
12
17
  export function KptImage({ src, alt = '', width = null, preview = false, locale, dictionary, className, ...rest }) {
13
18
  const { t } = useKptMessages({ locale, dictionary });
14
19
  const [open, setOpen] = useState(false);
15
- return (_jsxs("div", { className: cn('kpt-image-host', className), ...rest, children: [_jsxs("div", { className: cn('kpt-image', preview && 'kpt-image--preview'), style: width ? { width } : undefined, onClick: () => preview && setOpen(true), children: [_jsx("img", { className: "kpt-image__img", src: src, alt: alt }), preview ? (_jsx("div", { className: "kpt-image__mask", "aria-hidden": "true", children: _jsx(KptIcon, { name: "zoom-in" }) })) : null] }), open ? (_jsxs("div", { className: "kpt-image__overlay", onClick: () => setOpen(false), children: [_jsx("button", { type: "button", className: "kpt-image__close", onClick: () => setOpen(false), "aria-label": t('common.close'), children: _jsx(KptIcon, { name: "x" }) }), _jsx("img", { className: "kpt-image__full", src: src, alt: alt, onClick: (event) => event.stopPropagation() })] })) : null] }));
20
+ const dialogRef = useRef(null);
21
+ const triggerRef = useRef(null);
22
+ useEffect(() => {
23
+ const el = dialogRef.current;
24
+ if (!el || !open)
25
+ return;
26
+ if (!el.open)
27
+ el.showModal();
28
+ const root = document.documentElement;
29
+ const saved = root.style.overflow;
30
+ root.style.overflow = 'hidden';
31
+ return () => {
32
+ root.style.overflow = saved;
33
+ if (el.open)
34
+ el.close();
35
+ };
36
+ }, [open]);
37
+ const style = width ? { width } : undefined;
38
+ if (!preview) {
39
+ return (_jsx("div", { className: cn('kpt-image-host', className), ...rest, children: _jsx("div", { className: "kpt-image", style: style, children: _jsx("img", { className: "kpt-image__img", src: src, alt: alt }) }) }));
40
+ }
41
+ // `close` przychodzi z każdej drogi: `×`, `Escape`, tło.
42
+ const onClosed = () => {
43
+ setOpen(false);
44
+ triggerRef.current?.focus();
45
+ };
46
+ // Okno wypełnia cały ekran, więc kliknięcie w nie samo (a nie w zdjęcie) to kliknięcie w tło.
47
+ const onDialogClick = (event) => {
48
+ if (event.target === event.currentTarget)
49
+ event.currentTarget.close();
50
+ };
51
+ return (_jsxs("div", { className: cn('kpt-image-host', className), ...rest, children: [_jsxs("button", { ref: triggerRef, type: "button", className: "kpt-image kpt-image--preview", "aria-haspopup": "dialog", "aria-label": alt ? t('image.previewOf', { alt }) : t('image.preview'), style: style, onClick: () => setOpen(true), children: [_jsx("img", { className: "kpt-image__img", src: src, alt: "" }), _jsx("span", { className: "kpt-image__mask", "aria-hidden": "true", children: _jsx(KptIcon, { name: "zoom-in" }) })] }), _jsxs("dialog", { ref: dialogRef, className: "kpt-image__overlay", "aria-label": alt || t('image.preview'), onClose: onClosed, onClick: onDialogClick, children: [_jsx("button", { type: "button", className: "kpt-image__close", onClick: () => dialogRef.current?.close(), "aria-label": t('common.close'), children: _jsx(KptIcon, { name: "x" }) }), _jsx("img", { className: "kpt-image__full", src: src, alt: alt, loading: "lazy" })] })] }));
16
52
  }
@@ -5,27 +5,29 @@ Import: `import { KptImage } from '@konce-pt/react';`
5
5
 
6
6
  ## DOM
7
7
  <div class="kpt-image-host">
8
- <div class="kpt-image kpt-image--preview" style="width:240px">
9
- <img class="kpt-image__img" src="…" alt="…">
10
- <div class="kpt-image__mask" aria-hidden="true">…zoom-in…</div>
11
- </div>
12
- <!-- open: -->
13
- <div class="kpt-image__overlay">
14
- <button class="kpt-image__close">…</button>
15
- <img class="kpt-image__full" src="…" alt="…">
16
- </div>
8
+ <!-- with preview: -->
9
+ <button class="kpt-image kpt-image--preview" type="button" aria-haspopup="dialog"
10
+ aria-label="Show full size: Photo" style="width:240px">
11
+ <img class="kpt-image__img" src="…" alt="">
12
+ <span class="kpt-image__mask" aria-hidden="true">…zoom-in…</span>
13
+ </button>
14
+ <dialog class="kpt-image__overlay" aria-label="Photo">
15
+ <button class="kpt-image__close" type="button" aria-label="Close">…</button>
16
+ <img class="kpt-image__full" src="…" alt="Photo" loading="lazy">
17
+ </dialog>
18
+ <!-- without preview: <div class="kpt-image"><img class="kpt-image__img" …></div> -->
17
19
  </div>
18
20
 
19
21
  ## Props
20
22
  - `src`: string (required); `alt`: string
21
23
  - `width`: string | null — a CSS length, e.g. `'240px'`
22
- - `preview`: boolean — a click opens the overlay
24
+ - `preview`: boolean — the thumbnail becomes a button opening a modal preview
23
25
  - `className`, `ref` and every other `<div>` prop go to the host
24
26
 
25
27
  ## The overlay closes around the photo, not on it
26
- The click on `.kpt-image__full` stops propagating, so only the dark area around the image closes
27
- the preview — clicking the photo itself (to look closer, to drag it) must not dismiss it. The × is
28
- there for the explicit way out.
28
+ The dialog fills the viewport, so a click on the dialog element itself — not on the photo or the
29
+ × — is a click on the backdrop and closes the preview. Clicking the photo (to look closer, to drag
30
+ it) does not dismiss it; Escape and × are the explicit ways out.
29
31
 
30
32
  ## The mask is decorative
31
33
  The zoom-in overlay is `aria-hidden`: it repeats what the cursor (`zoom-in`) already says and would
@@ -36,11 +38,16 @@ only add noise for a screen reader.
36
38
  <KptImage src="/logo.svg" alt="Logo" />
37
39
 
38
40
  ## i18n
39
- The close button label comes from `common.close`.
41
+ The thumbnail is named from `image.previewOf` (`{alt}`) or `image.preview`; the close button from `common.close`.
40
42
 
41
43
  ## Tokens
42
- The radius `--kpt-radius-md`, the mask and the overlay are black at 40% / 80% via `color-mix`.
44
+ The radius `--kpt-radius-md`; the backdrop and the hover mask `--kpt-color-overlay-scrim`; the × button and the magnifier disc `--kpt-color-surface` / `--kpt-color-on-surface` with `--kpt-elevation-2`.
43
45
 
44
46
  ## Accessibility
45
- `alt` goes to both the thumbnail and the full-size image; the close button is labelled from the
46
- dictionary.
47
+ With `preview`, the thumbnail is a `<button aria-haspopup="dialog">` named “Show full size: <alt>”
48
+ (`image.previewOf`, or `image.preview` without `alt`), so it is reached with Tab and opened with
49
+ Enter or Space; the magnifier shows on hover and on `:focus-visible`. The preview is a native modal
50
+ `<dialog>` opened with `showModal()` and named by `alt`: focus moves to its × button and stays inside,
51
+ Escape closes it, the page behind is inert, document scrolling is locked while it is open, and focus
52
+ returns to the thumbnail. It sits in the top layer, so no ancestor with `transform` or
53
+ `overflow: hidden` can clip it.
@@ -38,4 +38,4 @@ export interface KptInputProps extends Omit<ComponentPropsWithRef<'input'>, 'typ
38
38
  * <KptInput name="email" type="email" defaultValue="" />
39
39
  * <KptInput value={email} onValueChange={setEmail} invalid={!ok} touched={blurred} />
40
40
  */
41
- export declare function KptInput({ type, required, invalid, touched, onValueChange, onTouch, hostProps, className, id, onChange, onBlur, 'aria-invalid': ariaInvalid, ...rest }: Readonly<KptInputProps>): import("react").JSX.Element;
41
+ export declare function KptInput({ type, required, invalid, touched, onValueChange, onTouch, hostProps, className, id, onChange, onBlur, 'aria-invalid': ariaInvalid, 'aria-describedby': ariaDescribedBy, ...rest }: Readonly<KptInputProps>): import("react").JSX.Element;
@@ -1,8 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useCallback } from 'react';
3
3
  import { cn } from "../internal/cn.js";
4
- import { useKptId } from "../internal/use-id.js";
5
- import { useKptFormFieldContext } from "../form-field/form-field-context.js";
4
+ import { joinIds, useKptFieldA11y } from "../form-field/form-field-context.js";
6
5
  /**
7
6
  * Pole tekstowe Koncept UI. Renderuje natywny `<input>` w hoście `.kpt-input-host`.
8
7
  *
@@ -17,10 +16,9 @@ import { useKptFormFieldContext } from "../form-field/form-field-context.js";
17
16
  * <KptInput name="email" type="email" defaultValue="" />
18
17
  * <KptInput value={email} onValueChange={setEmail} invalid={!ok} touched={blurred} />
19
18
  */
20
- export function KptInput({ type = 'text', required = false, invalid = false, touched, onValueChange, onTouch, hostProps, className, id, onChange, onBlur, 'aria-invalid': ariaInvalid, ...rest }) {
21
- const field = useKptFormFieldContext();
19
+ export function KptInput({ type = 'text', required = false, invalid = false, touched, onValueChange, onTouch, hostProps, className, id, onChange, onBlur, 'aria-invalid': ariaInvalid, 'aria-describedby': ariaDescribedBy, ...rest }) {
22
20
  const { className: hostClassName, ...hostRest } = hostProps ?? {};
23
- const inputId = useKptId(id ?? field?.controlId, 'kpt-input');
21
+ const field = useKptFieldA11y(id, 'kpt-input');
24
22
  // Stan nieprawidłowy pokazujemy dopiero po dotknięciu pola — jak natywne `:user-invalid`.
25
23
  // Bez `touched` nie ma czego bramkować, więc decyduje samo `invalid`.
26
24
  const showInvalid = invalid && (touched ?? true);
@@ -32,5 +30,5 @@ export function KptInput({ type = 'text', required = false, invalid = false, tou
32
30
  onBlur?.(event);
33
31
  onTouch?.();
34
32
  }, [onBlur, onTouch]);
35
- return (_jsx("span", { className: cn('kpt-input-host', hostClassName), ...hostRest, children: _jsx("input", { className: cn('kpt-input__field', className), id: inputId, type: type, "aria-required": required || undefined, "aria-invalid": ariaInvalid ?? (showInvalid || undefined), onChange: handleChange, onBlur: handleBlur, ...rest }) }));
33
+ return (_jsx("span", { className: cn('kpt-input-host', hostClassName), ...hostRest, children: _jsx("input", { className: cn('kpt-input__field', className), id: field.id, "aria-describedby": joinIds(ariaDescribedBy, field.describedBy), type: type, "aria-required": required || undefined, "aria-invalid": ariaInvalid ?? (showInvalid || undefined), onChange: handleChange, onBlur: handleBlur, ...rest }) }));
36
34
  }
@@ -46,6 +46,11 @@ configuration. An explicit `id` prop still wins.
46
46
  <KptInput type="email" invalid={!!err} onTouch={markTouched} />
47
47
  </KptFormField>
48
48
 
49
+ ## Label and description
50
+ `id` goes to the native field, not the host; without one it comes from `KptFormField` or is
51
+ generated. Inside `KptFormField` the wrapper's `label[for]` points at it and the hint or error is
52
+ added to `aria-describedby` (merged with your own). Without a wrapper, name it with `aria-label`.
53
+
49
54
  ## Tokens
50
55
  `--kpt-form-field-{bg,text,placeholder,border,border-hover,border-focus,border-error,radius}`.
51
56
 
@@ -16,4 +16,4 @@ export interface KptInputMaskProps extends Omit<ComponentPropsWithRef<'input'>,
16
16
  * @example
17
17
  * <KptInputMask mask="(999) 999-99-99" value={phone} onValueChange={setPhone} />
18
18
  */
19
- export declare function KptInputMask({ value, defaultValue, onValueChange, mask, placeholder, invalid, touched, onTouch, disabled, className, id, onBlur, ...rest }: Readonly<KptInputMaskProps>): import("react").JSX.Element;
19
+ export declare function KptInputMask({ value, defaultValue, onValueChange, mask, placeholder, invalid, touched, onTouch, disabled, className, id, onBlur, 'aria-describedby': ariaDescribedBy, inputMode, ...rest }: Readonly<KptInputMaskProps>): import("react").JSX.Element;
@@ -1,8 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { cn } from "../internal/cn.js";
3
3
  import { useControllableState } from "../internal/use-controllable-state.js";
4
- import { useKptId } from "../internal/use-id.js";
5
- import { useKptFormFieldContext } from "../form-field/form-field-context.js";
4
+ import { joinIds, useKptFieldA11y } from "../form-field/form-field-context.js";
6
5
  /** Tokeny maski: `9` = cyfra, `a` = litera, `*` = znak alfanumeryczny. */
7
6
  const TOKENS = {
8
7
  '9': /\d/,
@@ -16,9 +15,12 @@ const TOKENS = {
16
15
  * @example
17
16
  * <KptInputMask mask="(999) 999-99-99" value={phone} onValueChange={setPhone} />
18
17
  */
19
- export function KptInputMask({ value, defaultValue = '', onValueChange, mask = '', placeholder, invalid = false, touched, onTouch, disabled = false, className, id, onBlur, ...rest }) {
20
- const field = useKptFormFieldContext();
21
- const inputId = useKptId(id ?? field?.controlId, 'kpt-input-mask');
18
+ export function KptInputMask({ value, defaultValue = '', onValueChange, mask = '', placeholder, invalid = false, touched, onTouch, disabled = false, className, id, onBlur, 'aria-describedby': ariaDescribedBy, inputMode, ...rest }) {
19
+ const field = useKptFieldA11y(id, 'kpt-input-mask');
20
+ // Klawiatura ekranowa z maski: same tokeny `9` (telefon, kod pocztowy) → numeryczna.
21
+ const tokens = [...mask].filter((char) => char in TOKENS);
22
+ const autoInputMode = tokens.length && tokens.every((char) => char === '9') ? 'numeric' : 'text';
23
+ const inputId = field.id;
22
24
  const [current, setCurrent] = useControllableState({ value, defaultValue, onChange: onValueChange });
23
25
  const showInvalid = invalid && (touched ?? true);
24
26
  /*
@@ -58,7 +60,7 @@ export function KptInputMask({ value, defaultValue = '', onValueChange, mask = '
58
60
  // wpisany znak zostałby na ekranie, więc DOM zrównujemy ręcznie.
59
61
  event.target.value = formatted;
60
62
  };
61
- return (_jsx("span", { className: "kpt-input-mask-host", children: _jsx("input", { className: cn('kpt-input-mask__input', showInvalid && 'kpt-input-mask--invalid', className), id: inputId, type: "text", value: current, disabled: disabled, placeholder: placeholder || mask, onChange: onInput, onBlur: (event) => {
63
+ return (_jsx("span", { className: "kpt-input-mask-host", children: _jsx("input", { className: cn('kpt-input-mask__input', showInvalid && 'kpt-input-mask--invalid', className), id: inputId, "aria-invalid": showInvalid || undefined, "aria-describedby": joinIds(ariaDescribedBy, field.describedBy), inputMode: inputMode ?? autoInputMode, type: "text", value: current, disabled: disabled, placeholder: placeholder || mask, onChange: onInput, onBlur: (event) => {
62
64
  onBlur?.(event);
63
65
  onTouch?.();
64
66
  }, ...rest }) }));
@@ -29,6 +29,17 @@ change, so React would not re-render and the character would linger on screen.
29
29
  <KptInputMask mask="(999) 999-99-99" value={phone} onValueChange={setPhone} />
30
30
  <KptInputMask mask="aa-9999" value={code} onValueChange={setCode} />
31
31
 
32
+ ## Screen reader and keyboard
33
+ The on-screen keyboard follows the mask: only `9` tokens (phone, postal code) give
34
+ `inputMode="numeric"`, otherwise `text`; pass `inputMode` to override it, and `autoComplete` (`tel`,
35
+ `postal-code`…) so the browser can fill the field in.
36
+
37
+ ## Label and description
38
+ `id` goes to the native field, not the host; without one it comes from `KptFormField` or is
39
+ generated. Inside `KptFormField` the wrapper's `label[for]` points at it and the hint or error is
40
+ added to `aria-describedby` (merged with your own). Without a wrapper, name it with `aria-label`.
41
+ New: `aria-invalid` after touch.
42
+
32
43
  ## Tokens
33
44
  `--kpt-form-field-{bg,text,placeholder,border,border-focus,border-error,radius}`.
34
45