tyrell-react 1.0.0-TC12 → 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 (91) hide show
  1. package/README.md +8 -8
  2. package/dist/components/TyButton.d.ts.map +1 -1
  3. package/dist/components/TyButton.js +11 -5
  4. package/dist/components/TyButton.js.map +1 -1
  5. package/dist/components/TyCalendar.d.ts.map +1 -1
  6. package/dist/components/TyCalendar.js +7 -6
  7. package/dist/components/TyCalendar.js.map +1 -1
  8. package/dist/components/TyCheckbox.d.ts.map +1 -1
  9. package/dist/components/TyCheckbox.js +11 -18
  10. package/dist/components/TyCheckbox.js.map +1 -1
  11. package/dist/components/TyCopy.d.ts.map +1 -1
  12. package/dist/components/TyCopy.js +7 -3
  13. package/dist/components/TyCopy.js.map +1 -1
  14. package/dist/components/TyDatePicker.d.ts.map +1 -1
  15. package/dist/components/TyDatePicker.js +10 -9
  16. package/dist/components/TyDatePicker.js.map +1 -1
  17. package/dist/components/TyDropdown.d.ts.map +1 -1
  18. package/dist/components/TyDropdown.js +27 -15
  19. package/dist/components/TyDropdown.js.map +1 -1
  20. package/dist/components/TyFileUpload.d.ts.map +1 -1
  21. package/dist/components/TyFileUpload.js +7 -3
  22. package/dist/components/TyFileUpload.js.map +1 -1
  23. package/dist/components/TyIcon.d.ts.map +1 -1
  24. package/dist/components/TyIcon.js +7 -6
  25. package/dist/components/TyIcon.js.map +1 -1
  26. package/dist/components/TyInput.d.ts.map +1 -1
  27. package/dist/components/TyInput.js +10 -3
  28. package/dist/components/TyInput.js.map +1 -1
  29. package/dist/components/TyModal.d.ts.map +1 -1
  30. package/dist/components/TyModal.js +35 -10
  31. package/dist/components/TyModal.js.map +1 -1
  32. package/dist/components/TyMultiselect.d.ts.map +1 -1
  33. package/dist/components/TyMultiselect.js +16 -14
  34. package/dist/components/TyMultiselect.js.map +1 -1
  35. package/dist/components/TyOption.d.ts.map +1 -1
  36. package/dist/components/TyOption.js +7 -3
  37. package/dist/components/TyOption.js.map +1 -1
  38. package/dist/components/TyPopup.d.ts.map +1 -1
  39. package/dist/components/TyPopup.js +7 -6
  40. package/dist/components/TyPopup.js.map +1 -1
  41. package/dist/components/TyRadio.d.ts.map +1 -1
  42. package/dist/components/TyRadio.js +6 -17
  43. package/dist/components/TyRadio.js.map +1 -1
  44. package/dist/components/TyRadioGroup.d.ts.map +1 -1
  45. package/dist/components/TyRadioGroup.js +5 -2
  46. package/dist/components/TyRadioGroup.js.map +1 -1
  47. package/dist/components/TyScrollContainer.d.ts.map +1 -1
  48. package/dist/components/TyScrollContainer.js +22 -4
  49. package/dist/components/TyScrollContainer.js.map +1 -1
  50. package/dist/components/TySwitch.d.ts.map +1 -1
  51. package/dist/components/TySwitch.js +8 -18
  52. package/dist/components/TySwitch.js.map +1 -1
  53. package/dist/components/TyTab.d.ts.map +1 -1
  54. package/dist/components/TyTab.js +3 -1
  55. package/dist/components/TyTab.js.map +1 -1
  56. package/dist/components/TyTag.d.ts.map +1 -1
  57. package/dist/components/TyTag.js +11 -5
  58. package/dist/components/TyTag.js.map +1 -1
  59. package/dist/components/TyTextarea.d.ts.map +1 -1
  60. package/dist/components/TyTextarea.js +5 -2
  61. package/dist/components/TyTextarea.js.map +1 -1
  62. package/dist/components/TyTooltip.d.ts.map +1 -1
  63. package/dist/components/TyTooltip.js +4 -3
  64. package/dist/components/TyTooltip.js.map +1 -1
  65. package/dist/utils/use-boolean-prop.d.ts +36 -0
  66. package/dist/utils/use-boolean-prop.d.ts.map +1 -0
  67. package/dist/utils/use-boolean-prop.js +62 -0
  68. package/dist/utils/use-boolean-prop.js.map +1 -0
  69. package/package.json +1 -1
  70. package/src/components/TyButton.tsx +12 -5
  71. package/src/components/TyCalendar.tsx +5 -6
  72. package/src/components/TyCheckbox.tsx +11 -16
  73. package/src/components/TyCopy.tsx +9 -4
  74. package/src/components/TyDatePicker.tsx +7 -10
  75. package/src/components/TyDropdown.tsx +26 -20
  76. package/src/components/TyFileUpload.tsx +8 -3
  77. package/src/components/TyIcon.tsx +6 -7
  78. package/src/components/TyInput.tsx +10 -3
  79. package/src/components/TyModal.tsx +31 -13
  80. package/src/components/TyMultiselect.tsx +13 -17
  81. package/src/components/TyOption.tsx +8 -3
  82. package/src/components/TyPopup.tsx +5 -6
  83. package/src/components/TyRadio.tsx +6 -15
  84. package/src/components/TyRadioGroup.tsx +6 -2
  85. package/src/components/TyScrollContainer.tsx +17 -2
  86. package/src/components/TySwitch.tsx +8 -16
  87. package/src/components/TyTab.tsx +3 -1
  88. package/src/components/TyTag.tsx +12 -5
  89. package/src/components/TyTextarea.tsx +6 -2
  90. package/src/components/TyTooltip.tsx +3 -3
  91. package/src/utils/use-boolean-prop.ts +62 -0
