@kosmeo-ui/react 0.1.1 → 0.2.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 (187) hide show
  1. package/dist/components/buttons/button/index.d.ts +2 -0
  2. package/dist/components/buttons/button/index.d.ts.map +1 -1
  3. package/dist/components/buttons/button/index.js +2 -2
  4. package/dist/components/buttons/button/index.js.map +1 -1
  5. package/dist/components/buttons/button/styles.css +6 -4
  6. package/dist/components/buttons/rowActions/styles.css +7 -0
  7. package/dist/components/data-display/addressBlock/styles.css +2 -4
  8. package/dist/components/data-display/dataTable/styles.css +15 -0
  9. package/dist/components/data-display/infiniteScroll/filters/searchFilter.d.ts.map +1 -1
  10. package/dist/components/data-display/infiniteScroll/filters/searchFilter.js +4 -35
  11. package/dist/components/data-display/infiniteScroll/filters/searchFilter.js.map +1 -1
  12. package/dist/components/data-display/infiniteScroll/filters/styles.css +4 -0
  13. package/dist/components/data-display/infiniteScroll/header/deleteButton.d.ts.map +1 -1
  14. package/dist/components/data-display/infiniteScroll/header/deleteButton.js +1 -1
  15. package/dist/components/data-display/infiniteScroll/header/deleteButton.js.map +1 -1
  16. package/dist/components/data-display/infiniteScroll/header/index.d.ts.map +1 -1
  17. package/dist/components/data-display/infiniteScroll/header/index.js +1 -1
  18. package/dist/components/data-display/infiniteScroll/header/index.js.map +1 -1
  19. package/dist/components/data-display/infiniteScroll/table/index.d.ts.map +1 -1
  20. package/dist/components/data-display/infiniteScroll/table/index.js +2 -2
  21. package/dist/components/data-display/infiniteScroll/table/index.js.map +1 -1
  22. package/dist/components/data-display/infiniteScroll/tags/index.d.ts.map +1 -1
  23. package/dist/components/data-display/infiniteScroll/tags/index.js +1 -6
  24. package/dist/components/data-display/infiniteScroll/tags/index.js.map +1 -1
  25. package/dist/components/data-display/infiniteScroll/tags/listFilterTag.d.ts.map +1 -1
  26. package/dist/components/data-display/infiniteScroll/tags/listFilterTag.js +3 -3
  27. package/dist/components/data-display/infiniteScroll/tags/listFilterTag.js.map +1 -1
  28. package/dist/components/data-display/infiniteScroll/tags/rangeFilterTag.d.ts.map +1 -1
  29. package/dist/components/data-display/infiniteScroll/tags/rangeFilterTag.js +3 -13
  30. package/dist/components/data-display/infiniteScroll/tags/rangeFilterTag.js.map +1 -1
  31. package/dist/components/data-display/infiniteScroll/tags/selectFilterTag.d.ts.map +1 -1
  32. package/dist/components/data-display/infiniteScroll/tags/selectFilterTag.js +3 -5
  33. package/dist/components/data-display/infiniteScroll/tags/selectFilterTag.js.map +1 -1
  34. package/dist/components/data-display/infiniteScroll/tags/styles.css +1 -29
  35. package/dist/components/data-display/recoveryCodeGrid/styles.css +1 -2
  36. package/dist/components/data-display/statCard/styles.css +1 -1
  37. package/dist/components/data-display/table/styles.css +12 -1
  38. package/dist/components/data-display/tagCard/index.d.ts.map +1 -1
  39. package/dist/components/data-display/tagCard/index.js +1 -1
  40. package/dist/components/data-display/tagCard/index.js.map +1 -1
  41. package/dist/components/feedback/badge/styles.css +8 -6
  42. package/dist/components/feedback/emptyState/styles.css +2 -4
  43. package/dist/components/feedback/progressBar/index.d.ts.map +1 -1
  44. package/dist/components/feedback/progressBar/index.js +7 -2
  45. package/dist/components/feedback/progressBar/index.js.map +1 -1
  46. package/dist/components/feedback/progressBar/styles.css +15 -6
  47. package/dist/components/feedback/spinner/styles.css +0 -13
  48. package/dist/components/forms/fieldShell/styles.css +1 -1
  49. package/dist/components/forms/formActions/styles.css +1 -1
  50. package/dist/components/forms/infoRow/styles.css +1 -2
  51. package/dist/components/forms/requiredAsterisk/styles.css +1 -1
  52. package/dist/components/index.d.ts +2 -0
  53. package/dist/components/index.d.ts.map +1 -1
  54. package/dist/components/index.js +1 -0
  55. package/dist/components/index.js.map +1 -1
  56. package/dist/components/inputs/checkbox/index.d.ts +6 -1
  57. package/dist/components/inputs/checkbox/index.d.ts.map +1 -1
  58. package/dist/components/inputs/checkbox/index.js +9 -2
  59. package/dist/components/inputs/checkbox/index.js.map +1 -1
  60. package/dist/components/inputs/checkbox/styles.css +8 -0
  61. package/dist/components/inputs/colorPicker/styles.css +1 -1
  62. package/dist/components/inputs/dateInput/index.d.ts +3 -3
  63. package/dist/components/inputs/dateInput/index.d.ts.map +1 -1
  64. package/dist/components/inputs/dateInput/index.js +13 -3
  65. package/dist/components/inputs/dateInput/index.js.map +1 -1
  66. package/dist/components/inputs/dropdown/index.d.ts +2 -0
  67. package/dist/components/inputs/dropdown/index.d.ts.map +1 -1
  68. package/dist/components/inputs/dropdown/index.js +1 -1
  69. package/dist/components/inputs/dropdown/index.js.map +1 -1
  70. package/dist/components/inputs/dropdownArrowButton/index.d.ts.map +1 -1
  71. package/dist/components/inputs/dropdownArrowButton/index.js +1 -1
  72. package/dist/components/inputs/dropdownArrowButton/index.js.map +1 -1
  73. package/dist/components/inputs/dropdownArrowButton/styles.css +1 -1
  74. package/dist/components/inputs/input/index.d.ts +5 -1
  75. package/dist/components/inputs/input/index.d.ts.map +1 -1
  76. package/dist/components/inputs/input/index.js +25 -4
  77. package/dist/components/inputs/input/index.js.map +1 -1
  78. package/dist/components/inputs/input/styles.css +46 -2
  79. package/dist/components/inputs/multiSelect/index.d.ts.map +1 -1
  80. package/dist/components/inputs/multiSelect/index.js +1 -1
  81. package/dist/components/inputs/multiSelect/index.js.map +1 -1
  82. package/dist/components/inputs/multiSelect/styles.css +1 -1
  83. package/dist/components/inputs/phoneInput/index.d.ts.map +1 -1
  84. package/dist/components/inputs/phoneInput/index.js +1 -1
  85. package/dist/components/inputs/phoneInput/index.js.map +1 -1
  86. package/dist/components/inputs/richTextEditor/index.d.ts.map +1 -1
  87. package/dist/components/inputs/richTextEditor/index.js +14 -3
  88. package/dist/components/inputs/richTextEditor/index.js.map +1 -1
  89. package/dist/components/layout/borderedSection/index.d.ts +3 -1
  90. package/dist/components/layout/borderedSection/index.d.ts.map +1 -1
  91. package/dist/components/layout/borderedSection/index.js +2 -2
  92. package/dist/components/layout/borderedSection/index.js.map +1 -1
  93. package/dist/components/layout/card/styles.css +22 -0
  94. package/dist/components/layout/sectionHeader/styles.css +1 -2
  95. package/dist/components/navigation/pagination/index.d.ts +23 -0
  96. package/dist/components/navigation/pagination/index.d.ts.map +1 -0
  97. package/dist/components/navigation/pagination/index.js +29 -0
  98. package/dist/components/navigation/pagination/index.js.map +1 -0
  99. package/dist/components/navigation/pagination/styles.css +113 -0
  100. package/dist/components/navigation/pagination/utils.d.ts +9 -0
  101. package/dist/components/navigation/pagination/utils.d.ts.map +1 -0
  102. package/dist/components/navigation/pagination/utils.js +25 -0
  103. package/dist/components/navigation/pagination/utils.js.map +1 -0
  104. package/dist/components/navigation/sidebar/styles.css +9 -1
  105. package/dist/components/navigation/tabButton/styles.css +6 -6
  106. package/dist/components/navigation/tabs/styles.css +2 -2
  107. package/dist/components/overlays/confirmationModal/index.d.ts +5 -0
  108. package/dist/components/overlays/confirmationModal/index.d.ts.map +1 -1
  109. package/dist/components/overlays/confirmationModal/index.js +23 -4
  110. package/dist/components/overlays/confirmationModal/index.js.map +1 -1
  111. package/dist/components/overlays/confirmationModal/styles.css +3 -0
  112. package/dist/components/overlays/modal/index.d.ts +2 -0
  113. package/dist/components/overlays/modal/index.d.ts.map +1 -1
  114. package/dist/components/overlays/modal/index.js +13 -4
  115. package/dist/components/overlays/modal/index.js.map +1 -1
  116. package/dist/hooks/index.d.ts +1 -0
  117. package/dist/hooks/index.d.ts.map +1 -1
  118. package/dist/hooks/index.js +1 -0
  119. package/dist/hooks/index.js.map +1 -1
  120. package/dist/hooks/usePaginated.d.ts +43 -0
  121. package/dist/hooks/usePaginated.d.ts.map +1 -0
  122. package/dist/hooks/usePaginated.js +60 -0
  123. package/dist/hooks/usePaginated.js.map +1 -0
  124. package/dist/styles.css +37 -14
  125. package/package.json +1 -1
  126. package/src/components/buttons/button/index.tsx +14 -5
  127. package/src/components/buttons/button/styles.css +6 -4
  128. package/src/components/buttons/rowActions/styles.css +7 -0
  129. package/src/components/data-display/addressBlock/styles.css +2 -4
  130. package/src/components/data-display/dataTable/styles.css +15 -0
  131. package/src/components/data-display/infiniteScroll/filters/searchFilter.tsx +16 -65
  132. package/src/components/data-display/infiniteScroll/filters/styles.css +4 -0
  133. package/src/components/data-display/infiniteScroll/header/deleteButton.tsx +1 -0
  134. package/src/components/data-display/infiniteScroll/header/index.tsx +4 -0
  135. package/src/components/data-display/infiniteScroll/table/index.tsx +2 -0
  136. package/src/components/data-display/infiniteScroll/tags/index.tsx +1 -11
  137. package/src/components/data-display/infiniteScroll/tags/listFilterTag.tsx +12 -16
  138. package/src/components/data-display/infiniteScroll/tags/rangeFilterTag.tsx +4 -22
  139. package/src/components/data-display/infiniteScroll/tags/selectFilterTag.tsx +4 -11
  140. package/src/components/data-display/infiniteScroll/tags/styles.css +1 -29
  141. package/src/components/data-display/recoveryCodeGrid/styles.css +1 -2
  142. package/src/components/data-display/statCard/styles.css +1 -1
  143. package/src/components/data-display/table/styles.css +12 -1
  144. package/src/components/data-display/tagCard/index.tsx +5 -3
  145. package/src/components/feedback/badge/styles.css +8 -6
  146. package/src/components/feedback/emptyState/styles.css +2 -4
  147. package/src/components/feedback/progressBar/index.tsx +14 -8
  148. package/src/components/feedback/progressBar/styles.css +15 -6
  149. package/src/components/feedback/spinner/styles.css +0 -13
  150. package/src/components/forms/fieldShell/styles.css +1 -1
  151. package/src/components/forms/formActions/styles.css +1 -1
  152. package/src/components/forms/infoRow/styles.css +1 -2
  153. package/src/components/forms/requiredAsterisk/styles.css +1 -1
  154. package/src/components/index.ts +2 -0
  155. package/src/components/inputs/checkbox/index.tsx +24 -4
  156. package/src/components/inputs/checkbox/styles.css +8 -0
  157. package/src/components/inputs/colorPicker/styles.css +1 -1
  158. package/src/components/inputs/dateInput/index.tsx +13 -4
  159. package/src/components/inputs/dropdown/index.tsx +4 -0
  160. package/src/components/inputs/dropdownArrowButton/index.tsx +1 -0
  161. package/src/components/inputs/dropdownArrowButton/styles.css +1 -1
  162. package/src/components/inputs/input/index.tsx +47 -4
  163. package/src/components/inputs/input/styles.css +46 -2
  164. package/src/components/inputs/multiSelect/index.tsx +1 -0
  165. package/src/components/inputs/multiSelect/styles.css +1 -1
  166. package/src/components/inputs/phoneInput/index.tsx +6 -2
  167. package/src/components/inputs/richTextEditor/index.tsx +27 -3
  168. package/src/components/layout/borderedSection/index.tsx +4 -1
  169. package/src/components/layout/card/styles.css +22 -0
  170. package/src/components/layout/sectionHeader/styles.css +1 -2
  171. package/src/components/navigation/pagination/index.tsx +134 -0
  172. package/src/components/navigation/pagination/styles.css +113 -0
  173. package/src/components/navigation/pagination/utils.ts +32 -0
  174. package/src/components/navigation/sidebar/styles.css +9 -1
  175. package/src/components/navigation/tabButton/styles.css +6 -6
  176. package/src/components/navigation/tabs/styles.css +2 -2
  177. package/src/components/overlays/confirmationModal/index.tsx +37 -1
  178. package/src/components/overlays/confirmationModal/styles.css +3 -0
  179. package/src/components/overlays/modal/index.tsx +23 -4
  180. package/src/hooks/index.ts +4 -0
  181. package/src/hooks/usePaginated.ts +123 -0
  182. package/src/styles.css +37 -14
  183. package/dist/components/data-display/infiniteScroll/tags/searchFilterTag.d.ts +0 -10
  184. package/dist/components/data-display/infiniteScroll/tags/searchFilterTag.d.ts.map +0 -1
  185. package/dist/components/data-display/infiniteScroll/tags/searchFilterTag.js +0 -22
  186. package/dist/components/data-display/infiniteScroll/tags/searchFilterTag.js.map +0 -1
  187. package/src/components/data-display/infiniteScroll/tags/searchFilterTag.tsx +0 -41
