tyrell-react 1.0.0-TC11 → 1.0.0-TC17

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 (105) hide show
  1. package/README.md +8 -8
  2. package/dist/components/TyButton.d.ts +2 -0
  3. package/dist/components/TyButton.d.ts.map +1 -1
  4. package/dist/components/TyButton.js +13 -5
  5. package/dist/components/TyButton.js.map +1 -1
  6. package/dist/components/TyCalendar.d.ts.map +1 -1
  7. package/dist/components/TyCalendar.js +13 -7
  8. package/dist/components/TyCalendar.js.map +1 -1
  9. package/dist/components/TyCheckbox.d.ts.map +1 -1
  10. package/dist/components/TyCheckbox.js +11 -14
  11. package/dist/components/TyCheckbox.js.map +1 -1
  12. package/dist/components/TyCopy.d.ts.map +1 -1
  13. package/dist/components/TyCopy.js +7 -3
  14. package/dist/components/TyCopy.js.map +1 -1
  15. package/dist/components/TyDatePicker.d.ts.map +1 -1
  16. package/dist/components/TyDatePicker.js +16 -10
  17. package/dist/components/TyDatePicker.js.map +1 -1
  18. package/dist/components/TyDropdown.d.ts +16 -5
  19. package/dist/components/TyDropdown.d.ts.map +1 -1
  20. package/dist/components/TyDropdown.js +34 -19
  21. package/dist/components/TyDropdown.js.map +1 -1
  22. package/dist/components/TyFileUpload.d.ts +31 -0
  23. package/dist/components/TyFileUpload.d.ts.map +1 -0
  24. package/dist/components/TyFileUpload.js +56 -0
  25. package/dist/components/TyFileUpload.js.map +1 -0
  26. package/dist/components/TyIcon.d.ts.map +1 -1
  27. package/dist/components/TyIcon.js +7 -6
  28. package/dist/components/TyIcon.js.map +1 -1
  29. package/dist/components/TyInput.d.ts.map +1 -1
  30. package/dist/components/TyInput.js +15 -4
  31. package/dist/components/TyInput.js.map +1 -1
  32. package/dist/components/TyModal.d.ts.map +1 -1
  33. package/dist/components/TyModal.js +35 -10
  34. package/dist/components/TyModal.js.map +1 -1
  35. package/dist/components/TyMultiselect.d.ts +6 -0
  36. package/dist/components/TyMultiselect.d.ts.map +1 -1
  37. package/dist/components/TyMultiselect.js +22 -14
  38. package/dist/components/TyMultiselect.js.map +1 -1
  39. package/dist/components/TyOption.d.ts.map +1 -1
  40. package/dist/components/TyOption.js +7 -3
  41. package/dist/components/TyOption.js.map +1 -1
  42. package/dist/components/TyPopup.d.ts.map +1 -1
  43. package/dist/components/TyPopup.js +7 -6
  44. package/dist/components/TyPopup.js.map +1 -1
  45. package/dist/components/TyRadio.d.ts.map +1 -1
  46. package/dist/components/TyRadio.js +6 -2
  47. package/dist/components/TyRadio.js.map +1 -1
  48. package/dist/components/TyRadioGroup.d.ts.map +1 -1
  49. package/dist/components/TyRadioGroup.js +5 -2
  50. package/dist/components/TyRadioGroup.js.map +1 -1
  51. package/dist/components/TyScrollContainer.d.ts.map +1 -1
  52. package/dist/components/TyScrollContainer.js +22 -4
  53. package/dist/components/TyScrollContainer.js.map +1 -1
  54. package/dist/components/TySwitch.d.ts.map +1 -1
  55. package/dist/components/TySwitch.js +8 -3
  56. package/dist/components/TySwitch.js.map +1 -1
  57. package/dist/components/TyTab.d.ts.map +1 -1
  58. package/dist/components/TyTab.js +3 -1
  59. package/dist/components/TyTab.js.map +1 -1
  60. package/dist/components/TyTag.d.ts.map +1 -1
  61. package/dist/components/TyTag.js +11 -5
  62. package/dist/components/TyTag.js.map +1 -1
  63. package/dist/components/TyTextarea.d.ts.map +1 -1
  64. package/dist/components/TyTextarea.js +10 -2
  65. package/dist/components/TyTextarea.js.map +1 -1
  66. package/dist/components/TyTooltip.d.ts.map +1 -1
  67. package/dist/components/TyTooltip.js +4 -3
  68. package/dist/components/TyTooltip.js.map +1 -1
  69. package/dist/components/index.d.ts +4 -0
  70. package/dist/components/index.d.ts.map +1 -1
  71. package/dist/components/index.js +2 -0
  72. package/dist/components/index.js.map +1 -1
  73. package/dist/utils/react-version.d.ts +2 -0
  74. package/dist/utils/react-version.d.ts.map +1 -0
  75. package/dist/utils/react-version.js +8 -0
  76. package/dist/utils/react-version.js.map +1 -0
  77. package/dist/utils/use-boolean-prop.d.ts +36 -0
  78. package/dist/utils/use-boolean-prop.d.ts.map +1 -0
  79. package/dist/utils/use-boolean-prop.js +62 -0
  80. package/dist/utils/use-boolean-prop.js.map +1 -0
  81. package/package.json +1 -1
  82. package/src/components/TyButton.tsx +17 -5
  83. package/src/components/TyCalendar.tsx +10 -7
  84. package/src/components/TyCheckbox.tsx +11 -13
  85. package/src/components/TyCopy.tsx +9 -4
  86. package/src/components/TyDatePicker.tsx +12 -11
  87. package/src/components/TyDropdown.tsx +56 -34
  88. package/src/components/TyFileUpload.tsx +108 -0
  89. package/src/components/TyIcon.tsx +6 -7
  90. package/src/components/TyInput.tsx +14 -4
  91. package/src/components/TyModal.tsx +31 -13
  92. package/src/components/TyMultiselect.tsx +25 -17
  93. package/src/components/TyOption.tsx +8 -3
  94. package/src/components/TyPopup.tsx +5 -6
  95. package/src/components/TyRadio.tsx +7 -2
  96. package/src/components/TyRadioGroup.tsx +6 -2
  97. package/src/components/TyScrollContainer.tsx +17 -2
  98. package/src/components/TySwitch.tsx +9 -3
  99. package/src/components/TyTab.tsx +3 -1
  100. package/src/components/TyTag.tsx +12 -5
  101. package/src/components/TyTextarea.tsx +10 -2
  102. package/src/components/TyTooltip.tsx +3 -3
  103. package/src/components/index.ts +7 -0
  104. package/src/utils/react-version.ts +8 -0
  105. package/src/utils/use-boolean-prop.ts +62 -0
