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 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,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 (checked) webComponentProps.checked = '';
46
- if (disabled) webComponentProps.disabled = '';
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 (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,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 (checked) webComponentProps.checked = '';
96
- if (disabled) webComponentProps.disabled = '';
97
- if (required) webComponentProps.required = '';
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;
@@ -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,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
- ...(disabled && { disabled: "" }),
164
- ...(required && { required: "" }),
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
- 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;
@@ -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
+ }