tyrell-react 1.0.0-TC24 → 1.0.0-TC26

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 (65) hide show
  1. package/dist/components/TyCalendar.d.ts +6 -0
  2. package/dist/components/TyCalendar.d.ts.map +1 -1
  3. package/dist/components/TyCalendar.js +16 -2
  4. package/dist/components/TyCalendar.js.map +1 -1
  5. package/dist/components/TyCalendarMonth.d.ts +4 -0
  6. package/dist/components/TyCalendarMonth.d.ts.map +1 -1
  7. package/dist/components/TyCalendarMonth.js +5 -1
  8. package/dist/components/TyCalendarMonth.js.map +1 -1
  9. package/dist/components/TyCalendarNavigation.d.ts +4 -0
  10. package/dist/components/TyCalendarNavigation.d.ts.map +1 -1
  11. package/dist/components/TyCalendarNavigation.js +5 -1
  12. package/dist/components/TyCalendarNavigation.js.map +1 -1
  13. package/dist/components/TyCheckbox.d.ts +2 -0
  14. package/dist/components/TyCheckbox.d.ts.map +1 -1
  15. package/dist/components/TyCheckbox.js +4 -1
  16. package/dist/components/TyCheckbox.js.map +1 -1
  17. package/dist/components/TyCopy.d.ts +4 -0
  18. package/dist/components/TyCopy.d.ts.map +1 -1
  19. package/dist/components/TyCopy.js +19 -1
  20. package/dist/components/TyCopy.js.map +1 -1
  21. package/dist/components/TyDatePicker.d.ts +4 -0
  22. package/dist/components/TyDatePicker.d.ts.map +1 -1
  23. package/dist/components/TyDatePicker.js +7 -1
  24. package/dist/components/TyDatePicker.js.map +1 -1
  25. package/dist/components/TyDropdown.d.ts +2 -0
  26. package/dist/components/TyDropdown.d.ts.map +1 -1
  27. package/dist/components/TyDropdown.js +14 -2
  28. package/dist/components/TyDropdown.js.map +1 -1
  29. package/dist/components/TyMultiselect.d.ts +2 -0
  30. package/dist/components/TyMultiselect.d.ts.map +1 -1
  31. package/dist/components/TyMultiselect.js +14 -2
  32. package/dist/components/TyMultiselect.js.map +1 -1
  33. package/dist/components/TyOption.d.ts +8 -0
  34. package/dist/components/TyOption.d.ts.map +1 -1
  35. package/dist/components/TyOption.js +3 -1
  36. package/dist/components/TyOption.js.map +1 -1
  37. package/dist/components/TyScrollContainer.d.ts +26 -1
  38. package/dist/components/TyScrollContainer.d.ts.map +1 -1
  39. package/dist/components/TyScrollContainer.js +41 -24
  40. package/dist/components/TyScrollContainer.js.map +1 -1
  41. package/dist/components/TySelect.d.ts +75 -0
  42. package/dist/components/TySelect.d.ts.map +1 -0
  43. package/dist/components/TySelect.js +134 -0
  44. package/dist/components/TySelect.js.map +1 -0
  45. package/dist/components/index.d.ts +6 -0
  46. package/dist/components/index.d.ts.map +1 -1
  47. package/dist/components/index.js +5 -0
  48. package/dist/components/index.js.map +1 -1
  49. package/dist/version.d.ts +1 -1
  50. package/dist/version.js +1 -1
  51. package/package.json +1 -1
  52. package/src/components/TyCalendar.tsx +30 -1
  53. package/src/components/TyCalendarMonth.tsx +11 -1
  54. package/src/components/TyCalendarNavigation.tsx +11 -1
  55. package/src/components/TyCheckbox.tsx +8 -2
  56. package/src/components/TyCopy.tsx +25 -1
  57. package/src/components/TyDatePicker.tsx +16 -0
  58. package/src/components/TyDropdown.tsx +14 -2
  59. package/src/components/TyMultiselect.tsx +12 -1
  60. package/src/components/TyOption.tsx +14 -1
  61. package/src/components/TyScrollContainer.tsx +74 -24
  62. package/src/components/TySelect.tsx +240 -0
  63. package/src/components/index.ts +9 -0
  64. package/src/version.ts +1 -1
  65. package/src/components/EventConventionTest.tsx +0 -155