@@ -1,4 +1,5 @@
1
1
  import React, { useEffect, useRef } from 'react';
2
+ import { useBooleanProperty } from '../utils/use-boolean-prop';
2
3
 
3
4
  // Type definitions for Ty Checkbox component
4
5
  export interface TyCheckboxProps extends Omit<React.HTMLAttributes<HTMLElement>, 'onChange' | 'onInput'> {
@@ -109,16 +110,13 @@ export const TyCheckbox = React.forwardRef<HTMLElement, TyCheckboxProps>(
109
110
  }
110
111
  }, [ref]);
111
112
 
112
- // Imperatively sync `checked` to the underlying property. React 18 sets
113
- // boolean attributes as empty strings on first render but doesn't reliably
114
- // remove them when the prop flips back to false on a custom element.
115
- useEffect(() => {
116
- const element = elementRef.current as any;
117
- if (!element) return;
118
- if (Boolean(element.checked) !== Boolean(checked)) {
119
- element.checked = Boolean(checked);
120
- }
121
- }, [checked]);
113
+ // Imperative property sync for boolean props (see use-boolean-prop.ts).
114
+ // React 18 sets boolean attributes as empty strings on first render but
115
+ // doesn't reliably remove them when the prop flips back to false on a
116
+ // custom element. React 19+ handles this natively.
117
+ const isChecked = useBooleanProperty(elementRef, 'checked', checked);
118
+ const isDisabled = useBooleanProperty(elementRef, 'disabled', disabled);
119
+ const isRequired = useBooleanProperty(elementRef, 'required', required);
122
120
 
123
121
  // Convert React props to web component attributes
124
122
  const webComponentProps: Record<string, any> = {
@@ -127,9 +125,9 @@ export const TyCheckbox = React.forwardRef<HTMLElement, TyCheckboxProps>(
127
125
  };
128
126
 
129
127
  // Add boolean attributes
130
- if (checked) webComponentProps.checked = '';
131
- if (disabled) webComponentProps.disabled = '';
132
- if (required) webComponentProps.required = '';
128
+ if (isChecked) webComponentProps.checked = '';
129
+ if (isDisabled) webComponentProps.disabled = '';
130
+ if (isRequired) webComponentProps.required = '';
133
131
 
134
132
  // Add string attributes
135
133
  if (value) webComponentProps.value = value;
@@ -1,4 +1,5 @@
1
1
  import React, { useEffect, useRef } from 'react';
2
+ import { useBooleanProperty } from '../utils/use-boolean-prop';
2
3
 
3
4
  // Type definitions for Ty Copy component
4
5
  export interface TyCopyProps extends Omit<React.HTMLAttributes<HTMLElement>, 'onChange'> {
@@ -53,6 +54,10 @@ export const TyCopy = React.forwardRef<HTMLElement, TyCopyProps>(
53
54
  }
54
55
  }, [ref]);