@@ -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,5 +1,6 @@
1
1
  import React, { useEffect, useRef, useCallback } from 'react';
2
2
  import { needsPropertyBridge } from '../utils/react-version';
3
+ import { useBooleanProperty } from '../utils/use-boolean-prop';
3
4
 
4
5
  // Type definitions for Ty Multiselect component
5
6
  export interface TyMultiselectEventDetail {
@@ -144,6 +145,13 @@ export const TyMultiselect = React.forwardRef<HTMLElement, TyMultiselectProps>(
144
145
  };
145
146
  }, [handleChange, handleSearch, onChange, onSearch]);
146
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
+
147
155
  // Convert React props to web component attributes
148
156
  const webComponentProps: Record<string, any> = {
149
157
  ...props,
@@ -161,14 +169,11 @@ export const TyMultiselect = React.forwardRef<HTMLElement, TyMultiselectProps>(
161
169
  webComponentProps.placeholder = placeholder;
162
170
  }
163
171
 
164
- if (loading) webComponentProps.loading = '';
165
- if (disabled) {
166
- webComponentProps.disabled = ''; // Boolean attributes as empty string
167
- }
168
-
169
- if (readonly) {
170
- webComponentProps.readonly = ''; // Boolean attributes as empty string
171
- }
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'] = '';
172
177
 
173
178
  if (flavor) {
174
179
  webComponentProps.flavor = flavor;
@@ -178,18 +183,9 @@ export const TyMultiselect = React.forwardRef<HTMLElement, TyMultiselectProps>(
178
183
  webComponentProps.label = label;
179
184
  }
180
185
 
181
- if (required) {
182
- webComponentProps.required = ''; // Boolean attributes as empty string
183
- }
184
-
185
186
  if (name) {
186
187
  webComponentProps.name = name;
187
188
  }
188
-
189
- // External (remote) search mode: parent owns filtering, multiselect dispatches search events
190
- if (externalSearch) {
191
- webComponentProps['external-search'] = '';
192
- }
193
189
 
194
190
  // Add debounce attribute
195
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
@@ -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 Popup component
4
5
  export interface TyPopupProps extends Omit<React.HTMLAttributes<HTMLElement>, 'onClose'> {
@@ -92,13 +93,11 @@ export const TyPopup = React.forwardRef<TyPopupElement, TyPopupProps>(
92
93
  webComponentProps.offset = offset.toString();
93
94
  }
94
95
 
95
- if (manual) {
96
- webComponentProps.manual = ''; // Boolean attributes as empty string
97
- }
96
+ const isManual = useBooleanProperty(elementRef, 'manual', manual);
97
+ const isDisableClose = useBooleanProperty(elementRef, 'disableClose', disableClose);
98
98
 
99
- if (disableClose) {
100
- webComponentProps['disable-close'] = ''; // Boolean attributes as empty string
101
- }
99
+ if (isManual) webComponentProps.manual = '';
100
+ if (isDisableClose) webComponentProps['disable-close'] = '';
102
101
 
103
102
  return React.createElement(
104
103
  'ty-popup',
@@ -1,5 +1,5 @@
1
1
  import React, { useEffect, useRef } from 'react';
2
- import { needsPropertyBridge } from '../utils/react-version';
2
+ import { useBooleanProperty } from '../utils/use-boolean-prop';
3
3
 
4
4
  export interface TyRadioProps extends React.HTMLAttributes<HTMLElement> {
5
5
  /** Form field value (selected by parent ty-radio-group when matches its `value`) */
@@ -38,26 +38,17 @@ export const TyRadio = React.forwardRef<HTMLElement, TyRadioProps>(
38
38
  }
39
39
  }, [ref]);
40
40
 
41
- // Imperatively sync `checked` to the underlying property. React 18 sets
42
- // boolean attributes as empty strings on first render but doesn't reliably
43
- // remove them when the prop flips back to false on a custom element.
44
- // React 19+ handles boolean prop-to-property bridging natively.
45
- useEffect(() => {
46
- if (!needsPropertyBridge) return;
47
- const element = elementRef.current as any;
48
- if (!element) return;
49
- if (Boolean(element.checked) !== Boolean(checked)) {
50
- element.checked = Boolean(checked);
51
- }
52
- }, [checked]);
41
+ // Imperative property sync for boolean props (see use-boolean-prop.ts).
42
+ const isChecked = useBooleanProperty(elementRef, 'checked', checked);
43
+ const isDisabled = useBooleanProperty(elementRef, 'disabled', disabled);
53
44
 
54
45
  const webComponentProps: Record<string, any> = {
55
46
  ...props,
56
47
  ref: elementRef,
57
48
  };
58
49
 
59
- if (checked) webComponentProps.checked = '';
60
- if (disabled) webComponentProps.disabled = '';
50
+ if (isChecked) webComponentProps.checked = '';
51
+ if (isDisabled) webComponentProps.disabled = '';
61
52
 
62
53
  if (value !== undefined) webComponentProps.value = value;
63
54
  if (size) webComponentProps.size = size;
@@ -1,4 +1,5 @@
1
1
  import React, { useEffect, useRef } from 'react';
2
+ import { useBooleanProperty } from '../utils/use-boolean-prop';
2
3
 
3
4
  export interface TyRadioGroupProps extends Omit<React.HTMLAttributes<HTMLElement>, 'onChange' | 'onInput'> {
4
5
  /** Currently selected value (matches one child `<TyRadio value="...">`) */
@@ -98,13 +99,16 @@ export const TyRadioGroup = React.forwardRef<HTMLElement, TyRadioGroupProps>(
98
99
  }
99
100
  }, [ref]);
100
101
 
102
+ const isDisabled = useBooleanProperty(elementRef, 'disabled', disabled);
103
+ const isRequired = useBooleanProperty(elementRef, 'required', required);
104
+
101
105
  const webComponentProps: Record<string, any> = {
102
106
  ...props,
103
107
  ref: elementRef,
104
108
  };
105
109
 
106
- if (disabled) webComponentProps.disabled = '';
107
- if (required) webComponentProps.required = '';
110
+ if (isDisabled) webComponentProps.disabled = '';
111
+ if (isRequired) webComponentProps.required = '';
108
112
 
109
113
  if (value !== undefined) webComponentProps.value = value;
110
114
  if (name) webComponentProps.name = name;
@@ -1,4 +1,6 @@
1
1
  import React, { useEffect, useRef, useImperativeHandle } from 'react';
2
+ import { needsPropertyBridge } from '../utils/react-version';
3
+ import { useBooleanProperty, coerceBool } from '../utils/use-boolean-prop';
2
4
 
3
5
  // Type definitions for Ty ScrollContainer component
4
6
  export interface TyScrollContainerProps extends React.HTMLAttributes<HTMLElement> {
@@ -63,6 +65,19 @@ export const TyScrollContainer = React.forwardRef<TyScrollContainerRef, TyScroll
63
65
  }
64
66
  }), []);