@@ -0,0 +1,240 @@
1
+ import React, { useEffect, useRef, useCallback } from 'react';
2
+ import { needsPropertyBridge } from '../utils/react-version';
3
+ import { useBooleanProperty } from '../utils/use-boolean-prop';
4
+
5
+ // Type definitions for Ty Select component
6
+ export interface TySelectItem {
7
+ value: string;
8
+ label: string;
9
+ flavor: string | null;
10
+ }
11
+
12
+ export interface TySelectEventDetail {
13
+ /** Scalar for single select, array when `multiple` */
14
+ value: string | string[] | null;
15
+ /** Always the array form of the selection */
16
+ values: string[];
17
+ /** Rich info per selected value — enough to render chips out-of-band */
18
+ items: TySelectItem[];
19
+ /** Action that triggered the change */
20
+ action: 'add' | 'remove' | 'clear' | 'set';
21
+ /** The specific value that changed */
22
+ item: string | null;
23
+ }
24
+
25
+ export interface TySelectProps extends Omit<React.HTMLAttributes<HTMLElement>, 'onChange' | 'style'> {
26
+ style?: import('./TyInput').TyInputCSSProperties;
27
+
28
+ /** Selected value(s) — string, comma-separated string, or array */
29
+ value?: string | string[];
30
+
31
+ /** Multi select (native <select multiple> semantics). Default: single. */
32
+ multiple?: boolean;
33
+
34
+ /** Compact content-hugging trigger (toolbars, filter bars) instead of the full-width field look */
35
+ compact?: boolean;
36
+
37
+ /** Placeholder shown while nothing is selected */
38
+ placeholder?: string;
39
+
40
+ /** Label text above the field */
41
+ label?: string;
42
+
43
+ /** Form field name — single submits one entry, multiple submits repeated entries */
44
+ name?: string;
45
+
46
+ /** Disable the select */
47
+ disabled?: boolean;
48
+
49
+ /** Read-only */
50
+ readonly?: boolean;
51
+
52
+ /** Mark the field as required */
53
+ required?: boolean;
54
+
55
+ /**
56
+ * Search row visibility: 'auto' (default) shows it only for 8+ options,
57
+ * 'always' / 'never' force it. external-search always shows it.
58
+ */
59
+ searchable?: 'auto' | 'always' | 'never' | boolean;
60
+
61
+ /**
62
+ * External (remote) search mode — the component stops filtering and emits
63
+ * debounced `search` events; replace the option children in response.
64
+ */
65
+ externalSearch?: boolean;
66
+
67
+ /** Debounce for the search event in ms (0-5000) */
68
+ debounce?: number;
69
+
70
+ /** Loading state — shows a spinner in the options area (external search in flight) */
71
+ loading?: boolean;
72
+
73
+ /** Size variant */
74
+ size?: 'sm' | 'md' | 'lg';
75
+
76
+ /** Callback when selection changes */
77
+ onChange?: (event: CustomEvent<TySelectEventDetail>) => void;
78
+
79
+ /** Callback fired on each search input change (debounced). Use for external/server-side filtering. */
80
+ onSearch?: (event: CustomEvent<{ query: string; element: HTMLElement }>) => void;
81
+ onOpen?: (event: CustomEvent) => void;
82
+ onClose?: (event: CustomEvent) => void;
83
+
84
+ /** TyOption children (plus optional slot="trigger" / slot="start" / slot="end" elements) */
85
+ children?: React.ReactNode;
86
+ }
87
+
88
+ // React wrapper for ty-select web component
89
+ export const TySelect = React.forwardRef<HTMLElement, TySelectProps>(
90
+ ({
91
+ value,
92
+ multiple,
93
+ compact,
94
+ placeholder,
95
+ label,
96
+ name,
97
+ disabled,
98
+ readonly,
99
+ required,
100
+ searchable,
101
+ externalSearch,
102
+ debounce,
103
+ loading,
104
+ size,
105
+ onChange,
106
+ onSearch,
107
+ onOpen,
108
+ onClose,
109
+ children,
110
+ ...props
111
+ }, ref) => {
112
+ const elementRef = useRef<HTMLElement>(null);
113
+
114
+ // Handle ref forwarding
115
+ useEffect(() => {
116
+ if (ref && elementRef.current) {
117
+ if (typeof ref === 'function') {
118
+ ref(elementRef.current);
119
+ } else {
120
+ ref.current = elementRef.current;
121
+ }
122
+ }
123
+ }, [ref]);
124
+
125
+ // Imperatively sync `value` so resets ('' or null) reliably clear the
126
+ // visible selection. React 18 workaround; React 19+ handles this natively.
127
+ useEffect(() => {
128
+ if (!needsPropertyBridge) return;
129
+ const element = elementRef.current as any;
130
+ if (!element) return;
131
+ const next = Array.isArray(value) ? value.join(',') : (value ?? '');
132
+ if (element.value !== next) {
133
+ element.value = next;
134
+ }
135
+ }, [value]);
136
+
137
+ // Handle events
138
+ const handleChange = useCallback((event: Event) => {
139
+ if (onChange) onChange(event as CustomEvent<TySelectEventDetail>);
140
+ }, [onChange]);
141
+
142
+ const handleSearch = useCallback((event: Event) => {
143
+ if (onSearch) onSearch(event as CustomEvent<{ query: string; element: HTMLElement }>);
144
+ }, [onSearch]);
145
+
146
+ const handleOpen = useCallback((event: Event) => { if (onOpen) onOpen(event as CustomEvent); }, [onOpen]);
147
+ const handleClose = useCallback((event: Event) => { if (onClose) onClose(event as CustomEvent); }, [onClose]);
148
+
149
+ // Set up event listeners
150
+ useEffect(() => {
151
+ const element = elementRef.current;
152
+ if (!element) return;
153
+
154
+ if (onChange) element.addEventListener('change', handleChange);
155
+ if (onSearch) element.addEventListener('search', handleSearch);
156
+ if (onOpen) element.addEventListener('open', handleOpen);
157
+ if (onClose) element.addEventListener('close', handleClose);
158
+
159
+ return () => {
160
+ if (onChange) element.removeEventListener('change', handleChange);
161
+ if (onSearch) element.removeEventListener('search', handleSearch);
162
+ if (onOpen) element.removeEventListener('open', handleOpen);
163
+ if (onClose) element.removeEventListener('close', handleClose);
164
+ };
165
+ }, [handleChange, handleSearch, handleOpen, handleClose, onChange, onSearch, onOpen, onClose]);
166
+
167
+ // Imperative property sync for boolean props (see use-boolean-prop.ts).
168
+ const isMultiple = useBooleanProperty(elementRef, 'multiple', multiple);
169
+ const isCompact = useBooleanProperty(elementRef, 'compact', compact);
170
+ const isDisabled = useBooleanProperty(elementRef, 'disabled', disabled);
171
+ const isReadonly = useBooleanProperty(elementRef, 'readonly', readonly);
172
+ const isRequired = useBooleanProperty(elementRef, 'required', required);
173
+ const isLoading = useBooleanProperty(elementRef, 'loading', loading);
174
+ const isExternalSearch = useBooleanProperty(elementRef, 'externalSearch', externalSearch);
175
+
176
+ // Convert React props to web component attributes
177
+ const webComponentProps: Record<string, any> = {
178
+ ...props,
179
+ ref: elementRef,
180
+ };
181
+
182
+ // Handle value conversion (array to comma-separated string)
183
+ if (value !== undefined) {
184
+ webComponentProps.value = Array.isArray(value) ? value.join(',') : value;
185
+ }
186
+
187
+ if (isMultiple) webComponentProps.multiple = '';
188
+ if (isCompact) webComponentProps.compact = '';
189
+ if (isDisabled) webComponentProps.disabled = '';
190
+ if (isReadonly) webComponentProps.readonly = '';
191
+ if (isRequired) webComponentProps.required = '';
192
+ if (isLoading) webComponentProps.loading = '';
193
+ if (isExternalSearch) webComponentProps['external-search'] = '';
194
+
195
+ if (placeholder) webComponentProps.placeholder = placeholder;
196
+ if (label) webComponentProps.label = label;
197
+ if (name) webComponentProps.name = name;
198
+ if (size) webComponentProps.size = size;
199
+ if (debounce !== undefined) webComponentProps.debounce = debounce.toString();
200
+ if (searchable === 'always' || searchable === true) webComponentProps.searchable = 'true';
201
+ else if (searchable === 'never' || searchable === false) webComponentProps.searchable = 'false';
202
+ // 'auto' / undefined → omit (component default)
203
+
204
+ return React.createElement('ty-select', webComponentProps, children);
205
+ }
206
+ );
207
+
208
+ TySelect.displayName = 'TySelect';
209
+
210
+ // ============================================================================
211
+ // TySelectedTags — out-of-band chip display for a TySelect
212
+ // ============================================================================
213
+
214
+ export interface TySelectedTagsProps extends React.HTMLAttributes<HTMLElement> {
215
+ /** id of the ty-select to display. Falls back to the previous element sibling. */
216
+ htmlFor?: string;
217
+ /** Optional <template> child as chip blueprint ({value}/{label}/{flavor}/{data-*} placeholders) */
218
+ children?: React.ReactNode;
219
+ }
220
+
221
+ // React wrapper for ty-selected-tags web component
222
+ export const TySelectedTags = React.forwardRef<HTMLElement, TySelectedTagsProps>(
223
+ ({ htmlFor, children, ...props }, ref) => {
224
+ const elementRef = useRef<HTMLElement>(null);
225
+
226
+ useEffect(() => {
227
+ if (ref && elementRef.current) {
228
+ if (typeof ref === 'function') ref(elementRef.current);
229
+ else ref.current = elementRef.current;
230
+ }
231
+ }, [ref]);
232
+
233
+ const webComponentProps: Record<string, any> = { ...props, ref: elementRef };
234
+ if (htmlFor) webComponentProps.for = htmlFor;
235
+
236
+ return React.createElement('ty-selected-tags', webComponentProps, children);
237
+ }
238
+ );
239
+
240
+ TySelectedTags.displayName = 'TySelectedTags';
@@ -55,6 +55,9 @@ export type { TyIconProps } from './TyIcon';
55
55
 
