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.
- package/README.md +8 -8
- package/dist/components/TyButton.d.ts +2 -0
- package/dist/components/TyButton.d.ts.map +1 -1
- package/dist/components/TyButton.js +13 -5
- package/dist/components/TyButton.js.map +1 -1
- package/dist/components/TyCalendar.d.ts.map +1 -1
- package/dist/components/TyCalendar.js +13 -7
- package/dist/components/TyCalendar.js.map +1 -1
- package/dist/components/TyCheckbox.d.ts.map +1 -1
- package/dist/components/TyCheckbox.js +11 -14
- package/dist/components/TyCheckbox.js.map +1 -1
- package/dist/components/TyCopy.d.ts.map +1 -1
- package/dist/components/TyCopy.js +7 -3
- package/dist/components/TyCopy.js.map +1 -1
- package/dist/components/TyDatePicker.d.ts.map +1 -1
- package/dist/components/TyDatePicker.js +16 -10
- package/dist/components/TyDatePicker.js.map +1 -1
- package/dist/components/TyDropdown.d.ts +16 -5
- package/dist/components/TyDropdown.d.ts.map +1 -1
- package/dist/components/TyDropdown.js +34 -19
- package/dist/components/TyDropdown.js.map +1 -1
- package/dist/components/TyFileUpload.d.ts +31 -0
- package/dist/components/TyFileUpload.d.ts.map +1 -0
- package/dist/components/TyFileUpload.js +56 -0
- package/dist/components/TyFileUpload.js.map +1 -0
- package/dist/components/TyIcon.d.ts.map +1 -1
- package/dist/components/TyIcon.js +7 -6
- package/dist/components/TyIcon.js.map +1 -1
- package/dist/components/TyInput.d.ts.map +1 -1
- package/dist/components/TyInput.js +15 -4
- package/dist/components/TyInput.js.map +1 -1
- package/dist/components/TyModal.d.ts.map +1 -1
- package/dist/components/TyModal.js +35 -10
- package/dist/components/TyModal.js.map +1 -1
- package/dist/components/TyMultiselect.d.ts +6 -0
- package/dist/components/TyMultiselect.d.ts.map +1 -1
- package/dist/components/TyMultiselect.js +22 -14
- package/dist/components/TyMultiselect.js.map +1 -1
- package/dist/components/TyOption.d.ts.map +1 -1
- package/dist/components/TyOption.js +7 -3
- package/dist/components/TyOption.js.map +1 -1
- package/dist/components/TyPopup.d.ts.map +1 -1
- package/dist/components/TyPopup.js +7 -6
- package/dist/components/TyPopup.js.map +1 -1
- package/dist/components/TyRadio.d.ts.map +1 -1
- package/dist/components/TyRadio.js +6 -2
- package/dist/components/TyRadio.js.map +1 -1
- package/dist/components/TyRadioGroup.d.ts.map +1 -1
- package/dist/components/TyRadioGroup.js +5 -2
- package/dist/components/TyRadioGroup.js.map +1 -1
- package/dist/components/TyScrollContainer.d.ts.map +1 -1
- package/dist/components/TyScrollContainer.js +22 -4
- package/dist/components/TyScrollContainer.js.map +1 -1
- package/dist/components/TySwitch.d.ts.map +1 -1
- package/dist/components/TySwitch.js +8 -3
- package/dist/components/TySwitch.js.map +1 -1
- package/dist/components/TyTab.d.ts.map +1 -1
- package/dist/components/TyTab.js +3 -1
- package/dist/components/TyTab.js.map +1 -1
- package/dist/components/TyTag.d.ts.map +1 -1
- package/dist/components/TyTag.js +11 -5
- package/dist/components/TyTag.js.map +1 -1
- package/dist/components/TyTextarea.d.ts.map +1 -1
- package/dist/components/TyTextarea.js +10 -2
- package/dist/components/TyTextarea.js.map +1 -1
- package/dist/components/TyTooltip.d.ts.map +1 -1
- package/dist/components/TyTooltip.js +4 -3
- package/dist/components/TyTooltip.js.map +1 -1
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +2 -0
- package/dist/components/index.js.map +1 -1
- package/dist/utils/react-version.d.ts +2 -0
- package/dist/utils/react-version.d.ts.map +1 -0
- package/dist/utils/react-version.js +8 -0
- package/dist/utils/react-version.js.map +1 -0
- package/dist/utils/use-boolean-prop.d.ts +36 -0
- package/dist/utils/use-boolean-prop.d.ts.map +1 -0
- package/dist/utils/use-boolean-prop.js +62 -0
- package/dist/utils/use-boolean-prop.js.map +1 -0
- package/package.json +1 -1
- package/src/components/TyButton.tsx +17 -5
- package/src/components/TyCalendar.tsx +10 -7
- package/src/components/TyCheckbox.tsx +11 -13
- package/src/components/TyCopy.tsx +9 -4
- package/src/components/TyDatePicker.tsx +12 -11
- package/src/components/TyDropdown.tsx +56 -34
- package/src/components/TyFileUpload.tsx +108 -0
- package/src/components/TyIcon.tsx +6 -7
- package/src/components/TyInput.tsx +14 -4
- package/src/components/TyModal.tsx +31 -13
- package/src/components/TyMultiselect.tsx +25 -17
- package/src/components/TyOption.tsx +8 -3
- package/src/components/TyPopup.tsx +5 -6
- package/src/components/TyRadio.tsx +7 -2
- package/src/components/TyRadioGroup.tsx +6 -2
- package/src/components/TyScrollContainer.tsx +17 -2
- package/src/components/TySwitch.tsx +9 -3
- package/src/components/TyTab.tsx +3 -1
- package/src/components/TyTag.tsx +12 -5
- package/src/components/TyTextarea.tsx +10 -2
- package/src/components/TyTooltip.tsx +3 -3
- package/src/components/index.ts +7 -0
- package/src/utils/react-version.ts +8 -0
- 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 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
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
96
|
+
const isManual = useBooleanProperty(elementRef, 'manual', manual);
|
|
97
|
+
const isDisableClose = useBooleanProperty(elementRef, 'disableClose', disableClose);
|
|
98
98
|
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
}
|
|
99
|
+
if (isManual) webComponentProps.manual = '';
|
|
100
|
+
if (isDisableClose) webComponentProps['disable-close'] = '';
|
|
102
101
|
|
|
103
102
|
return React.createElement(
|
|
104
103
|
'ty-popup',
|
|
@@ -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 TyRadioProps extends React.HTMLAttributes<HTMLElement> {
|
|
4
5
|
/** Form field value (selected by parent ty-radio-group when matches its `value`) */
|
|
@@ -37,13 +38,17 @@ export const TyRadio = React.forwardRef<HTMLElement, TyRadioProps>(
|
|
|
37
38
|
}
|
|
38
39
|
}, [ref]);
|
|
39
40
|
|
|
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);
|
|
44
|
+
|
|
40
45
|
const webComponentProps: Record<string, any> = {
|
|
41
46
|
...props,
|
|
42
47
|
ref: elementRef,
|
|
43
48
|
};
|
|
44
49
|
|
|
45
|
-
if (
|
|
46
|
-
if (
|
|
50
|
+
if (isChecked) webComponentProps.checked = '';
|
|
51
|
+
if (isDisabled) webComponentProps.disabled = '';
|
|
47
52
|
|
|
48
53
|
if (value !== undefined) webComponentProps.value = value;
|
|
49
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 (
|
|
107
|
-
if (
|
|
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
|
|
77
|
-
if (
|
|
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,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 TySwitchProps extends Omit<React.HTMLAttributes<HTMLElement>, 'onChange' | 'onInput'> {
|
|
4
5
|
/** Checked (on) state */
|
|
@@ -87,14 +88,19 @@ export const TySwitch = React.forwardRef<HTMLElement, TySwitchProps>(
|
|
|
87
88
|
}
|
|
88
89
|
}, [ref]);
|
|
89
90
|
|
|
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);
|
|
95
|
+
|
|
90
96
|
const webComponentProps: Record<string, any> = {
|
|
91
97
|
...props,
|
|
92
98
|
ref: elementRef,
|
|
93
99
|
};
|
|
94
100
|
|
|
95
|
-
if (
|
|
96
|
-
if (
|
|
97
|
-
if (
|
|
101
|
+
if (isChecked) webComponentProps.checked = '';
|
|
102
|
+
if (isDisabled) webComponentProps.disabled = '';
|
|
103
|
+
if (isRequired) webComponentProps.required = '';
|
|
98
104
|
|
|
99
105
|
if (value) webComponentProps.value = value;
|
|
100
106
|
if (name) webComponentProps.name = name;
|
package/src/components/TyTab.tsx
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/src/components/TyTag.tsx
CHANGED
|
@@ -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
|
-
...(
|
|
68
|
-
...(
|
|
69
|
-
...(
|
|
70
|
-
...(
|
|
71
|
-
...(
|
|
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,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-textarea events
|
|
4
6
|
export interface TyTextareaEventDetail {
|
|
@@ -147,7 +149,10 @@ export const TyTextarea = React.forwardRef<HTMLElement, TyTextareaProps>(
|
|
|
147
149
|
}, [ref]);
|
|
148
150
|
|
|
149
151
|
// Imperatively sync `value` to the underlying element's property.
|
|
152
|
+
// React 18 workaround: prop-to-property bridging is unreliable for empty
|
|
153
|
+
// strings on custom elements. React 19+ handles this natively.
|
|
150
154
|
useEffect(() => {
|
|
155
|
+
if (!needsPropertyBridge) return;
|
|
151
156
|
const element = elementRef.current as any;
|
|
152
157
|
if (!element) return;
|
|
153
158
|
const next = (props as any).value ?? '';
|
|
@@ -156,12 +161,15 @@ export const TyTextarea = React.forwardRef<HTMLElement, TyTextareaProps>(
|
|
|
156
161
|
}
|
|
157
162
|
}, [(props as any).value]);
|
|
158
163
|
|
|
164
|
+
const isDisabled = useBooleanProperty(elementRef, 'disabled', disabled);
|
|
165
|
+
const isRequired = useBooleanProperty(elementRef, 'required', required);
|
|
166
|
+
|
|
159
167
|
return React.createElement(
|
|
160
168
|
'ty-textarea',
|
|
161
169
|
{
|
|
162
170
|
...props,
|
|
163
|
-
...(
|
|
164
|
-
...(
|
|
171
|
+
...(isDisabled && { disabled: "" }),
|
|
172
|
+
...(isRequired && { required: "" }),
|
|
165
173
|
...(minHeight && { 'min-height': minHeight }), // Convert camelCase to kebab-case
|
|
166
174
|
...(maxHeight && { 'max-height': maxHeight }), // Convert camelCase to kebab-case
|
|
167
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
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
68
|
+
const isDisabled = useBooleanProperty(elementRef, 'disabled', disabled);
|
|
69
|
+
if (isDisabled) webComponentProps.disabled = '';
|
|
70
70
|
|
|
71
71
|
if (flavor) {
|
|
72
72
|
webComponentProps.flavor = flavor;
|
package/src/components/index.ts
CHANGED
|
@@ -65,6 +65,9 @@ export type { TyRadioGroupProps, TyRadioGroupEventDetail } from './TyRadioGroup'
|
|
|
65
65
|
export { TyCopy } from './TyCopy';
|
|
66
66
|
export type { TyCopyProps } from './TyCopy';
|
|
67
67
|
|
|
68
|
+
export { TyFileUpload } from './TyFileUpload';
|
|
69
|
+
export type { TyFileUploadProps, TyFileUploadEventDetail } from './TyFileUpload';
|
|
70
|
+
|
|
68
71
|
export { TyTabs } from './TyTabs';
|
|
69
72
|
export type { TyTabsProps, TabChangeDetail } from './TyTabs';
|
|
70
73
|
|
|
@@ -112,6 +115,7 @@ export { TySwitch as Switch } from './TySwitch';
|
|
|
112
115
|
export { TyRadio as Radio } from './TyRadio';
|
|
113
116
|
export { TyRadioGroup as RadioGroup } from './TyRadioGroup';
|
|
114
117
|
export { TyCopy as Copy } from './TyCopy';
|
|
118
|
+
export { TyFileUpload as FileUpload } from './TyFileUpload';
|
|
115
119
|
export { TyTabs as Tabs } from './TyTabs';
|
|
116
120
|
export { TyTab as Tab } from './TyTab';
|
|
117
121
|
export { TyCalendarMonth as CalendarMonth } from './TyCalendarMonth';
|
|
@@ -179,6 +183,9 @@ export type { TyRadioGroupProps as RadioGroupProps, TyRadioGroupEventDetail as R
|
|
|
179
183
|
// Copy types
|
|
180
184
|
export type { TyCopyProps as CopyProps } from './TyCopy';
|
|
181
185
|
|
|
186
|
+
// FileUpload types
|
|
187
|
+
export type { TyFileUploadProps as FileUploadProps, TyFileUploadEventDetail as FileUploadEventDetail } from './TyFileUpload';
|
|
188
|
+
|
|
182
189
|
// Tabs types
|
|
183
190
|
export type { TyTabsProps as TabsProps } from './TyTabs';
|
|
184
191
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
// React 19 natively bridges props to custom-element properties (including
|
|
4
|
+
// empty strings, `false` booleans, functions, and objects). On React 18 we
|
|
5
|
+
// must imperatively assign these properties via useEffect. Wrappers gate
|
|
6
|
+
// their bridging effects on this flag so the workaround is dead code on
|
|
7
|
+
// React 19+ and removable when React 18 support is eventually dropped.
|
|
8
|
+
export const needsPropertyBridge = parseInt(React.version, 10) < 19;
|
|
@@ -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
|
+
}
|