55
56
 
57
+ const isMultiline = useBooleanProperty(elementRef, 'multiline', multiline);
58
+ const isDisabled = useBooleanProperty(elementRef, 'disabled', disabled);
59
+ const isRequired = useBooleanProperty(elementRef, 'required', required);
60
+
56
61
  // Convert React props to web component attributes
57
62
  const webComponentProps: Record<string, any> = {
58
63
  ...props,
@@ -65,11 +70,11 @@ export const TyCopy = React.forwardRef<HTMLElement, TyCopyProps>(
65
70
  if (size) webComponentProps.size = size;
66
71
  if (flavor) webComponentProps.flavor = flavor;
67
72
  if (format) webComponentProps.format = format;
68
-
73
+
69
74
  // Add boolean attributes
70
- if (multiline) webComponentProps.multiline = '';
71
- if (disabled) webComponentProps.disabled = '';
72
- if (required) webComponentProps.required = '';
75
+ if (isMultiline) webComponentProps.multiline = '';
76
+ if (isDisabled) webComponentProps.disabled = '';
77
+ if (isRequired) webComponentProps.required = '';
73
78
 
74
79
  return React.createElement('ty-copy', webComponentProps);
75
80
  }
@@ -1,4 +1,6 @@
1
1
  import React, { useEffect, useRef, useCallback } from 'react';
2
+ import { needsPropertyBridge } from '../utils/react-version';
3
+ import { useBooleanProperty } from '../utils/use-boolean-prop';
2
4
 
3
5
  // Type definitions for Ty DatePicker component
4
6
  export interface TyDatePickerEventDetail {
@@ -92,8 +94,11 @@ export const TyDatePicker = React.forwardRef<HTMLElement, TyDatePickerProps>(
92
94
  }
93
95
  }, [ref]);
94
96
 