56
56
  export { TyModal } from './TyModal';
57
57
  export type { TyModalProps, TyModalEventDetail, TyModalRef } from './TyModal';
58
+ // Platform/ARIA name alias (the element is also registered as ty-dialog)
59
+ export { TyModal as TyDialog } from './TyModal';
60
+ export type { TyModalProps as TyDialogProps, TyModalEventDetail as TyDialogEventDetail, TyModalRef as TyDialogRef } from './TyModal';
58
61
 
59
62
  export { TyTooltip } from './TyTooltip';
60
63
  export type { TyTooltipProps } from './TyTooltip';
@@ -62,6 +65,9 @@ export type { TyTooltipProps } from './TyTooltip';
62
65
  export { TyMultiselect } from './TyMultiselect';
63
66
  export type { TyMultiselectProps, TyMultiselectEventDetail } from './TyMultiselect';
64
67
 
68
+ export { TySelect, TySelectedTags } from './TySelect';
69
+ export type { TySelectProps, TySelectEventDetail, TySelectItem, TySelectedTagsProps } from './TySelect';
70
+
65
71
  export { TyCalendar } from './TyCalendar';
66
72
  export type { TyCalendarProps, TyCalendarChangeEventDetail, TyCalendarNavigateEventDetail } from './TyCalendar';