@@ -88,7 +88,7 @@ button.k-badge {
88
88
 
89
89
  .k-badge-warning {
90
90
  background-color: var(--warning-color);
91
- color: var(--text-color);
91
+ color: var(--warning-color-contrast);
92
92
  }
93
93
 
94
94
  .k-badge-success {
@@ -107,13 +107,15 @@ button.k-badge {
107
107
  }
108
108
 
109
109
  /* Subtle variants - transparent background with colored text */
110
+ /* Subtle text blends the variant color 60/40 with --text-color: the full
111
+ variant color on its own 15% tint falls under 4.5:1 for most variants. */
110
112
  .k-badge-subtle.k-badge-primary {
111
113
  background-color: color-mix(
112
114
  in srgb,
113
115
  var(--primary-color) 15%,
114
116
  var(--background-color)
115
117
  );
116
- color: var(--primary-color);
118
+ color: color-mix(in srgb, var(--primary-color) 60%, var(--text-color));
117
119
  }
118
120
 
119
121
  .k-badge-subtle.k-badge-secondary {
@@ -122,7 +124,7 @@ button.k-badge {
122
124
  var(--secondary-color) 15%,
123
125
  var(--background-color)
124
126
  );
125
- color: var(--secondary-color);
127
+ color: color-mix(in srgb, var(--secondary-color) 60%, var(--text-color));
126
128
  }
127
129
 
128
130
  .k-badge-subtle.k-badge-light {
@@ -140,7 +142,7 @@ button.k-badge {
140
142
  var(--danger-color) 15%,
141
143
  var(--background-color)
142
144
  );
143
- color: var(--danger-color);
145
+ color: color-mix(in srgb, var(--danger-color) 60%, var(--text-color));
144
146
  }
145
147
 
146
148
  .k-badge-subtle.k-badge-warning {
@@ -149,7 +151,7 @@ button.k-badge {
149
151
  var(--warning-color) 20%,
150
152
  var(--background-color)
151
153
  );
152
- color: var(--warning-color);
154
+ color: var(--warning-color-text);
153
155
  }
154
156
 
155
157
  .k-badge-subtle.k-badge-success {
@@ -158,5 +160,5 @@ button.k-badge {
158
160
  var(--success-color) 15%,
159
161
  var(--background-color)
160
162
  );
161
- color: var(--success-color);
163
+ color: color-mix(in srgb, var(--success-color) 60%, var(--text-color));
162
164
  }
@@ -1,7 +1,6 @@
1
1
  .k-empty-state-inline {
2
2
  font-size: 0.875rem;
3
- color: var(--text-color);
4
- opacity: 0.65;
3
+ color: var(--text-color-secondary);
5
4
  font-style: italic;
6
5
  padding: 0.5rem 0;
7
6
  }
@@ -14,8 +13,7 @@
14
13
  gap: 0.75rem;
15
14
  padding: 2rem 1rem;
16
15
  text-align: center;
17
- color: var(--text-color);
18
- opacity: 0.75;
16
+ color: var(--text-color-secondary);
19
17
  }
20
18
 
21
19
  .k-empty-state-icon {
@@ -17,16 +17,22 @@ const ProgressBar = (props: ProgressBarProps) => {
17
17
  const fillBorderRadius =
18
18
  clampedProgress === 100 ? "0.25rem" : "0.25rem 0 0 0.25rem";
19
19
 
20
+ // The percentage sits beside the bar, not on the fill: no single text color
21
+ // reaches 4.5:1 on every variant's fill in both themes.
20
22
  return (
21
- <div className={`k-progress-bar-container`}>
22
- <div
23
- className={`k-progress-bar-fill k-progress-bar-fill-${variant}`}
24
- style={{ width: `${clampedProgress}%`, borderRadius: fillBorderRadius }}
25
- >
26
- {showPercentage && clampedProgress > 0 && (
27
- <span className="k-progress-bar-text">{clampedProgress}%</span>
28
- )}
23
+ <div className="k-progress-bar">
24
+ <div className="k-progress-bar-container">
25
+ <div
26
+ className={`k-progress-bar-fill k-progress-bar-fill-${variant}`}
27
+ style={{
28
+ width: `${clampedProgress}%`,
29
+ borderRadius: fillBorderRadius,
30
+ }}
31
+ />
29
32
  </div>
33
+ {showPercentage && (
34
+ <span className="k-progress-bar-text">{clampedProgress}%</span>
35
+ )}
30
36
  </div>
31
37
  );
32
38
  };
@@ -1,5 +1,13 @@
1
- .k-progress-bar-container {
1
+ .k-progress-bar {
2
+ display: flex;
3
+ align-items: center;
4
+ gap: 0.5rem;
2
5
  width: 100%;
6
+ }
7
+
8
+ .k-progress-bar-container {
9
+ flex: 1;
10
+ min-width: 0;
3
11
  height: 1.5625rem;
4
12
  background-color: var(--background-color-dark);
5
13
  border-radius: 0.25rem;
@@ -10,16 +18,17 @@
10
18
  .k-progress-bar-fill {
11
19
  height: 100%;
12
20
  background-color: var(--success-color);
13
- display: flex;
14
- align-items: center;
15
- justify-content: center;
16
- color: var(--text-color-light);
17
- font-weight: 600;
18
21
  transition: width 0.5s ease-in-out;
19
22
  }
20
23
 
21
24
  .k-progress-bar-text {
25
+ flex-shrink: 0;
26
+ min-width: 2.75rem;
27
+ text-align: right;
22
28
  font-size: var(--text-size-sm);
29
+ font-weight: 600;
30
+ font-variant-numeric: tabular-nums;
31
+ color: var(--text-color);
23
32
  }
24
33
 
25
34
  .k-progress-bar-fill-primary {
@@ -49,16 +49,3 @@
49
49
  .k-spinner-warning {
50
50
  border-left-color: var(--warning-color);
51
51
  }
52
-
53
- /* Visually hidden class for accessibility text */
54
- .k-sr-only {
55
- position: absolute;
56
- width: 0.0625rem;
57
- height: 0.0625rem;
58
- padding: 0;
59
- margin: -0.0625rem;
60
- overflow: hidden;
61
- clip: rect(0, 0, 0, 0);
62
- white-space: nowrap;
63
- border: 0;
64
- }
@@ -28,7 +28,7 @@
28
28
  .k-field-shell-error {
29
29
  margin-top: 0.25rem;
30
30
  font-size: 0.8125rem;
31
- color: var(--danger-color);
31
+ color: var(--danger-color-text);
32
32
  }
33
33
 
34
34
  .k-field-shell-helper {
@@ -42,7 +42,7 @@
42
42
  var(--danger-color) 12%,
43
43
  var(--background-color)
44
44
  );
45
- color: var(--danger-color);
45
+ color: var(--danger-color-text);
46
46
  border: 0.0625rem solid var(--danger-color);
47
47
  border-radius: 0.375rem;
48
48
  padding: 0.5rem 0.75rem;
@@ -22,8 +22,7 @@
22
22
 
23
23
  .k-info-row-label {
24
24
  font-weight: 500;
25
- color: var(--text-color);
26
- opacity: 0.7;
25
+ color: var(--text-color-secondary);
27
26
  }
28
27
 
29
28
  .k-info-row-value {
@@ -1,3 +1,3 @@
1
1
  .k-mandatory-asterisk {
2
- color: var(--danger-color);
2
+ color: var(--danger-color-text);
3
3
  }
@@ -39,6 +39,7 @@ export { default as RadioGroup } from "./inputs/radioGroup/index.js";
39
39
  export { default as ListRow } from "./data-display/listRow/index.js";
40
40
  export { default as TagCard } from "./data-display/tagCard/index.js";
41
41
  export { default as Stepper } from "./navigation/stepper/index.js";
42
+ export { default as Pagination } from "./navigation/pagination/index.js";
42
43
  export { default as Typeahead } from "./inputs/typeahead/index.js";
43
44
  export { default as AuthPage } from "./layout/authPage/index.js";
44
45
  export { default as Checkbox } from "./inputs/checkbox/index.js";
@@ -120,6 +121,7 @@ export type {
120
121
  AddressBlockProps,
121
122
  AddressBlockAddress,
122
123
  } from "./data-display/addressBlock/index.js";
124
+ export type { PaginationProps } from "./navigation/pagination/index.js";
123
125
  export type {
124
126
  StepperProps,
125
127
  StepperStep,
@@ -7,22 +7,39 @@ interface CheckboxProps {
7
7
  label?: string | React.ReactNode;
8
8
  labelLocation?: "top" | "right" | "bottom" | "left";
9
9
  checked: boolean;
10
- onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
10
+ /** **_Optional_** only when `readOnly`, which never calls it */
11
+ onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
11
12
  onClick?: (event: React.MouseEvent<HTMLInputElement>) => void;
12
13
  /** **_Optional_** whether the field is required (shows red asterisk and enables validation) */
13
14
  required?: boolean;
14
15
  /** **_Optional_** if the checkbox is disabled */
15
16
  disabled?: boolean;
17
+ /** **_Optional_** if the checkbox is read-only: unlike `disabled`, it stays focusable and is announced as read-only */
18
+ readOnly?: boolean;
16
19
  /** **_Optional_** ID of the field. If one is not provided, it will be randomly generated */
17
20
  id?: string;
21
+ /** **_Optional_** accessible name when there's no visible `label` (e.g. a row-select checkbox) */
22
+ "aria-label"?: string;
18
23
  }
19
24
 
20
25
  const Checkbox = (props: CheckboxProps) => {
21
- const { labelLocation = "top", disabled = false, required = false } = props;
26
+ const {
27
+ labelLocation = "top",
28
+ disabled = false,
29
+ required = false,
30
+ readOnly = false,
31
+ } = props;
22
32
 
23
33
  const inputId =
24
34
  props.id || `checkbox-${Math.random().toString(36).substring(2, 9)}`;
25
35
 
36
+ // Browsers ignore `readonly` on checkboxes, so a click (or Space) is
37
+ // cancelled here instead; aria-readonly tells assistive tech why.
38
+ const handleClick = (event: React.MouseEvent<HTMLInputElement>) => {
39
+ if (readOnly) event.preventDefault();
40
+ props.onClick?.(event);
41
+ };
42
+
26
43
  return (
27
44
  <div className={`k-checkbox-container k-${labelLocation}`}>
28
45
  {props.label && (
@@ -42,8 +59,11 @@ const Checkbox = (props: CheckboxProps) => {
42
59
  type="checkbox"
43
60
  required={required}
44
61
  checked={props.checked}
45
- onClick={props.onClick}
46
- onChange={props.onChange}
62
+ onClick={handleClick}
63
+ onChange={readOnly ? undefined : props.onChange}
64
+ readOnly={readOnly}
65
+ aria-readonly={readOnly || undefined}
66
+ aria-label={props["aria-label"]}
47
67
  className="k-checkbox"
48
68
  disabled={disabled}
49
69
  />
@@ -36,6 +36,14 @@
36
36
  color: var(--text-color);
37
37
  }
38
38
 
39
+ .k-checkbox[aria-readonly="true"] {
40
+ cursor: default;
41
+ }
42
+
43
+ .k-checkbox-label:has(+ .k-checkbox[aria-readonly="true"]) {
44
+ color: var(--text-color-secondary);
45
+ }
46
+
39
47
  .k-checkbox:disabled {
40
48
  cursor: not-allowed;
41
49
  opacity: 0.6;
@@ -48,5 +48,5 @@
48
48
 
49
49
  .k-color-picker-error {
50
50
  font-size: 0.8125rem;
51
- color: var(--danger-color);
51
+ color: var(--danger-color-text);
52
52
  }
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import FieldShell from "../../forms/fieldShell/index.js";
4
- import { ChangeEvent } from "react";
4
+ import { ChangeEvent, useId } from "react";
5
5
  import "./styles.css";
6
6
  import {
7
7
  dateInputToIso,
@@ -50,9 +50,12 @@ export const DateInput = ({
50
50
  disabled,
51
51
  min,
52
52
  max,
53
- id,
53
+ id: idProp,
54
54
  name,
55
55
  }: DateInputProps) => {
56
+ // Without an id the label isn't linked to the input.
57
+ const fallbackId = useId();
58
+ const id = idProp ?? fallbackId;
56
59
  const handle = (e: ChangeEvent<HTMLInputElement>) =>
57
60
  onChange(dateInputToIso(e.target.value));
58
61
  return (
@@ -89,9 +92,12 @@ export const DateTimeInput = ({
89
92
  disabled,
90
93
  min,
91
94
  max,
92
- id,
95
+ id: idProp,
93
96
  name,
94
97
  }: DateTimeInputProps) => {
98
+ // Without an id the label isn't linked to the input.
99
+ const fallbackId = useId();
100
+ const id = idProp ?? fallbackId;
95
101
  const handle = (e: ChangeEvent<HTMLInputElement>) =>
96
102
  onChange(dateTimeLocalToIso(e.target.value));
97
103
  return (
@@ -128,9 +134,12 @@ export const TimeInput = ({
128
134
  disabled,
129
135
  min,
130
136
  max,
131
- id,
137
+ id: idProp,
132
138
  name,
133
139
  }: TimeInputProps) => {
140
+ // Without an id the label isn't linked to the input.
141
+ const fallbackId = useId();
142
+ const id = idProp ?? fallbackId;
134
143
  const handle = (e: ChangeEvent<HTMLInputElement>) =>
135
144
  onChange(e.target.value || null);
136
145
  return (
@@ -23,6 +23,8 @@ export interface DropdownProps extends PropsWithSize, PropsWithVariant {
23
23
  isOpen?: boolean;
24
24
  /** Controlled mode: callback when open state changes */
25
25
  onOpenChange?: (isOpen: boolean) => void;
26
+ /** Accessible name for the trigger. Required when it shows only an `icon`. */
27
+ "aria-label"?: string;
26
28
  }
27
29
 
28
30
  const Dropdown = (props: DropdownProps) => {
@@ -63,6 +65,7 @@ const Dropdown = (props: DropdownProps) => {
63
65
  variant={props.variant}
64
66
  icon={props.icon}
65
67
  onClick={handleTriggerClick}
68
+ aria-label={props["aria-label"]}
66
69
  >
67
70
  {props.label && label}
68
71
  </IconButton>
@@ -71,6 +74,7 @@ const Dropdown = (props: DropdownProps) => {
71
74
  size={props.size}
72
75
  variant={props.variant}
73
76
  onClick={handleTriggerClick}
77
+ aria-label={props["aria-label"]}
74
78
  >
75
79
  {props.label && label}
76
80
  </Button>
@@ -25,6 +25,7 @@ const DropdownArrowButton = ({
25
25
  onClick={onClick}
26
26
  tabIndex={-1}
27
27
  disabled={disabled}
28
+ aria-label={isOpen ? "Hide options" : "Show options"}
28
29
  >
29
30
  <FontAwesomeIcon
30
31
  icon={faChevronDown}
@@ -15,7 +15,7 @@
15
15
  }
16
16
 
17
17
  .k-dropdown-arrow:hover:not(:disabled) {
18
- color: var(--primary-color);
18
+ color: var(--primary-color-text);
19
19
  }
20
20
 
21
21
  .k-dropdown-arrow:focus {
@@ -1,11 +1,11 @@
1
1
  "use client";
2
2
 
3
- import { faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons";
3
+ import { faEye, faEyeSlash, faXmark } from "@fortawesome/free-solid-svg-icons";
4
4
  import DropdownArrowButton from "../dropdownArrowButton/index.js";
5
+ import { ReactNode, useCallback, useRef, useState } from "react";
5
6
  import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
6
7
  import FieldShell from "../../forms/fieldShell/index.js";
7
8
  import { PropsWithSize } from "../../types.js";
8
- import { ReactNode, useState } from "react";
9
9
  import "./styles.css";
10
10
 
11
11
  interface InputProps extends PropsWithSize {
@@ -27,6 +27,8 @@ interface InputProps extends PropsWithSize {
27
27
 
28
28
  /** **_Optional_** if the field is disabled */
29
29
  disabled?: boolean;
30
+ /** **_Optional_** if the field is read-only: unlike `disabled`, it stays focusable and is submitted */
31
+ readOnly?: boolean;
30
32
  /** **_Optional_** if the field is a dropdown */
31
33
  dropDown?: boolean;
32
34
  /** **_Optional_** whether or not the drop down options are open */
@@ -73,6 +75,8 @@ interface InputProps extends PropsWithSize {
73
75
  onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>;
74
76
  /** **_Optional_** function that is called when the input is clicked */
75
77
  onClick?: React.MouseEventHandler<HTMLInputElement>;
78
+ /** **_Optional_** shows an × button at the end of a non-empty field; clear the value here. Sits at the right edge, so don't combine with `suffix` or `type="password"` */
79
+ onClear?: () => void;
76
80
  }
77
81
 
78
82
  const Input = (props: InputProps) => {
@@ -86,6 +90,31 @@ const Input = (props: InputProps) => {
86
90
  suffix,
87
91
  } = props;
88
92
  const [showPassword, setShowPassword] = useState(false);
93
+ const inputRef = useRef<HTMLInputElement | null>(null);
94
+ const { ref: forwardedRef } = props;
95
+
96
+ // Keep our own handle on the input (the clear button refocuses it) while
97
+ // still honouring a caller's `ref`.
98
+ const setRefs = useCallback(
99
+ (el: HTMLInputElement | null) => {
100
+ inputRef.current = el;
101
+ if (typeof forwardedRef === "function") forwardedRef(el);
102
+ else if (forwardedRef) forwardedRef.current = el;
103
+ },
104
+ [forwardedRef]
105
+ );
106
+
107
+ const showClear =
108
+ !!props.onClear &&
109
+ String(props.value ?? "") !== "" &&
110
+ !disabled &&
111
+ !props.readOnly;
112
+
113
+ const handleClear = () => {
114
+ props.onClear?.();
115
+ // The button unmounts once the field is empty; keep focus in the field.
116
+ inputRef.current?.focus();
117
+ };
89
118
 
90
119
  const inputId =
91
120
  props.id || `input-${Math.random().toString(36).substring(2, 9)}`;
@@ -102,6 +131,7 @@ const Input = (props: InputProps) => {
102
131
  "k-input",
103
132
  `k-input-${size}`,
104
133
  type === "password" && "k-input-password",
134
+ props.onClear && "k-input-clearable",
105
135
  hasAddon && "k-input-with-addon",
106
136
  prefix !== undefined && "k-input-with-prefix",
107
137
  suffix !== undefined && "k-input-with-suffix",
@@ -110,9 +140,11 @@ const Input = (props: InputProps) => {
110
140
  .join(" ");
111
141
 
112
142
  const renderAddon = (content: ReactNode, side: "prefix" | "suffix") => (
143
+ // Not aria-hidden: addons can be interactive (PhoneInput's country
144
+ // picker), and text like "$" or "USD" is useful context. Icons hide
145
+ // themselves.
113
146
  <span
114
147
  className={`k-input-addon k-input-addon-${side} k-input-addon-${size}`}
115
- aria-hidden="true"
116
148
  >
117
149
  {content}
118
150
  </span>
@@ -140,7 +172,7 @@ const Input = (props: InputProps) => {
140
172
  >
141
173
  {prefix !== undefined && renderAddon(prefix, "prefix")}
142
174
  <input
143
- ref={props.ref}
175
+ ref={setRefs}
144
176
  id={inputId}
145
177
  name={props.name}
146
178
  type={inputType}
@@ -152,6 +184,7 @@ const Input = (props: InputProps) => {
152
184
  autoFocus={props.autoFocus}
153
185
  autoComplete={props.autoComplete}
154
186
  disabled={disabled}
187
+ readOnly={props.readOnly}
155
188
  onChange={props.onChange}
156
189
  onFocus={props.onFocus}
157
190
  onBlur={props.onBlur}
@@ -159,6 +192,16 @@ const Input = (props: InputProps) => {
159
192
  onKeyDown={props.onKeyDown}
160
193
  className={inputClasses}
161
194
  />
195
+ {showClear && (
196
+ <button
197
+ type="button"
198
+ onClick={handleClear}
199
+ className="k-input-clear-button"
200
+ aria-label="Clear"
201
+ >
202
+ <FontAwesomeIcon icon={faXmark} />
203
+ </button>
204
+ )}
162
205
  {suffix !== undefined && renderAddon(suffix, "suffix")}
163
206
  {props.dropDown && (
164
207
  <DropdownArrowButton
@@ -51,6 +51,13 @@
51
51
  box-shadow: 0 0 0 0.1875rem var(--border-shadow-color-active);
52
52
  }
53
53
 
54
+ /* No opacity, unlike disabled: a read-only value keeps full text contrast. */
55
+ .k-input[readonly]:not(:disabled) {
56
+ background-color: var(--background-color-dark);
57
+ color: var(--text-color-secondary);
58
+ cursor: default;
59
+ }
60
+
54
61
  .k-input:disabled {
55
62
  background-color: var(--bg-color-disabled);
56
63
  color: var(--text-color-disabled);
@@ -58,9 +65,46 @@
58
65
  opacity: 0.6;
59
66
  }
60
67
 
68
+ .k-input-clearable {
69
+ padding-right: 2.25rem;
70
+ }
71
+
72
+ .k-input-clear-button {
73
+ position: absolute;
74
+ right: 0.5rem;
75
+ top: 50%;
76
+ transform: translateY(-50%);
77
+ display: flex;
78
+ align-items: center;
79
+ justify-content: center;
80
+ width: 1.5rem;
81
+ height: 1.5rem;
82
+ padding: 0;
83
+ border: none;
84
+ border-radius: 50%;
85
+ background: none;
86
+ color: var(--text-color-secondary);
87
+ cursor: pointer;
88
+ transition: color 0.2s ease-in-out;
89
+ }
90
+
91
+ .k-input-clear-button:hover {
92
+ color: var(--primary-color-text);
93
+ }
94
+
95
+ .k-input-clear-button:focus-visible {
96
+ outline: none;
97
+ box-shadow: 0 0 0 0.125rem var(--border-shadow-color-active);
98
+ }
99
+
61
100
  .k-password-toggle-button {
62
101
  position: absolute;
63
- right: 0.75rem;
102
+ right: 0.5rem;
103
+ top: 50%;
104
+ transform: translateY(-50%);
105
+ /* 24px hit area (WCAG 2.5.8) around the small eye icon. */
106
+ width: 1.5rem;
107
+ height: 1.5rem;
64
108
  background: none;
65
109
  border: none;
66
110
  cursor: pointer;
@@ -73,7 +117,7 @@
73
117
  }
74
118
 
75
119
  .k-password-toggle-button:hover {
76
- color: var(--primary-color);
120
+ color: var(--primary-color-text);
77
121
  }
78
122
 
79
123
  .k-password-toggle-button:focus {
@@ -179,6 +179,7 @@ const MultiSelect = <T,>(props: MultiSelectProps<T>) => {
179
179
  <button
180
180
  type="button"
181
181
  className="k-multiselect-selected-item-remove"
182
+ aria-label={`Remove ${String(v[props.labelKey])}`}
182
183
  onClick={(e) => {
183
184
  e.stopPropagation();
184
185
  handleOptionToggle(v);
@@ -165,7 +165,7 @@
165
165
  }
166
166
 
167
167
  .k-multiselect-selected-item-remove:hover {
168
- color: var(--danger-color);
168
+ color: var(--danger-color-text);
169
169
  }
170
170
 
171
171
  /* Dropdown */
@@ -169,10 +169,14 @@ const PhoneInput = (props: PhoneInputProps) => {
169
169
  className="k-phone-input-picker-trigger"
170
170
  onClick={() => !disabled && setPickerOpen((o) => !o)}
171
171
  disabled={disabled}
172
- aria-label={`Country: ${countryName(country)}`}
172
+ // Starts with the visible "+1" so voice-control users can say what
173
+ // they see (WCAG 2.5.3); the flag is decorative next to the name.
174
+ aria-label={`+${getCountryCallingCode(country)} ${countryName(country)}, change country`}
173
175
  aria-expanded={pickerOpen}
174
176
  >
175
- <span className="k-phone-input-flag">{flagEmoji(country)}</span>
177
+ <span className="k-phone-input-flag" aria-hidden="true">
178
+ {flagEmoji(country)}
179
+ </span>
176
180
  <span className="k-phone-input-dial">
177
181
  +{getCountryCallingCode(country)}
178
182
  </span>