tyrell-react 1.0.0-RC6

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 (144) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +410 -0
  3. package/dist/components/TyButton.d.ts +50 -0
  4. package/dist/components/TyButton.d.ts.map +1 -0
  5. package/dist/components/TyButton.js +68 -0
  6. package/dist/components/TyButton.js.map +1 -0
  7. package/dist/components/TyCalendar.d.ts +63 -0
  8. package/dist/components/TyCalendar.d.ts.map +1 -0
  9. package/dist/components/TyCalendar.js +127 -0
  10. package/dist/components/TyCalendar.js.map +1 -0
  11. package/dist/components/TyCalendarMonth.d.ts +32 -0
  12. package/dist/components/TyCalendarMonth.d.ts.map +1 -0
  13. package/dist/components/TyCalendarMonth.js +54 -0
  14. package/dist/components/TyCalendarMonth.js.map +1 -0
  15. package/dist/components/TyCalendarNavigation.d.ts +21 -0
  16. package/dist/components/TyCalendarNavigation.d.ts.map +1 -0
  17. package/dist/components/TyCalendarNavigation.js +50 -0
  18. package/dist/components/TyCalendarNavigation.js.map +1 -0
  19. package/dist/components/TyCheckbox.d.ts +39 -0
  20. package/dist/components/TyCheckbox.d.ts.map +1 -0
  21. package/dist/components/TyCheckbox.js +83 -0
  22. package/dist/components/TyCheckbox.js.map +1 -0
  23. package/dist/components/TyCopy.d.ts +21 -0
  24. package/dist/components/TyCopy.d.ts.map +1 -0
  25. package/dist/components/TyCopy.js +42 -0
  26. package/dist/components/TyCopy.js.map +1 -0
  27. package/dist/components/TyDatePicker.d.ts +45 -0
  28. package/dist/components/TyDatePicker.d.ts.map +1 -0
  29. package/dist/components/TyDatePicker.js +119 -0
  30. package/dist/components/TyDatePicker.js.map +1 -0
  31. package/dist/components/TyDropdown.d.ts +56 -0
  32. package/dist/components/TyDropdown.d.ts.map +1 -0
  33. package/dist/components/TyDropdown.js +110 -0
  34. package/dist/components/TyDropdown.js.map +1 -0
  35. package/dist/components/TyIcon.d.ts +17 -0
  36. package/dist/components/TyIcon.d.ts.map +1 -0
  37. package/dist/components/TyIcon.js +41 -0
  38. package/dist/components/TyIcon.js.map +1 -0
  39. package/dist/components/TyInput.d.ts +65 -0
  40. package/dist/components/TyInput.d.ts.map +1 -0
  41. package/dist/components/TyInput.js +127 -0
  42. package/dist/components/TyInput.js.map +1 -0
  43. package/dist/components/TyModal.d.ts +29 -0
  44. package/dist/components/TyModal.d.ts.map +1 -0
  45. package/dist/components/TyModal.js +74 -0
  46. package/dist/components/TyModal.js.map +1 -0
  47. package/dist/components/TyMultiselect.d.ts +51 -0
  48. package/dist/components/TyMultiselect.d.ts.map +1 -0
  49. package/dist/components/TyMultiselect.js +107 -0
  50. package/dist/components/TyMultiselect.js.map +1 -0
  51. package/dist/components/TyOption.d.ts +10 -0
  52. package/dist/components/TyOption.d.ts.map +1 -0
  53. package/dist/components/TyOption.js +25 -0
  54. package/dist/components/TyOption.js.map +1 -0
  55. package/dist/components/TyPopup.d.ts +24 -0
  56. package/dist/components/TyPopup.d.ts.map +1 -0
  57. package/dist/components/TyPopup.js +61 -0
  58. package/dist/components/TyPopup.js.map +1 -0
  59. package/dist/components/TyRadio.d.ts +20 -0
  60. package/dist/components/TyRadio.d.ts.map +1 -0
  61. package/dist/components/TyRadio.js +46 -0
  62. package/dist/components/TyRadio.js.map +1 -0
  63. package/dist/components/TyRadioGroup.d.ts +40 -0
  64. package/dist/components/TyRadioGroup.d.ts.map +1 -0
  65. package/dist/components/TyRadioGroup.js +58 -0
  66. package/dist/components/TyRadioGroup.js.map +1 -0
  67. package/dist/components/TyResizeObserver.d.ts +11 -0
  68. package/dist/components/TyResizeObserver.d.ts.map +1 -0
  69. package/dist/components/TyResizeObserver.js +28 -0
  70. package/dist/components/TyResizeObserver.js.map +1 -0
  71. package/dist/components/TyScrollContainer.d.ts +25 -0
  72. package/dist/components/TyScrollContainer.d.ts.map +1 -0
  73. package/dist/components/TyScrollContainer.js +43 -0
  74. package/dist/components/TyScrollContainer.js.map +1 -0
  75. package/dist/components/TyStep.d.ts +17 -0
  76. package/dist/components/TyStep.d.ts.map +1 -0
  77. package/dist/components/TyStep.js +35 -0
  78. package/dist/components/TyStep.js.map +1 -0
  79. package/dist/components/TySwitch.d.ts +35 -0
  80. package/dist/components/TySwitch.d.ts.map +1 -0
  81. package/dist/components/TySwitch.js +69 -0
  82. package/dist/components/TySwitch.js.map +1 -0
  83. package/dist/components/TyTab.d.ts +13 -0
  84. package/dist/components/TyTab.d.ts.map +1 -0
  85. package/dist/components/TyTab.js +32 -0
  86. package/dist/components/TyTab.js.map +1 -0
  87. package/dist/components/TyTabs.d.ts +23 -0
  88. package/dist/components/TyTabs.d.ts.map +1 -0
  89. package/dist/components/TyTabs.js +48 -0
  90. package/dist/components/TyTabs.js.map +1 -0
  91. package/dist/components/TyTag.d.ts +22 -0
  92. package/dist/components/TyTag.d.ts.map +1 -0
  93. package/dist/components/TyTag.js +45 -0
  94. package/dist/components/TyTag.js.map +1 -0
  95. package/dist/components/TyTextarea.d.ts +37 -0
  96. package/dist/components/TyTextarea.d.ts.map +1 -0
  97. package/dist/components/TyTextarea.js +113 -0
  98. package/dist/components/TyTextarea.js.map +1 -0
  99. package/dist/components/TyTooltip.d.ts +17 -0
  100. package/dist/components/TyTooltip.d.ts.map +1 -0
  101. package/dist/components/TyTooltip.js +40 -0
  102. package/dist/components/TyTooltip.js.map +1 -0
  103. package/dist/components/TyWizard.d.ts +26 -0
  104. package/dist/components/TyWizard.d.ts.map +1 -0
  105. package/dist/components/TyWizard.js +50 -0
  106. package/dist/components/TyWizard.js.map +1 -0
  107. package/dist/components/index.d.ts +105 -0
  108. package/dist/components/index.d.ts.map +1 -0
  109. package/dist/components/index.js +112 -0
  110. package/dist/components/index.js.map +1 -0
  111. package/dist/utils/react-version.d.ts +2 -0
  112. package/dist/utils/react-version.d.ts.map +1 -0
  113. package/dist/utils/react-version.js +8 -0
  114. package/dist/utils/react-version.js.map +1 -0
  115. package/package.json +46 -0
  116. package/src/components/EventConventionTest.tsx +155 -0
  117. package/src/components/TyButton.tsx +145 -0
  118. package/src/components/TyCalendar.tsx +248 -0
  119. package/src/components/TyCalendarMonth.tsx +108 -0
  120. package/src/components/TyCalendarNavigation.tsx +91 -0
  121. package/src/components/TyCheckbox.tsx +152 -0
  122. package/src/components/TyCopy.tsx +78 -0
  123. package/src/components/TyDatePicker.tsx +220 -0
  124. package/src/components/TyDropdown.tsx +225 -0
  125. package/src/components/TyIcon.tsx +72 -0
  126. package/src/components/TyInput.tsx +232 -0
  127. package/src/components/TyModal.tsx +142 -0
  128. package/src/components/TyMultiselect.tsx +203 -0
  129. package/src/components/TyOption.tsx +42 -0
  130. package/src/components/TyPopup.tsx +111 -0
  131. package/src/components/TyRadio.tsx +70 -0
  132. package/src/components/TyRadioGroup.tsx +121 -0
  133. package/src/components/TyResizeObserver.tsx +54 -0
  134. package/src/components/TyScrollContainer.tsx +87 -0
  135. package/src/components/TyStep.tsx +71 -0
  136. package/src/components/TySwitch.tsx +122 -0
  137. package/src/components/TyTab.tsx +63 -0
  138. package/src/components/TyTabs.tsx +93 -0
  139. package/src/components/TyTag.tsx +79 -0
  140. package/src/components/TyTextarea.tsx +177 -0
  141. package/src/components/TyTooltip.tsx +83 -0
  142. package/src/components/TyWizard.tsx +99 -0
  143. package/src/components/index.ts +251 -0
  144. package/src/utils/react-version.ts +8 -0