67
73
 
@@ -85,6 +91,9 @@ export type { TyRadioGroupProps, TyRadioGroupEventDetail } from './TyRadioGroup'
85
91
 
86
92
  export { TyCopy } from './TyCopy';
87
93
  export type { TyCopyProps } from './TyCopy';
94
+ // Descriptive name alias (the element is also registered as ty-copy-field)
95
+ export { TyCopy as TyCopyField } from './TyCopy';
96
+ export type { TyCopyProps as TyCopyFieldProps } from './TyCopy';
88
97
 
89
98
  export { TyFileUpload } from './TyFileUpload';
90
99
  export type { TyFileUploadProps, TyFileUploadEventDetail } from './TyFileUpload';
package/src/version.ts CHANGED
@@ -3,4 +3,4 @@
3
3
  // Run 'npm run generate:version' to regenerate.
4
4
 
5
5
  /** Current version of tyrell-react. Synced with package.json on build. */
6
- export const VERSION = '1.0.0-TC24'
6
+ export const VERSION = '1.0.0-TC26'
@@ -1,155 +0,0 @@
1
- /**
2
- * Test Example: React Event Convention
3
- *
4
- * This file demonstrates the new event handling convention
5
- * for tyrell-react components.
6
- *
7
- * To test:
8
- * 1. Create a new React project or use existing
9
- * 2. npm install tyrell-react
10
- * 3. Add this component to your app
11
- * 4. Observe console logs and state updates
12
- */
13
-
14
- import React, { useState } from 'react';
15
- import { TyInput } from './TyInput';
16
- import { TyTextarea } from './TyTextarea';
17
- import { TyCheckbox } from './TyCheckbox';
18
- import type { TyInputEventDetail } from './TyInput';
19
- import type { TyTextareaEventDetail } from './TyTextarea';
20
- import type { TyCheckboxEventDetail } from './TyCheckbox';
21
-
22
- export function EventConventionTest() {
23
- const [inputValue, setInputValue] = useState('');
24
- const [textareaValue, setTextareaValue] = useState('');
25
- const [checked, setChecked] = useState(false);
26
- const [logs, setLogs] = useState<string[]>([]);
27
-
28
- const addLog = (message: string) => {
29
- setLogs(prev => [...prev, `${new Date().toLocaleTimeString()}: ${message}`]);
30
- };
31
-
32
- return (
33
- <div className="p-8 space-y-6 max-w-2xl">
34
- <h1 className="text-2xl font-bold">React Event Convention Test</h1>
35
-
36
- {/* Input Test */}
37
- <div className="space-y-2">
38
- <h2 className="text-lg font-semibold">TyInput Test</h2>
39
- <TyInput
40
- label="Email"
41
- placeholder="Type to test onChange..."
42
- value={inputValue}
43
- onChange={(e: CustomEvent<TyInputEventDetail>) => {
44
- setInputValue(e.detail.value);
45
- addLog(`onChange: "${e.detail.value}" (fires on keystroke)`);
46
- }}
47
- onChangeCommit={(e: CustomEvent<TyInputEventDetail>) => {
48
- addLog(`onChangeCommit: "${e.detail.value}" (fires on blur)`);
49
- }}
50
- onFocus={() => addLog('onFocus')}
51
- onBlur={() => addLog('onBlur')}
52
- />
53
- <p className="text-sm text-gray-600">
54
- Current value: <strong>{inputValue}</strong>
55
- </p>
56
- </div>
57
-
58
- {/* Textarea Test */}
59
- <div className="space-y-2">
60
- <h2 className="text-lg font-semibold">TyTextarea Test</h2>
61
- <TyTextarea
62
- label="Comments"
63
- placeholder="Type to test onChange..."
64
- value={textareaValue}
65
- rows={3}
66
- onChange={(e: CustomEvent<TyTextareaEventDetail>) => {
67
- setTextareaValue(e.detail.value);
68
- addLog(`Textarea onChange: "${e.detail.value}"`);
69
- }}
70
- onChangeCommit={(e: CustomEvent<TyTextareaEventDetail>) => {
71
- addLog(`Textarea onChangeCommit: "${e.detail.value}"`);
72
- }}
73
- />
74
- <p className="text-sm text-gray-600">
75
- Current value: <strong>{textareaValue}</strong>
76
- </p>
77
- </div>
78
-
79
- {/* Checkbox Test */}
80
- <div className="space-y-2">
81
- <h2 className="text-lg font-semibold">TyCheckbox Test</h2>
82
- <TyCheckbox
83
- checked={checked}
84
- onChange={(e: CustomEvent<TyCheckboxEventDetail>) => {
85
- setChecked(e.detail.checked);
86
- addLog(`Checkbox onChange: ${e.detail.checked} (fires immediately)`);
87
- }}
88
- onChangeCommit={(e: CustomEvent<TyCheckboxEventDetail>) => {
89
- addLog(`Checkbox onChangeCommit: ${e.detail.checked} (fires on blur)`);
90
- }}
91
- >
92
- Subscribe to newsletter
93
- </TyCheckbox>
94
- <p className="text-sm text-gray-600">
95
- Current state: <strong>{checked ? 'Checked' : 'Unchecked'}</strong>
96
- </p>
97
- </div>
98
-
99
- {/* Event Log */}
100
- <div className="space-y-2">
101
- <h2 className="text-lg font-semibold">Event Log</h2>
102
- <div className="bg-gray-100 p-4 rounded max-h-64 overflow-y-auto font-mono text-xs">
103
- {logs.length === 0 ? (
104
- <p className="text-gray-500">No events yet. Start typing or checking boxes!</p>
105
- ) : (
106
- logs.map((log, i) => (
107
- <div key={i} className="text-gray-800">
108
- {log}
109
- </div>
110
- ))
111
- )}
112
- </div>
113
- <button
114
- onClick={() => setLogs([])}
115
- className="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
116
- >
117
- Clear Log
118
- </button>
119
- </div>
120
-
121
- {/* Expected Behavior */}
122
- <div className="bg-blue-50 p-4 rounded">
123
- <h3 className="font-semibold mb-2">Expected Behavior:</h3>
124
- <ul className="list-disc list-inside space-y-1 text-sm">
125
- <li>
126
- <strong>onChange</strong> - Fires on every keystroke/state change (React convention)
127
- </li>
128
- <li>
129
- <strong>onChangeCommit</strong> - Fires on blur if value changed (optional)
130
- </li>
131
- <li>
132
- <strong>onFocus</strong> - Fires when element gains focus
133
- </li>
134
- <li>
135
- <strong>onBlur</strong> - Fires when element loses focus
136
- </li>
137
- </ul>
138
- </div>
139
-
140
- {/* Verification */}
141
- <div className="bg-green-50 p-4 rounded">
142
- <h3 className="font-semibold mb-2">Verification Checklist:</h3>
143
- <ul className="list-disc list-inside space-y-1 text-sm">
144
- <li>✅ onChange fires on EVERY keystroke (not just on blur)</li>
145
- <li>✅ State updates in real-time as you type</li>
146
- <li>✅ onChangeCommit fires ONLY on blur (if value changed)</li>
147
- <li>✅ Event order: onChange → onBlur → onChangeCommit</li>
148
- <li>✅ Checkbox onChange fires immediately on click</li>
149
- </ul>
150
- </div>
151
- </div>
152
- );
153
- }
154
-
155
- export default EventConventionTest;