65
67
 
68
+ // shadow defaults to true; only the explicit-false case matters at the
69
+ // attribute level. Bridge it imperatively so flipping back to true
70
+ // propagates on React 18.
71
+ useEffect(() => {
72
+ if (!needsPropertyBridge) return;
73
+ if (shadow === undefined) return;
74
+ const el = elementRef.current as any;
75
+ if (!el) return;
76
+ const next = coerceBool(shadow);
77
+ if (Boolean(el.shadow) !== next) el.shadow = next;
78
+ }, [shadow]);
79
+ const isHideScrollbar = useBooleanProperty(elementRef, 'hideScrollbar', hideScrollbar);
80
+
66
81
  // Convert React props to web component attributes
67
82
  const webComponentProps: Record<string, any> = {
68
83
  ...props,
@@ -73,8 +88,8 @@ export const TyScrollContainer = React.forwardRef<TyScrollContainerRef, TyScroll
73
88
  if (maxHeight) webComponentProps['max-height'] = maxHeight;
74
89
 
75
90
  // Add boolean attributes
76
- if (shadow === false) webComponentProps.shadow = 'false';
77
- if (hideScrollbar) webComponentProps['hide-scrollbar'] = true;
91
+ if (shadow !== undefined && !coerceBool(shadow)) webComponentProps.shadow = 'false';
92
+ if (isHideScrollbar) webComponentProps['hide-scrollbar'] = '';
78
93
 
79
94
  return React.createElement(
80
95
  'ty-scroll-container',
@@ -1,5 +1,5 @@
1
1
  import React, { useEffect, useRef } from 'react';
2
- import { needsPropertyBridge } from '../utils/react-version';
2
+ import { useBooleanProperty } from '../utils/use-boolean-prop';
3
3
 
4
4
  export interface TySwitchProps extends Omit<React.HTMLAttributes<HTMLElement>, 'onChange' | 'onInput'> {
5
5
  /** Checked (on) state */
@@ -88,27 +88,19 @@ export const TySwitch = React.forwardRef<HTMLElement, TySwitchProps>(
88
88
  }
89
89
  }, [ref]);
90
90
 
91
- // Imperatively sync `checked` to the underlying property. React 18 sets
92
- // boolean attributes as empty strings on first render but doesn't reliably
93
- // remove them when the prop flips back to false on a custom element.
94
- // React 19+ handles boolean prop-to-property bridging natively.
95
- useEffect(() => {
96
- if (!needsPropertyBridge) return;
97
- const element = elementRef.current as any;
98
- if (!element) return;
99
- if (Boolean(element.checked) !== Boolean(checked)) {
100
- element.checked = Boolean(checked);
101
- }
102
- }, [checked]);
91
+ // Imperative property sync for boolean props (see use-boolean-prop.ts).
92
+ const isChecked = useBooleanProperty(elementRef, 'checked', checked);
93
+ const isDisabled = useBooleanProperty(elementRef, 'disabled', disabled);
94
+ const isRequired = useBooleanProperty(elementRef, 'required', required);
103
95
 
104
96
  const webComponentProps: Record<string, any> = {
105
97
  ...props,
106
98
  ref: elementRef,
107
99
  };
108
100
 
109
- if (checked) webComponentProps.checked = '';
110
- if (disabled) webComponentProps.disabled = '';
111
- if (required) webComponentProps.required = '';
101
+ if (isChecked) webComponentProps.checked = '';
102
+ if (isDisabled) webComponentProps.disabled = '';
103
+ if (isRequired) webComponentProps.required = '';
112
104
 
113
105
  if (value) webComponentProps.value = value;
114
106
  if (name) webComponentProps.name = name;
@@ -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 Tab component
4
5
  export interface TyTabProps extends React.HTMLAttributes<HTMLElement> {
@@ -47,7 +48,8 @@ export const TyTab = React.forwardRef<HTMLElement, TyTabProps>(
47
48
  webComponentProps.id = id;
48
49
 
49
50
  // Add boolean attributes
50
- if (disabled) webComponentProps.disabled = '';
51
+ const isDisabled = useBooleanProperty(elementRef, 'disabled', disabled);
52
+ if (isDisabled) webComponentProps.disabled = '';
51
53
 
52
54
  // Add string attributes
53
55
  if (label) webComponentProps.label = label;
@@ -1,4 +1,5 @@
1
1
  import React, { useEffect, useRef, useCallback } from 'react';
2
+ import { useBooleanProperty } from '../utils/use-boolean-prop';
2
3
 
3
4
  // CSS custom properties that cascade into the shadow DOM for full color control
4
5
  export interface TyTagCSSProperties extends React.CSSProperties {
@@ -57,6 +58,12 @@ export const TyTag = React.forwardRef<HTMLElement, TyTagProps>(
57
58
  }
58
59
  }, [ref]);
59
60
 
61
+ const isNotPill = useBooleanProperty(elementRef, 'notPill', notPill);
62
+ const isClickable = useBooleanProperty(elementRef, 'clickable', clickable);
63
+ const isDismissible = useBooleanProperty(elementRef, 'dismissible', dismissible);
64
+ const isDisabled = useBooleanProperty(elementRef, 'disabled', disabled);
65
+ const isSelected = useBooleanProperty(elementRef, 'selected', selected);
66
+
60
67
  return React.createElement(
61
68
  'ty-tag',
62
69
  {
@@ -64,11 +71,11 @@ export const TyTag = React.forwardRef<HTMLElement, TyTagProps>(
64
71
  // click is dispatched as composed CustomEvent by the web component — React's
65
72
  // synthetic onClick already catches it, so we just pass it through as onClick
66
73
  ...(onClick && { onClick }),
67
- ...(notPill && { 'not-pill': "" }),
68
- ...(clickable && { clickable: "" }),
69
- ...(dismissible && { dismissible: "" }),
70
- ...(disabled && { disabled: "" }),
71
- ...(selected && { selected: "" }),
74
+ ...(isNotPill && { 'not-pill': "" }),
75
+ ...(isClickable && { clickable: "" }),
76
+ ...(isDismissible && { dismissible: "" }),
77
+ ...(isDisabled && { disabled: "" }),
78
+ ...(isSelected && { selected: "" }),
72
79
  ref: elementRef,
73
80
  },
74
81
  children
@@ -1,5 +1,6 @@
1
1
  import React, { useEffect, useRef, useCallback } from 'react';
2
2
  import { needsPropertyBridge } from '../utils/react-version';
3
+ import { useBooleanProperty } from '../utils/use-boolean-prop';
3
4
 
4
5
  // Event detail structure for ty-textarea events
5
6
  export interface TyTextareaEventDetail {
@@ -160,12 +161,15 @@ export const TyTextarea = React.forwardRef<HTMLElement, TyTextareaProps>(
160
161
  }
161
162
  }, [(props as any).value]);
162
163
 
164
+ const isDisabled = useBooleanProperty(elementRef, 'disabled', disabled);
165
+ const isRequired = useBooleanProperty(elementRef, 'required', required);
166
+
163
167
  return React.createElement(
164
168
  'ty-textarea',
165
169
  {
166
170
  ...props,
167
- ...(disabled && { disabled: "" }),
168
- ...(required && { required: "" }),
171
+ ...(isDisabled && { disabled: "" }),
172
+ ...(isRequired && { required: "" }),
169
173
  ...(minHeight && { 'min-height': minHeight }), // Convert camelCase to kebab-case
170
174
  ...(maxHeight && { 'max-height': maxHeight }), // Convert camelCase to kebab-case
171
175
  ref: elementRef,
@@ -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 Tooltip component
4
5
  export interface TyTooltipProps extends React.HTMLAttributes<HTMLElement> {
@@ -64,9 +65,8 @@ export const TyTooltip = React.forwardRef<HTMLElement, TyTooltipProps>(
64
65
  webComponentProps.delay = delay.toString();
65
66
  }
66
67
 
67
- if (disabled) {
68
- webComponentProps.disabled = ''; // Boolean attributes as empty string
69
- }
68
+ const isDisabled = useBooleanProperty(elementRef, 'disabled', disabled);
69
+ if (isDisabled) webComponentProps.disabled = '';
70
70
 
71
71
  if (flavor) {
72
72
  webComponentProps.flavor = flavor;
@@ -0,0 +1,62 @@
1
+ import { useEffect, RefObject } from 'react';
2
+ import { needsPropertyBridge } from './react-version';
3
+
4
+ /**
5
+ * Coerce a value to a boolean using the same rules as the core property
6
+ * manager (packages/core/src/utils/property-manager.ts:142-152). This matters
7
+ * because consumers sometimes pass the string "false" through untyped call
8
+ * sites (JSON config, query params, server-rendered props) — and the JS
9
+ * `Boolean("false")` is surprisingly `true`.
10
+ *
11
+ * undefined | null | false | "false" | "0" → false
12
+ * "" → true (HTML boolean-attribute convention)
13
+ * any other truthy → true
14
+ */
15
+ export function coerceBool(value: unknown): boolean {
16
+ if (value === undefined || value === null || value === false) return false;
17
+ if (typeof value === 'string') {
18
+ if (value === '') return true;
19
+ const norm = value.toLowerCase().trim();
20
+ if (norm === 'false' || norm === '0') return false;
21
+ return true;
22
+ }
23
+ return Boolean(value);
24
+ }
25
+
26
+ /**
27
+ * Imperatively keep a boolean property on the underlying custom element in
28
+ * sync with its React prop.
29
+ *
30
+ * Why this exists: React 18 sets boolean attributes as empty strings on the
31
+ * first render to a custom element but does not reliably *remove* them when
32
+ * the prop flips back to `false`. Without this bridge, `<TyModal open>` →
33
+ * `<TyModal open={false}>` leaves the `open` attribute on the element and
34
+ * the modal stays open. Same class of bug affects `disabled`, `required`,
35
+ * `clearable`, `loading`, `readonly`, `protected`, etc.
36
+ *
37
+ * React 19+ bridges this natively — the effect short-circuits via
38
+ * `needsPropertyBridge`, so this is dead code on modern React.
39
+ *
40
+ * Pass the *camelCase JS property name* on the element (e.g. `externalSearch`
41
+ * for the `external-search` attribute). The core base class handles the
42
+ * attribute-side sync once the property changes.
43
+ *
44
+ * Returns the coerced boolean so the caller can also drive its conditional
45
+ * JSX attribute emission with a value that correctly handles "false".
46
+ */
47
+ export function useBooleanProperty(
48
+ ref: RefObject<HTMLElement | null>,
49
+ propName: string,
50
+ value: unknown
51
+ ): boolean {
52
+ const coerced = coerceBool(value);
53
+ useEffect(() => {
54
+ if (!needsPropertyBridge) return;
55
+ const el = ref.current as any;
56
+ if (!el) return;
57
+ if (Boolean(el[propName]) !== coerced) {
58
+ el[propName] = coerced;
59
+ }
60
+ }, [coerced, propName, ref]);
61
+ return coerced;
62
+ }