95
- // Sync value property with the web component
97
+ // Sync value property with the web component.
98
+ // React 18 workaround: prop-to-property bridging is unreliable for empty
99
+ // strings on custom elements. React 19+ handles this natively.
96
100
  useEffect(() => {
101
+ if (!needsPropertyBridge) return;
97
102
  const element = elementRef.current;
98
103
  if (element && value !== undefined) {
99
104
  // Set the value property directly on the element
@@ -181,22 +186,18 @@ export const TyDatePicker = React.forwardRef<HTMLElement, TyDatePickerProps>(
181
186
  webComponentProps.placeholder = placeholder;
182
187
  }
183
188
 
184
- if (required) {
185
- webComponentProps.required = ''; // Boolean attributes as empty string
186
- }
189
+ const isRequired = useBooleanProperty(elementRef, 'required', required);
190
+ const isDisabled = useBooleanProperty(elementRef, 'disabled', disabled);
191
+ const isClearable = useBooleanProperty(elementRef, 'clearable', clearable);
187
192
 
188
- if (disabled) {
189
- webComponentProps.disabled = ''; // Boolean attributes as empty string
190
- }
193
+ if (isRequired) webComponentProps.required = '';
194
+ if (isDisabled) webComponentProps.disabled = '';
195
+ if (isClearable) webComponentProps.clearable = '';
191
196
 
192
197
  if (name) {
193
198
  webComponentProps.name = name;
194
199
  }
195
200
 
196
- if (clearable) {
197
- webComponentProps.clearable = ''; // Boolean attributes as empty string
198
- }
199
-
200
201
  if (format) {
201
202
  webComponentProps.format = format;
202
203
  }
@@ -1,4 +1,6 @@
1
1
  import React, { useEffect, useRef, useCallback } from 'react';
2
+ import { needsPropertyBridge } from '../utils/react-version';
3
+ import { useBooleanProperty, coerceBool } from '../utils/use-boolean-prop';
2
4
 
3
5
  // Option data structure for data-driven approach
4
6
  export interface OptionData {
@@ -32,30 +34,42 @@ export interface TyDropdownProps extends Omit<React.HTMLAttributes<HTMLElement>,
32
34
 
33
35
  /** Disable the dropdown */
34
36
  disabled?: boolean;
35
-
37
+
38
+ /**
39
+ * Loading state — replaces the open popup options list with a centered
40
+ * spinner. Search input stays usable. Pair with `externalSearch` while
41
+ * fetching results from a parent-owned data source.
42
+ */
43
+ loading?: boolean;
44
+
36
45
  /** Make dropdown readonly */
37
46
  readonly?: boolean;
38
47
 
39
48
  /** Required field */
40
49
  required?: boolean;
41
50
 
42
- /** Enable search functionality */
43
- searchable?: boolean;
44
-
45
51
  /** Show clear button */
46
52
  clearable?: boolean;
47
-
53
+
48
54
  /** Disable clear button (alias for clearable={false}) */
49
55
  notClearable?: boolean;
50
-
56
+
51
57
  /** Debounce in milliseconds (0-5000) */
52
58
  debounce?: number;
53
-
54
- /** Disable search functionality (ClojureScript: not-searchable) */
55
- notSearchable?: boolean;
56
-
57
- /** @deprecated Use notSearchable instead. External search handling */
59
+
60
+ /**
61
+ * Switch to external (remote) search mode. Default is `false` — the dropdown
62
+ * filters its options locally. When `true`, the dropdown stops filtering and
63
+ * dispatches `search` events on each keystroke; the parent owns filtering
64
+ * and updates the children.
65
+ */
58
66
  externalSearch?: boolean;
67
+
68
+ /** @deprecated Use `externalSearch` instead. */
69
+ notSearchable?: boolean;
70
+
71
+ /** @deprecated Use `externalSearch` instead. Pass `searchable={false}` was equivalent to `externalSearch={true}`. */
72
+ searchable?: boolean;
59
73
 
60
74
  /** Form field name for form submission */
61
75
  name?: string;
@@ -79,8 +93,10 @@ export const TyDropdown = React.forwardRef<HTMLElement, TyDropdownProps>(
79
93
  onChange,
80
94
  onSearch,
81
95
  disabled,
82
- notSearchable,
96
+ loading,
83
97
  externalSearch,
98
+ notSearchable,
99
+ searchable,
84
100
  clearable,
85
101
  notClearable,
86
102
  debounce,
@@ -129,7 +145,9 @@ export const TyDropdown = React.forwardRef<HTMLElement, TyDropdownProps>(
129
145
  // it changes. React 18's prop-to-property bridging for custom elements is
130
146
  // unreliable for empty strings (programmatic resets), so we set the
131
147
  // property directly to guarantee the dropdown clears on `value=""`.
148
+ // React 19+ handles this natively, so the effect short-circuits there.
132
149
  useEffect(() => {
150
+ if (!needsPropertyBridge) return;
133
151
  const element = elementRef.current as any;
134
152
  if (!element) return;
135
153
  if (element.value !== value) {
@@ -169,41 +187,45 @@ export const TyDropdown = React.forwardRef<HTMLElement, TyDropdownProps>(
169
187
  return children;
170
188
  };
171
189
 
190
+ // Imperative property sync for boolean props (see use-boolean-prop.ts).
191
+ const isDisabled = useBooleanProperty(elementRef, 'disabled', disabled);
192
+ const isLoading = useBooleanProperty(elementRef, 'loading', loading);
193
+ const isExternalSearch = coerceBool(externalSearch) || coerceBool(notSearchable) || searchable === false;
194
+ useBooleanProperty(elementRef, 'externalSearch', isExternalSearch);
195
+ // clearable: explicit boolean OR the `notClearable` alias inverts it
196
+ const isClearable = clearable !== undefined
197
+ ? coerceBool(clearable)
198
+ : (coerceBool(notClearable) ? false : undefined);
199
+ useEffect(() => {
200
+ if (!needsPropertyBridge) return;
201
+ if (isClearable === undefined) return;
202
+ const el = elementRef.current as any;
203
+ if (!el) return;
204
+ if (Boolean(el.clearable) !== isClearable) el.clearable = isClearable;
205
+ }, [isClearable]);
206
+
172
207
  // Convert React props to web component attributes
173
208
  const webComponentProps: Record<string, any> = {
174
209
  ...props,
175
210
  ref: elementRef,
176
211
  };
177
212
 
178
- // Add conditional attributes
179
- if (disabled) webComponentProps.disabled = '';
180
-
181
- // Handle search functionality (prefer not-searchable over deprecated externalSearch)
182
- if (notSearchable) {
183
- webComponentProps['not-searchable'] = '';
184
- } else if (externalSearch) {
185
- // Support deprecated externalSearch for backward compatibility
186
- webComponentProps['not-searchable'] = '';
187
- }
188
-
213
+ if (isDisabled) webComponentProps.disabled = '';
214
+ if (isLoading) webComponentProps.loading = '';
215
+ if (isExternalSearch) webComponentProps['external-search'] = '';
216
+
189
217
  // Handle clearable functionality
190
- if (clearable !== undefined) {
191
- if (clearable) {
192
- webComponentProps.clearable = '';
193
- } else {
194
- webComponentProps['not-clearable'] = '';
195
- }
196
- }
197
-
198
- if (notClearable) {
218
+ if (isClearable === true) {
219
+ webComponentProps.clearable = '';
220
+ } else if (isClearable === false) {
199
221
  webComponentProps['not-clearable'] = '';
200
222
  }
201
-
223
+
202
224
  // Add debounce attribute
203
225
  if (debounce !== undefined) {
204
226
  webComponentProps.debounce = debounce;
205
227
  }
206
-
228
+
207
229
  // Add string attributes
208
230
  if (name) webComponentProps.name = name;
209
231
 
@@ -0,0 +1,108 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { useBooleanProperty } from '../utils/use-boolean-prop';
3
+
4
+ // Type definitions for Ty File Upload component
5
+ export interface TyFileUploadProps extends Omit<React.HTMLAttributes<HTMLElement>, 'onChange'> {
6
+ /** Form field name — used as the FormData key */
7
+ name?: string;
8
+
9
+ /** Allow selecting multiple files */
10
+ multiple?: boolean;
11
+
12
+ /** File type filter passed to the underlying input (e.g. "image/*", ".pdf,.docx") */
13
+ accept?: string;
14
+
15
+ /** Field label rendered above the drop zone */
16
+ label?: string;
17
+
18
+ /** Hint text shown inside the drop zone when no files are selected */
19
+ placeholder?: string;
20
+
21
+ /** Disable interaction */
22
+ disabled?: boolean;
23
+
24
+ /** Mark the field as required (shows asterisk) */
25
+ required?: boolean;
26
+
27
+ /** Validation error message — also applies danger border styling */
28
+ error?: string;
29
+
30
+ /**
31
+ * Fires when the selection changes — browse, drag-drop, or remove.
32
+ * Maps to the native 'change' event from ty-file-upload.
33
+ */
34
+ onChange?: (event: CustomEvent<TyFileUploadEventDetail>) => void;
35
+ }
36
+
37
+ export interface TyFileUploadEventDetail {
38
+ value: File[];
39
+ files: File[];
40
+ names: string[];
41
+ }
42
+
43
+ // React wrapper for ty-file-upload web component
44
+ export const TyFileUpload = React.forwardRef<HTMLElement, TyFileUploadProps>(
45
+ ({
46
+ name,
47
+ multiple,
48
+ accept,
49
+ label,
50
+ placeholder,
51
+ disabled,
52
+ required,
53
+ error,
54
+ onChange,
55
+ ...props
56
+ }, ref) => {
57
+ const elementRef = useRef<HTMLElement>(null);
58
+
59
+ useEffect(() => {
60
+ const element = elementRef.current;
61
+ if (!element) return;
62
+
63
+ const handleChange = (event: Event) => {
64
+ if (onChange) {
65
+ onChange(event as CustomEvent<TyFileUploadEventDetail>);
66
+ }
67
+ };
68
+
69
+ element.addEventListener('change', handleChange);
70
+ return () => {
71
+ element.removeEventListener('change', handleChange);
72
+ };
73
+ }, [onChange]);
74
+
75
+ useEffect(() => {
76
+ if (ref && elementRef.current) {
77
+ if (typeof ref === 'function') {
78
+ ref(elementRef.current);
79
+ } else {
80
+ ref.current = elementRef.current;
81
+ }
82
+ }
83
+ }, [ref]);
84
+
85
+ const isMultiple = useBooleanProperty(elementRef, 'multiple', multiple);
86
+ const isDisabled = useBooleanProperty(elementRef, 'disabled', disabled);
87
+ const isRequired = useBooleanProperty(elementRef, 'required', required);
88
+
89
+ const webComponentProps: Record<string, any> = {
90
+ ...props,
91
+ ref: elementRef,
92
+ };
93
+
94
+ if (isMultiple) webComponentProps.multiple = '';
95
+ if (isDisabled) webComponentProps.disabled = '';
96
+ if (isRequired) webComponentProps.required = '';
97
+
98
+ if (name) webComponentProps.name = name;
99
+ if (accept) webComponentProps.accept = accept;
100
+ if (label) webComponentProps.label = label;
101
+ if (placeholder) webComponentProps.placeholder = placeholder;
102
+ if (error) webComponentProps.error = error;
103
+
104
+ return React.createElement('ty-file-upload', webComponentProps);
105
+ }
106
+ );
107
+
108
+ TyFileUpload.displayName = 'TyFileUpload';
@@ -1,4 +1,5 @@
1
1
  import React, { useEffect, useRef } from 'react';
2
+ import { useBooleanProperty } from '../utils/use-boolean-prop';
2
3
 
3
4
  // Type definitions for Ty Icon component
4
5
  export interface TyIconProps extends React.HTMLAttributes<HTMLElement> {
@@ -37,6 +38,9 @@ export const TyIcon = React.forwardRef<HTMLElement, TyIconProps>(
37
38
  }
38
39
  }, [ref]);
39
40
 
41
+ const isSpin = useBooleanProperty(elementRef, 'spin', spin);
42
+ const isPulse = useBooleanProperty(elementRef, 'pulse', pulse);
43
+
40
44
  // Convert React props to web component attributes
41
45
  const webComponentProps: Record<string, any> = {
42
46
  ...props,
@@ -49,13 +53,8 @@ export const TyIcon = React.forwardRef<HTMLElement, TyIconProps>(
49
53
  webComponentProps.size = size;
50
54
  }
51
55
 
52
- if (spin) {
53
- webComponentProps.spin = ''; // Boolean attributes as empty string
54
- }
55
-
56
- if (pulse) {
57
- webComponentProps.pulse = ''; // Boolean attributes as empty string
58
- }
56
+ if (isSpin) webComponentProps.spin = '';
57
+ if (isPulse) webComponentProps.pulse = '';
59
58
 
60
59
  if (tempo) {
61
60
  webComponentProps.tempo = tempo;
@@ -1,4 +1,6 @@
1
1
  import React, { useEffect, useRef, useCallback } from 'react';
2
+ import { needsPropertyBridge } from '../utils/react-version';
3
+ import { useBooleanProperty } from '../utils/use-boolean-prop';
2
4
 
3
5
  // Event detail structure for ty-input events
4
6
  export interface TyInputEventDetail {
@@ -91,7 +93,7 @@ let _warnedOnInputProp = false;
91
93
 
92
94
  // React wrapper for ty-input web component
93
95
  export const TyInput = React.forwardRef<HTMLElement, TyInputProps>(
94
- ({ onChange, onChangeCommit, onFocus, onBlur, disabled, name, checked, debounce, ...props }, ref) => {
96
+ ({ onChange, onChangeCommit, onFocus, onBlur, disabled, required, name, checked, debounce, ...props }, ref) => {
95
97
  const elementRef = useRef<HTMLElement>(null);
96
98
 
97
99
  // Catch the most common mistake: passing `onInput` (React's prop) instead
@@ -193,8 +195,10 @@ export const TyInput = React.forwardRef<HTMLElement, TyInputProps>(
193
195
  // Imperatively sync `value` to the underlying element's property whenever
194
196
  // the React prop changes. React 18's prop-to-property bridging for custom
195
197
  // elements is unreliable for empty strings, so we set the property directly
196
- // to guarantee resets (`value=""`) clear the visible content.
198
+ // to guarantee resets (`value=""`) clear the visible content. React 19+
199
+ // handles this natively, so the effect short-circuits there.
197
200
  useEffect(() => {
201
+ if (!needsPropertyBridge) return;
198
202
  const element = elementRef.current as any;
199
203
  if (!element) return;
200
204
  const next = (props as any).value ?? '';
@@ -203,6 +207,11 @@ export const TyInput = React.forwardRef<HTMLElement, TyInputProps>(
203
207
  }
204
208
  }, [(props as any).value]);
205
209
 
210
+ // Imperative property sync for boolean props (see use-boolean-prop.ts).
211
+ const isDisabled = useBooleanProperty(elementRef, 'disabled', disabled);
212
+ const isRequired = useBooleanProperty(elementRef, 'required', required);
213
+ const isChecked = useBooleanProperty(elementRef, 'checked', checked);
214
+
206
215
  // Convert React props to web component attributes
207
216
  const webComponentProps: Record<string, any> = {
208
217
  ...props,
@@ -210,8 +219,9 @@ export const TyInput = React.forwardRef<HTMLElement, TyInputProps>(
210
219
  };
211
220
 
212
221
  // Add conditional attributes
213
- if (disabled) webComponentProps.disabled = '';
214
- if (checked) webComponentProps.checked = '';
222
+ if (isDisabled) webComponentProps.disabled = '';
223
+ if (isRequired) webComponentProps.required = '';
224
+ if (isChecked) webComponentProps.checked = '';
215
225
 
216
226
  // Add string attributes
217
227
  if (name) webComponentProps.name = name;
@@ -1,4 +1,6 @@
1
1
  import React, { useEffect, useRef, useImperativeHandle } from 'react';
2
+ import { useBooleanProperty, coerceBool } from '../utils/use-boolean-prop';
3
+ import { needsPropertyBridge } from '../utils/react-version';
2
4
 
3
5
  // Event detail structure for modal events
4
6
  export interface TyModalEventDetail {
@@ -104,33 +106,49 @@ export const TyModal = React.forwardRef<TyModalRef, TyModalProps>(
104
106
  };
105
107
  }, [onOpen, onClose]);
106
108
 
109
+ // Imperative property sync for boolean props (see use-boolean-prop.ts).
110
+ // Without this, flipping `open` from `true` to `false` on React 18 leaves
111
+ // the `open` attribute on the element and the modal stays open.
112
+ const isOpen = useBooleanProperty(elementRef, 'open', open);
113
+ const isProt = useBooleanProperty(elementRef, 'protected', isProtected);
114
+
115
+ // For default-true booleans (backdrop, closeOn*), only the explicit-false
116
+ // case is interesting — bridge it imperatively too so it propagates.
117
+ useEffect(() => {
118
+ if (!needsPropertyBridge) return;
119
+ const el = elementRef.current as any;
120
+ if (!el) return;
121
+ const setIf = (prop: string, raw: unknown) => {
122
+ if (raw === undefined) return;
123
+ const next = coerceBool(raw);
124
+ if (Boolean(el[prop]) !== next) el[prop] = next;
125
+ };
126
+ setIf('backdrop', backdrop);
127
+ setIf('closeOnOutsideClick', closeOnOutsideClick);
128
+ setIf('closeOnEscape', closeOnEscape);
129
+ }, [backdrop, closeOnOutsideClick, closeOnEscape]);
130
+
107
131
  // Convert React props to web component attributes
108
132
  const webComponentProps: Record<string, any> = {
109
133
  ...props,
110
134
  ref: elementRef,
111
135
  };
112
136
 
113
- // Add boolean attributes using correct HTML attribute names
114
- if (open) {
115
- webComponentProps.open = ''; // Boolean attributes as empty string
116
- }
137
+ if (isOpen) webComponentProps.open = '';
138
+ if (isProt) webComponentProps.protected = '';
117
139
 
118
- if (backdrop === false) { // Only set if explicitly false (default is true)
140
+ // Default-true booleans use "false" string on the attribute side; the
141
+ // core's parseBoolAttr handles it correctly.
142
+ if (backdrop !== undefined && !coerceBool(backdrop)) {
119
143
  webComponentProps.backdrop = 'false';
120
144
  }
121
-
122
- if (closeOnOutsideClick === false) { // Only set if explicitly false (default is true)
145
+ if (closeOnOutsideClick !== undefined && !coerceBool(closeOnOutsideClick)) {
123
146
  webComponentProps['close-on-outside-click'] = 'false';
124
147
  }
125
-
126
- if (closeOnEscape === false) { // Only set if explicitly false (default is true)
148
+ if (closeOnEscape !== undefined && !coerceBool(closeOnEscape)) {
127
149
  webComponentProps['close-on-escape'] = 'false';
128
150
  }
129
151
 
130
- if (isProtected) {
131
- webComponentProps.protected = ''; // Boolean attributes as empty string
132
- }
133
-
134
152
  return React.createElement(
135
153
  'ty-modal',
136
154
  webComponentProps,
@@ -1,4 +1,6 @@
1
1
  import React, { useEffect, useRef, useCallback } from 'react';
2
+ import { needsPropertyBridge } from '../utils/react-version';
3
+ import { useBooleanProperty } from '../utils/use-boolean-prop';
2
4
 
3
5
  // Type definitions for Ty Multiselect component
4
6
  export interface TyMultiselectEventDetail {
@@ -20,7 +22,14 @@ export interface TyMultiselectProps extends Omit<React.HTMLAttributes<HTMLElemen
20
22
 
21
23
  /** Disable the multiselect component */
22
24
  disabled?: boolean;
23
-
25
+
26
+ /**
27
+ * Loading state — replaces the available-options area with a centered
28
+ * spinner. Search input stays usable. Pair with `externalSearch` while
29
+ * fetching results from a parent-owned data source.
30
+ */
31
+ loading?: boolean;
32
+
24
33
  /** Make the multiselect read-only */
25
34
  readonly?: boolean;
26
35
 
@@ -67,6 +76,7 @@ export const TyMultiselect = React.forwardRef<HTMLElement, TyMultiselectProps>(
67
76
  value,
68
77
  placeholder,
69
78
  disabled,
79
+ loading,
70
80
  readonly,
71
81
  flavor,
72
82
  label,
@@ -95,7 +105,9 @@ export const TyMultiselect = React.forwardRef<HTMLElement, TyMultiselectProps>(
95
105
 
96
106
  // Imperatively sync `value` to the underlying property so resets
97
107
  // (`value=""` or null) reliably clear the visible selection.
108
+ // React 18 workaround; React 19+ handles this natively.
98
109
  useEffect(() => {
110
+ if (!needsPropertyBridge) return;
99
111
  const element = elementRef.current as any;
100
112
  if (!element) return;
101
113
  const next = (props as any).value ?? '';
@@ -133,6 +145,13 @@ export const TyMultiselect = React.forwardRef<HTMLElement, TyMultiselectProps>(
133
145
  };
134
146
  }, [handleChange, handleSearch, onChange, onSearch]);
135
147
 
148
+ // Imperative property sync for boolean props (see use-boolean-prop.ts).
149
+ const isDisabled = useBooleanProperty(elementRef, 'disabled', disabled);
150
+ const isLoading = useBooleanProperty(elementRef, 'loading', loading);
151
+ const isReadonly = useBooleanProperty(elementRef, 'readonly', readonly);
152
+ const isRequired = useBooleanProperty(elementRef, 'required', required);
153
+ const isExternalSearch = useBooleanProperty(elementRef, 'externalSearch', externalSearch);
154
+
136
155
  // Convert React props to web component attributes
137
156
  const webComponentProps: Record<string, any> = {
138
157
  ...props,
@@ -150,13 +169,11 @@ export const TyMultiselect = React.forwardRef<HTMLElement, TyMultiselectProps>(
150
169
  webComponentProps.placeholder = placeholder;
151
170
  }
152
171
 
153
- if (disabled) {
154
- webComponentProps.disabled = ''; // Boolean attributes as empty string
155
- }
156
-
157
- if (readonly) {
158
- webComponentProps.readonly = ''; // Boolean attributes as empty string
159
- }
172
+ if (isLoading) webComponentProps.loading = '';
173
+ if (isDisabled) webComponentProps.disabled = '';
174
+ if (isReadonly) webComponentProps.readonly = '';
175
+ if (isRequired) webComponentProps.required = '';
176
+ if (isExternalSearch) webComponentProps['external-search'] = '';
160
177
 
161
178
  if (flavor) {
162
179
  webComponentProps.flavor = flavor;
@@ -166,18 +183,9 @@ export const TyMultiselect = React.forwardRef<HTMLElement, TyMultiselectProps>(
166
183
  webComponentProps.label = label;
167
184
  }
168
185
 
169
- if (required) {
170
- webComponentProps.required = ''; // Boolean attributes as empty string
171
- }
172
-
173
186
  if (name) {
174
187
  webComponentProps.name = name;
175
188
  }
176
-
177
- // External (remote) search mode: parent owns filtering, multiselect dispatches search events
178
- if (externalSearch) {
179
- webComponentProps['external-search'] = '';
180
- }
181
189
 
182
190
  // Add debounce attribute
183
191
  if (debounce !== undefined) {
@@ -1,4 +1,5 @@
1
1
  import React, { useEffect, useRef } from 'react';
2
+ import { useBooleanProperty } from '../utils/use-boolean-prop';
2
3
 
3
4
  // Type definitions for Ty Option component
4
5
  export interface TyOptionProps extends React.HTMLAttributes<HTMLElement> {
@@ -25,13 +26,17 @@ export const TyOption = React.forwardRef<HTMLElement, TyOptionProps>(
25
26
  }
26
27
  }, [ref]);
27
28
 
29
+ const isDisabled = useBooleanProperty(elementRef, 'disabled', disabled);
30
+ const isSelected = useBooleanProperty(elementRef, 'selected', selected);
31
+ const isHidden = useBooleanProperty(elementRef, 'hidden', hidden);
32
+
28
33
  return React.createElement(
29
34
  'ty-option',
30
35
  {
31
36
  ...props,
32
- ...(disabled && { disabled: "" }),
33
- ...(selected && { selected: "" }),
34
- ...(hidden && { hidden: "" }),
37
+ ...(isDisabled && { disabled: "" }),
38
+ ...(isSelected && { selected: "" }),
39
+ ...(isHidden && { hidden: "" }),
35
40
  ref: elementRef,
36
41
  },
37
42
  children