@@ -0,0 +1,152 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { needsPropertyBridge } from '../utils/react-version';
3
+
4
+ // Type definitions for Ty Checkbox component
5
+ export interface TyCheckboxProps extends Omit<React.HTMLAttributes<HTMLElement>, 'onChange' | 'onInput'> {
6
+ /** Checked state */
7
+ checked?: boolean;
8
+
9
+ /** Form field value when checked */
10
+ value?: string;
11
+
12
+ /** Form field name */
13
+ name?: string;
14
+
15
+ /** Disable the checkbox */
16
+ disabled?: boolean;
17
+
18
+ /** Required field */
19
+ required?: boolean;
20
+
21
+ /** Error message */
22
+ error?: string;
23
+
24
+ /** Checkbox size */
25
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
26
+
27
+ /** Semantic styling variant */
28
+ flavor?: 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'neutral';
29
+
30
+ /**
31
+ * Fires when checkbox state changes (React convention)
32
+ * Maps to native 'input' event from ty-checkbox
33
+ */
34
+ onChange?: (event: CustomEvent<TyCheckboxEventDetail>) => void;
35
+
36
+ /**
37
+ * Fires on blur if value changed (native DOM behavior)
38
+ * Maps to native 'change' event from ty-checkbox
39
+ */
40
+ onChangeCommit?: (event: CustomEvent<TyCheckboxEventDetail>) => void;
41
+
42
+ /** Checkbox label content */
43
+ children?: React.ReactNode;
44
+ }
45
+
46
+ export interface TyCheckboxEventDetail {
47
+ value: boolean;
48
+ checked: boolean;
49
+ formValue: string | null;
50
+ originalEvent: Event;
51
+ }
52
+
53
+ // React wrapper for ty-checkbox web component
54
+ export const TyCheckbox = React.forwardRef<HTMLElement, TyCheckboxProps>(
55
+ ({
56
+ children,
57
+ checked,
58
+ value,
59
+ name,
60
+ disabled,
61
+ required,
62
+ error,
63
+ size,
64
+ flavor,
65
+ onChange,
66
+ onChangeCommit,
67
+ ...props
68
+ }, ref) => {
69
+ const elementRef = useRef<HTMLElement>(null);
70
+
71
+ // Handle change events
72
+ useEffect(() => {
73
+ const element = elementRef.current;
74
+ if (!element) return;
75
+
76
+ // Map onChange to input event (React convention)
77
+ const handleInput = (event: Event) => {
78
+ if (onChange) {
79
+ onChange(event as CustomEvent<TyCheckboxEventDetail>);
80
+ }
81
+ };
82
+
83
+ // Map onChangeCommit to change event (blur behavior)
84
+ const handleChangeCommit = (event: Event) => {
85
+ if (onChangeCommit) {
86
+ onChangeCommit(event as CustomEvent<TyCheckboxEventDetail>);
87
+ }
88
+ };
89
+
90
+ // Map onChange → input event (React convention)
91
+ element.addEventListener('input', handleInput);
92
+
93
+ // Map onChangeCommit → change event (blur behavior)
94
+ element.addEventListener('change', handleChangeCommit);
95
+
96
+ return () => {
97
+ element.removeEventListener('input', handleInput);
98
+ element.removeEventListener('change', handleChangeCommit);
99
+ };
100
+ }, [onChange, onChangeCommit]);
101
+
102
+ // Combine refs if needed
103
+ useEffect(() => {
104
+ if (ref && elementRef.current) {
105
+ if (typeof ref === 'function') {
106
+ ref(elementRef.current);
107
+ } else {
108
+ ref.current = elementRef.current;
109
+ }
110
+ }
111
+ }, [ref]);
112
+
113
+ // Imperatively sync `checked` to the underlying property. React 18 sets
114
+ // boolean attributes as empty strings on first render but doesn't reliably
115
+ // remove them when the prop flips back to false on a custom element.
116
+ // React 19+ handles boolean prop-to-property bridging natively.
117
+ useEffect(() => {
118
+ if (!needsPropertyBridge) return;
119
+ const element = elementRef.current as any;
120
+ if (!element) return;
121
+ if (Boolean(element.checked) !== Boolean(checked)) {
122
+ element.checked = Boolean(checked);
123
+ }
124
+ }, [checked]);
125
+
126
+ // Convert React props to web component attributes
127
+ const webComponentProps: Record<string, any> = {
128
+ ...props,
129
+ ref: elementRef,
130
+ };
131
+
132
+ // Add boolean attributes
133
+ if (checked) webComponentProps.checked = '';
134
+ if (disabled) webComponentProps.disabled = '';
135
+ if (required) webComponentProps.required = '';
136
+
137
+ // Add string attributes
138
+ if (value) webComponentProps.value = value;
139
+ if (name) webComponentProps.name = name;
140
+ if (error) webComponentProps.error = error;
141
+ if (size) webComponentProps.size = size;
142
+ if (flavor) webComponentProps.flavor = flavor;
143
+
144
+ return React.createElement(
145
+ 'ty-checkbox',
146
+ webComponentProps,
147
+ children
148
+ );
149
+ }
150
+ );
151
+
152
+ TyCheckbox.displayName = 'TyCheckbox';
@@ -0,0 +1,78 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+
3
+ // Type definitions for Ty Copy component
4
+ export interface TyCopyProps extends Omit<React.HTMLAttributes<HTMLElement>, 'onChange'> {
5
+ /** Text to copy */
6
+ value?: string;
7
+
8
+ /** Field label */
9
+ label?: string;
10
+
11
+ /** Component size */
12
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
13
+
14
+ /** Semantic styling variant */
15
+ flavor?: 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'neutral';
16
+
17
+ /** Display format */
18
+ format?: 'text' | 'code';
19
+
20
+ /** Multi-line display */
21
+ multiline?: boolean;
22
+
23
+ /** Disable the field */
24
+ disabled?: boolean;
25
+
26
+ /** Required field */
27
+ required?: boolean;
28
+ }
29
+
30
+ // React wrapper for ty-copy web component
31
+ export const TyCopy = React.forwardRef<HTMLElement, TyCopyProps>(
32
+ ({
33
+ value,
34
+ label,
35
+ size,
36
+ flavor,
37
+ format,
38
+ multiline,
39
+ disabled,
40
+ required,
41
+ ...props
42
+ }, ref) => {
43
+ const elementRef = useRef<HTMLElement>(null);
44
+
45
+ // Handle ref forwarding
46
+ useEffect(() => {
47
+ if (ref && elementRef.current) {
48
+ if (typeof ref === 'function') {
49
+ ref(elementRef.current);
50
+ } else {
51
+ ref.current = elementRef.current;
52
+ }
53
+ }
54
+ }, [ref]);
55
+
56
+ // Convert React props to web component attributes
57
+ const webComponentProps: Record<string, any> = {
58
+ ...props,
59
+ ref: elementRef,
60
+ };
61
+
62
+ // Add string attributes
63
+ if (value) webComponentProps.value = value;
64
+ if (label) webComponentProps.label = label;
65
+ if (size) webComponentProps.size = size;
66
+ if (flavor) webComponentProps.flavor = flavor;
67
+ if (format) webComponentProps.format = format;
68
+
69
+ // Add boolean attributes
70
+ if (multiline) webComponentProps.multiline = '';
71
+ if (disabled) webComponentProps.disabled = '';
72
+ if (required) webComponentProps.required = '';
73
+
74
+ return React.createElement('ty-copy', webComponentProps);
75
+ }
76
+ );
77
+
78
+ TyCopy.displayName = 'TyCopy';
@@ -0,0 +1,220 @@
1
+ import React, { useEffect, useRef, useCallback } from 'react';
2
+ import { needsPropertyBridge } from '../utils/react-version';
3
+
4
+ // Type definitions for Ty DatePicker component
5
+ export interface TyDatePickerEventDetail {
6
+ /** The selected date value (ISO string or formatted string based on format) */
7
+ value: string | null;
8
+ /** The selected date as milliseconds since epoch */
9
+ milliseconds: number | null;
10
+ /** Source of the change: "selection" | "time-change" | "clear" | "external" */
11
+ source: 'selection' | 'time-change' | 'clear' | 'external';
12
+ /** Formatted display value */
13
+ formatted: string | null;
14
+ }
15
+
16
+ export interface TyDatePickerProps extends Omit<React.HTMLAttributes<HTMLElement>, 'onChange'> {
17
+ /** The selected date value (ISO string or formatted string) */
18
+ value?: string;
19
+
20
+ /** Input size: "sm" | "md" | "lg" */
21
+ size?: 'sm' | 'md' | 'lg';
22
+
23
+ /** Visual flavor: "default" | "success" | "danger" | "warning" */
24
+ flavor?: 'default' | 'success' | 'danger' | 'warning';
25
+
26
+ /** Label text displayed above the input */
27
+ label?: string;
28
+
29
+ /** Placeholder text when no date is selected */
30
+ placeholder?: string;
31
+
32
+ /** Whether the field is required */
33
+ required?: boolean;
34
+
35
+ /** Whether the field is disabled */
36
+ disabled?: boolean;
37
+
38
+ /** Form field name for form submission */
39
+ name?: string;
40
+
41
+ /** Whether to show a clear button */
42
+ clearable?: boolean;
43
+
44
+ /** Date format: "short" | "medium" | "long" | "full" | custom format string */
45
+ format?: 'short' | 'medium' | 'long' | 'full' | string;
46
+
47
+ /** Locale for date formatting (e.g., "en-US", "de-DE") */
48
+ locale?: string;
49
+
50
+ /** Whether to include time selection */
51
+ withTime?: boolean;
52
+
53
+ /** Callback when the date value changes */
54
+ onChange?: (event: CustomEvent<TyDatePickerEventDetail>) => void;
55
+
56
+ /** Callback when the dropdown opens */
57
+ onOpen?: (event: CustomEvent<{}>) => void;
58
+
59
+ /** Callback when the dropdown closes */
60
+ onClose?: (event: CustomEvent<{}>) => void;
61
+ }
62
+
63
+ // React wrapper for ty-date-picker web component
64
+ export const TyDatePicker = React.forwardRef<HTMLElement, TyDatePickerProps>(
65
+ ({
66
+ value,
67
+ size,
68
+ flavor,
69
+ label,
70
+ placeholder,
71
+ required,
72
+ disabled,
73
+ name,
74
+ clearable,
75
+ format,
76
+ locale,
77
+ withTime,
78
+ onChange,
79
+ onOpen,
80
+ onClose,
81
+ ...props
82
+ }, ref) => {
83
+ const elementRef = useRef<HTMLElement>(null);
84
+
85
+ // Handle ref forwarding
86
+ useEffect(() => {
87
+ if (ref && elementRef.current) {
88
+ if (typeof ref === 'function') {
89
+ ref(elementRef.current);
90
+ } else {
91
+ ref.current = elementRef.current;
92
+ }
93
+ }
94
+ }, [ref]);
95
+
96
+ // Sync value property with the web component.
97
+ // React 18 workaround: prop-to-property bridging is unreliable for empty
98
+ // strings on custom elements. React 19+ handles this natively.
99
+ useEffect(() => {
100
+ if (!needsPropertyBridge) return;
101
+ const element = elementRef.current;
102
+ if (element && value !== undefined) {
103
+ // Set the value property directly on the element
104
+ (element as any).value = value || '';
105
+ }
106
+ }, [value]);
107
+
108
+ // Handle change events
109
+ const handleChange = useCallback((event: Event) => {
110
+ const customEvent = event as CustomEvent<TyDatePickerEventDetail>;
111
+ if (onChange) {
112
+ onChange(customEvent);
113
+ }
114
+ }, [onChange]);
115
+
116
+ // Handle open events
117
+ const handleOpen = useCallback((event: Event) => {
118
+ const customEvent = event as CustomEvent<{}>;
119
+ if (onOpen) {
120
+ onOpen(customEvent);
121
+ }
122
+ }, [onOpen]);
123
+
124
+ // Handle close events
125
+ const handleClose = useCallback((event: Event) => {
126
+ const customEvent = event as CustomEvent<{}>;
127
+ if (onClose) {
128
+ onClose(customEvent);
129
+ }
130
+ }, [onClose]);
131
+
132
+ // Set up event listeners
133
+ useEffect(() => {
134
+ const element = elementRef.current;
135
+ if (!element) return;
136
+
137
+ const listeners: Array<[string, EventListener]> = [];
138
+
139
+ if (onChange) {
140
+ element.addEventListener('change', handleChange);
141
+ listeners.push(['change', handleChange]);
142
+ }
143
+
144
+ if (onOpen) {
145
+ element.addEventListener('open', handleOpen);
146
+ listeners.push(['open', handleOpen]);
147
+ }
148
+
149
+ if (onClose) {
150
+ element.addEventListener('close', handleClose);
151
+ listeners.push(['close', handleClose]);
152
+ }
153
+
154
+ return () => {
155
+ listeners.forEach(([eventName, handler]) => {
156
+ element.removeEventListener(eventName, handler);
157
+ });
158
+ };
159
+ }, [handleChange, handleOpen, handleClose, onChange, onOpen, onClose]);
160
+
161
+ // Convert React props to web component attributes
162
+ const webComponentProps: Record<string, any> = {
163
+ ...props,
164
+ ref: elementRef,
165
+ };
166
+
167
+ // Add optional attributes only if they have values
168
+ if (value !== undefined) {
169
+ webComponentProps.value = value;
170
+ }
171
+
172
+ if (size) {
173
+ webComponentProps.size = size;
174
+ }
175
+
176
+ if (flavor) {
177
+ webComponentProps.flavor = flavor;
178
+ }
179
+
180
+ if (label) {
181
+ webComponentProps.label = label;
182
+ }
183
+
184
+ if (placeholder) {
185
+ webComponentProps.placeholder = placeholder;
186
+ }
187
+
188
+ if (required) {
189
+ webComponentProps.required = ''; // Boolean attributes as empty string
190
+ }
191
+
192
+ if (disabled) {
193
+ webComponentProps.disabled = ''; // Boolean attributes as empty string
194
+ }
195
+
196
+ if (name) {
197
+ webComponentProps.name = name;
198
+ }
199
+
200
+ if (clearable) {
201
+ webComponentProps.clearable = ''; // Boolean attributes as empty string
202
+ }
203
+
204
+ if (format) {
205
+ webComponentProps.format = format;
206
+ }
207
+
208
+ if (locale) {
209
+ webComponentProps.locale = locale;
210
+ }
211
+
212
+ if (withTime) {
213
+ webComponentProps['with-time'] = ''; // Convert camelCase to kebab-case
214
+ }
215
+
216
+ return React.createElement('ty-date-picker', webComponentProps);
217
+ }
218
+ );
219
+
220
+ TyDatePicker.displayName = 'TyDatePicker';
@@ -0,0 +1,225 @@
1
+ import React, { useEffect, useRef, useCallback } from 'react';
2
+ import { needsPropertyBridge } from '../utils/react-version';
3
+
4
+ // Option data structure for data-driven approach
5
+ export interface OptionData {
6
+ value: string;
7
+ text: string;
8
+ disabled?: boolean;
9
+ }
10
+
11
+ // Event detail structure for dropdown events
12
+ export interface TyDropdownEventDetail {
13
+ option: HTMLElement;
14
+ }
15
+
16
+ // Type definitions for Ty Dropdown component
17
+ export interface TyDropdownProps extends Omit<React.HTMLAttributes<HTMLElement>, 'onChange' | 'style'> {
18
+ style?: import('./TyInput').TyInputCSSProperties;
19
+ /** Semantic styling variant */
20
+ flavor?: 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'neutral';
21
+
22
+ /** Dropdown size */
23
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
24
+
25
+ /** Selected value */
26
+ value?: string;
27
+
28
+ /** Placeholder text */
29
+ placeholder?: string;
30
+
31
+ /** Dropdown label */
32
+ label?: string;
33
+
34
+ /** Disable the dropdown */
35
+ disabled?: boolean;
36
+
37
+ /** Make dropdown readonly */
38
+ readonly?: boolean;
39
+
40
+ /** Required field */
41
+ required?: boolean;
42
+
43
+ /** Show clear button */
44
+ clearable?: boolean;
45
+
46
+ /** Disable clear button (alias for clearable={false}) */
47
+ notClearable?: boolean;
48
+
49
+ /** Debounce in milliseconds (0-5000) */
50
+ debounce?: number;
51
+
52
+ /**
53
+ * Switch to external (remote) search mode. Default is `false` — the dropdown
54
+ * filters its options locally. When `true`, the dropdown stops filtering and
55
+ * dispatches `search` events on each keystroke; the parent owns filtering
56
+ * and updates the children.
57
+ */
58
+ externalSearch?: boolean;
59
+
60
+ /** @deprecated Use `externalSearch` instead. */
61
+ notSearchable?: boolean;
62
+
63
+ /** @deprecated Use `externalSearch` instead. Pass `searchable={false}` was equivalent to `externalSearch={true}`. */
64
+ searchable?: boolean;
65
+
66
+ /** Form field name for form submission */
67
+ name?: string;
68
+
69
+ // Data-driven approach
70
+ options?: OptionData[];
71
+
72
+ // React event handlers
73
+ onChange?: (event: CustomEvent<TyDropdownEventDetail>) => void;
74
+ onSearch?: (event: CustomEvent<{ query: string; element: HTMLElement }>) => void;
75
+
76
+ // Children for slotted approach
77
+ children?: React.ReactNode;
78
+ }
79
+
80
+ // React wrapper for ty-dropdown web component
81
+ export const TyDropdown = React.forwardRef<HTMLElement, TyDropdownProps>(
82
+ ({
83
+ options,
84
+ children,
85
+ onChange,
86
+ onSearch,
87
+ disabled,
88
+ externalSearch,
89
+ notSearchable,
90
+ searchable,
91
+ clearable,
92
+ notClearable,
93
+ debounce,
94
+ name,
95
+ value,
96
+ ...props
97
+ }, ref) => {
98
+ const elementRef = useRef<HTMLElement>(null);
99
+
100
+ const handleChange = useCallback((event: CustomEvent<TyDropdownEventDetail>) => {
101
+ if (onChange) {
102
+ onChange(event);
103
+ }
104
+ }, [onChange]);
105
+
106
+ const handleSearch = useCallback((event: CustomEvent<{ query: string; element: HTMLElement }>) => {
107
+ if (onSearch) {
108
+ onSearch(event);
109
+ }
110
+ }, [onSearch]);
111
+
112
+ useEffect(() => {
113
+ const element = elementRef.current;
114
+ if (!element) return;
115
+
116
+ // Listen for custom events from ty-dropdown
117
+ if (onChange) {
118
+ element.addEventListener('change', handleChange as EventListener);
119
+ }
120
+
121
+ if (onSearch) {
122
+ element.addEventListener('search', handleSearch as EventListener);
123
+ }
124
+
125
+ return () => {
126
+ if (onChange) {
127
+ element.removeEventListener('change', handleChange as EventListener);
128
+ }
129
+ if (onSearch) {
130
+ element.removeEventListener('search', handleSearch as EventListener);
131
+ }
132
+ };
133
+ }, [handleChange, handleSearch, onChange, onSearch]);
134
+
135
+ // Imperatively sync `value` to the underlying element's property whenever
136
+ // it changes. React 18's prop-to-property bridging for custom elements is
137
+ // unreliable for empty strings (programmatic resets), so we set the
138
+ // property directly to guarantee the dropdown clears on `value=""`.
139
+ // React 19+ handles this natively, so the effect short-circuits there.
140
+ useEffect(() => {
141
+ if (!needsPropertyBridge) return;
142
+ const element = elementRef.current as any;
143
+ if (!element) return;
144
+ if (element.value !== value) {
145
+ element.value = value ?? '';
146
+ }
147
+ }, [value]);
148
+
149
+ // Handle ref forwarding
150
+ useEffect(() => {
151
+ if (ref && elementRef.current) {
152
+ if (typeof ref === 'function') {
153
+ ref(elementRef.current);
154
+ } else {
155
+ ref.current = elementRef.current;
156
+ }
157
+ }
158
+ }, [ref]);
159
+
160
+ // Render options from data if provided, otherwise use children
161
+ const renderContent = () => {
162
+ if (options && options.length > 0) {
163
+ // Data-driven approach - create ty-option elements
164
+ return options.map((option, index) =>
165
+ React.createElement(
166
+ 'ty-option',
167
+ {
168
+ key: option.value || index,
169
+ value: option.value,
170
+ ...(option.disabled && { disabled: "" }),
171
+ },
172
+ option.text
173
+ )
174
+ );
175
+ }
176
+
177
+ // Slotted approach - use provided children
178
+ return children;
179
+ };
180
+
181
+ // Convert React props to web component attributes
182
+ const webComponentProps: Record<string, any> = {
183
+ ...props,
184
+ ref: elementRef,
185
+ };
186
+
187
+ // Add conditional attributes
188
+ if (disabled) webComponentProps.disabled = '';
189
+
190
+ // External search mode: parent owns filtering, dropdown dispatches search events.
191
+ // `notSearchable` and `searchable={false}` are deprecated aliases for `externalSearch`.
192
+ if (externalSearch || notSearchable || searchable === false) {
193
+ webComponentProps['external-search'] = '';
194
+ }
195
+
196
+ // Handle clearable functionality
197
+ if (clearable !== undefined) {
198
+ if (clearable) {
199
+ webComponentProps.clearable = '';
200
+ } else {
201
+ webComponentProps['not-clearable'] = '';
202
+ }
203
+ }
204
+
205
+ if (notClearable) {
206
+ webComponentProps['not-clearable'] = '';
207
+ }
208
+
209
+ // Add debounce attribute
210
+ if (debounce !== undefined) {
211
+ webComponentProps.debounce = debounce;
212
+ }
213
+
214
+ // Add string attributes
215
+ if (name) webComponentProps.name = name;
216
+
217
+ return React.createElement(
218
+ 'ty-dropdown',
219
+ webComponentProps,
220
+ renderContent()
221
+ );
222
+ }
223
+ );
224
+
225
+ TyDropdown.displayName = 'TyDropdown';