tyrell-react 1.0.0-RC12 → 1.0.0-RC14

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 (135) hide show
  1. package/README.md +1 -0
  2. package/dist/components/TyButton.d.ts +1 -1
  3. package/dist/components/TyButton.d.ts.map +1 -1
  4. package/dist/components/TyButton.js +2 -1
  5. package/dist/components/TyButton.js.map +1 -1
  6. package/dist/components/TyCalendar.d.ts +5 -0
  7. package/dist/components/TyCalendar.d.ts.map +1 -1
  8. package/dist/components/TyCalendar.js +6 -10
  9. package/dist/components/TyCalendar.js.map +1 -1
  10. package/dist/components/TyCalendarMonth.d.ts +5 -0
  11. package/dist/components/TyCalendarMonth.d.ts.map +1 -1
  12. package/dist/components/TyCalendarMonth.js +5 -7
  13. package/dist/components/TyCalendarMonth.js.map +1 -1
  14. package/dist/components/TyCalendarNavigation.d.ts.map +1 -1
  15. package/dist/components/TyCalendarNavigation.js +2 -6
  16. package/dist/components/TyCalendarNavigation.js.map +1 -1
  17. package/dist/components/TyCheckbox.d.ts +4 -1
  18. package/dist/components/TyCheckbox.d.ts.map +1 -1
  19. package/dist/components/TyCheckbox.js +2 -9
  20. package/dist/components/TyCheckbox.js.map +1 -1
  21. package/dist/components/TyCopy.d.ts +4 -1
  22. package/dist/components/TyCopy.d.ts.map +1 -1
  23. package/dist/components/TyCopy.js +2 -7
  24. package/dist/components/TyCopy.js.map +1 -1
  25. package/dist/components/TyDatePicker.d.ts +14 -4
  26. package/dist/components/TyDatePicker.d.ts.map +1 -1
  27. package/dist/components/TyDatePicker.js +8 -13
  28. package/dist/components/TyDatePicker.js.map +1 -1
  29. package/dist/components/TyFileUpload.d.ts.map +1 -1
  30. package/dist/components/TyFileUpload.js +2 -2
  31. package/dist/components/TyFileUpload.js.map +1 -1
  32. package/dist/components/TyIcon.d.ts.map +1 -1
  33. package/dist/components/TyIcon.js +0 -4
  34. package/dist/components/TyIcon.js.map +1 -1
  35. package/dist/components/TyInput.d.ts +6 -2
  36. package/dist/components/TyInput.d.ts.map +1 -1
  37. package/dist/components/TyInput.js +2 -11
  38. package/dist/components/TyInput.js.map +1 -1
  39. package/dist/components/TyModal.d.ts +21 -2
  40. package/dist/components/TyModal.d.ts.map +1 -1
  41. package/dist/components/TyModal.js +21 -18
  42. package/dist/components/TyModal.js.map +1 -1
  43. package/dist/components/TyOption.d.ts +3 -3
  44. package/dist/components/TyOption.d.ts.map +1 -1
  45. package/dist/components/TyOption.js +2 -3
  46. package/dist/components/TyOption.js.map +1 -1
  47. package/dist/components/TyPopup.d.ts +4 -2
  48. package/dist/components/TyPopup.d.ts.map +1 -1
  49. package/dist/components/TyPopup.js +4 -10
  50. package/dist/components/TyPopup.js.map +1 -1
  51. package/dist/components/TyRadio.d.ts +4 -1
  52. package/dist/components/TyRadio.d.ts.map +1 -1
  53. package/dist/components/TyRadio.js +2 -1
  54. package/dist/components/TyRadio.js.map +1 -1
  55. package/dist/components/TyRadioGroup.d.ts +4 -1
  56. package/dist/components/TyRadioGroup.d.ts.map +1 -1
  57. package/dist/components/TyRadioGroup.js +2 -1
  58. package/dist/components/TyRadioGroup.js.map +1 -1
  59. package/dist/components/TyResizeObserver.d.ts.map +1 -1
  60. package/dist/components/TyResizeObserver.js +2 -5
  61. package/dist/components/TyResizeObserver.js.map +1 -1
  62. package/dist/components/TyScrollContainer.d.ts.map +1 -1
  63. package/dist/components/TyScrollContainer.js +2 -7
  64. package/dist/components/TyScrollContainer.js.map +1 -1
  65. package/dist/components/TySelect.d.ts +44 -2
  66. package/dist/components/TySelect.d.ts.map +1 -1
  67. package/dist/components/TySelect.js +34 -13
  68. package/dist/components/TySelect.js.map +1 -1
  69. package/dist/components/TyStep.d.ts.map +1 -1
  70. package/dist/components/TyStep.js +2 -6
  71. package/dist/components/TyStep.js.map +1 -1
  72. package/dist/components/TySwitch.d.ts +4 -1
  73. package/dist/components/TySwitch.d.ts.map +1 -1
  74. package/dist/components/TySwitch.js +2 -1
  75. package/dist/components/TySwitch.js.map +1 -1
  76. package/dist/components/TyTab.d.ts.map +1 -1
  77. package/dist/components/TyTab.js +2 -7
  78. package/dist/components/TyTab.js.map +1 -1
  79. package/dist/components/TyTabs.d.ts.map +1 -1
  80. package/dist/components/TyTabs.js +2 -6
  81. package/dist/components/TyTabs.js.map +1 -1
  82. package/dist/components/TyTag.d.ts +4 -1
  83. package/dist/components/TyTag.d.ts.map +1 -1
  84. package/dist/components/TyTag.js +2 -3
  85. package/dist/components/TyTag.js.map +1 -1
  86. package/dist/components/TyTextarea.d.ts.map +1 -1
  87. package/dist/components/TyTextarea.js +2 -7
  88. package/dist/components/TyTextarea.js.map +1 -1
  89. package/dist/components/TyTooltip.d.ts +7 -3
  90. package/dist/components/TyTooltip.d.ts.map +1 -1
  91. package/dist/components/TyTooltip.js +2 -5
  92. package/dist/components/TyTooltip.js.map +1 -1
  93. package/dist/components/TyWizard.d.ts.map +1 -1
  94. package/dist/components/TyWizard.js +2 -6
  95. package/dist/components/TyWizard.js.map +1 -1
  96. package/dist/components/index.d.ts +1 -1
  97. package/dist/components/index.d.ts.map +1 -1
  98. package/dist/components/index.js +3 -63
  99. package/dist/components/index.js.map +1 -1
  100. package/dist/utils/host-props.d.ts +11 -0
  101. package/dist/utils/host-props.d.ts.map +1 -0
  102. package/dist/utils/host-props.js +16 -0
  103. package/dist/utils/host-props.js.map +1 -0
  104. package/dist/version.d.ts +1 -1
  105. package/dist/version.js +1 -1
  106. package/package.json +1 -1
  107. package/src/components/TyButton.tsx +3 -2
  108. package/src/components/TyCalendar.tsx +14 -11
  109. package/src/components/TyCalendarMonth.tsx +11 -8
  110. package/src/components/TyCalendarNavigation.tsx +2 -7
  111. package/src/components/TyCheckbox.tsx +6 -11
  112. package/src/components/TyCopy.tsx +6 -9
  113. package/src/components/TyDatePicker.tsx +31 -19
  114. package/src/components/TyFileUpload.tsx +2 -3
  115. package/src/components/TyIcon.tsx +0 -5
  116. package/src/components/TyInput.tsx +8 -15
  117. package/src/components/TyModal.tsx +46 -22
  118. package/src/components/TyOption.tsx +5 -7
  119. package/src/components/TyPopup.tsx +12 -14
  120. package/src/components/TyRadio.tsx +6 -2
  121. package/src/components/TyRadioGroup.tsx +6 -2
  122. package/src/components/TyResizeObserver.tsx +2 -6
  123. package/src/components/TyScrollContainer.tsx +2 -9
  124. package/src/components/TySelect.tsx +88 -22
  125. package/src/components/TyStep.tsx +2 -7
  126. package/src/components/TySwitch.tsx +6 -2
  127. package/src/components/TyTab.tsx +2 -8
  128. package/src/components/TyTabs.tsx +2 -7
  129. package/src/components/TyTag.tsx +6 -5
  130. package/src/components/TyTextarea.tsx +2 -9
  131. package/src/components/TyTooltip.tsx +13 -9
  132. package/src/components/TyWizard.tsx +2 -7
  133. package/src/components/index.ts +3 -98
  134. package/src/utils/host-props.ts +14 -0
  135. package/src/version.ts +1 -1
@@ -1,8 +1,8 @@
1
1
  import React, { useEffect, useRef, useImperativeHandle } from 'react';
2
+ import { hostProps } from '../utils/host-props';
2
3
  import { useBooleanProperty, coerceBool } from '../utils/use-boolean-prop';
3
4
  import { needsPropertyBridge } from '../utils/react-version';
4
5
 
5
- // Event detail structure for modal events
6
6
  export interface TyModalEventDetail {
7
7
  reason?: 'programmatic' | 'native' | 'backdrop' | 'escape' | 'close-button';
8
8
  returnValue?: string;
@@ -18,7 +18,6 @@ export interface TyModalBeforeCloseDetail {
18
18
  reason: 'programmatic' | 'backdrop' | 'escape' | 'close-button' | 'native';
19
19
  }
20
20
 
21
- // Type definitions for Ty Modal component
22
21
  export interface TyModalProps extends React.HTMLAttributes<HTMLElement> {
23
22
  /** Controls modal visibility */
24
23
  open?: boolean;
@@ -26,12 +25,34 @@ export interface TyModalProps extends React.HTMLAttributes<HTMLElement> {
26
25
  /** Show backdrop behind modal (default: true) */
27
26
  backdrop?: boolean;
28
27
 
29
- /** Allow closing modal by clicking backdrop (default: true) */
28
+ /**
29
+ * @deprecated Use `preventOutsideClick` — string-boolean attrs are being
30
+ * phased out. Still works; prevent-* wins when both are set.
31
+ */
30
32
  closeOnOutsideClick?: boolean;
31
33
 
32
- /** Allow closing modal with Escape key (default: true) */
34
+ /**
35
+ * @deprecated Use `preventEscape` — string-boolean attrs are being phased
36
+ * out. Still works; prevent-* wins when both are set.
37
+ */
33
38
  closeOnEscape?: boolean;
34
39
 
40
+ /** Backdrop clicks no longer close the modal (also hides the built-in ✕) */
41
+ preventOutsideClick?: boolean;
42
+
43
+ /**
44
+ * ESC no longer closes the modal (keydown is consumed, so the browser's
45
+ * double-ESC anti-trap never fires). A fully guarded modal must render its
46
+ * own close control — WCAG 2.1.2.
47
+ */
48
+ preventEscape?: boolean;
49
+
50
+ /**
51
+ * Accessible name — sets aria-label on the internal <dialog>. The dialog
52
+ * gets role="dialog" for free, but nothing names it unless you set this.
53
+ */
54
+ label?: string;
55
+
35
56
  /** React event handlers */
36
57
  onOpen?: (event: CustomEvent<TyModalEventDetail>) => void;
37
58
  onClose?: (event: CustomEvent<TyModalEventDetail>) => void;
@@ -45,7 +66,6 @@ export interface TyModalProps extends React.HTMLAttributes<HTMLElement> {
45
66
  children?: React.ReactNode;
46
67
  }
47
68
 
48
- // Ref interface for imperative methods
49
69
  export interface TyModalRef {
50
70
  show: () => void;
51
71
  /**
@@ -57,13 +77,15 @@ export interface TyModalRef {
57
77
  element: HTMLElement | null;
58
78
  }
59
79
 
60
- // React wrapper for ty-modal web component
61
80
  export const TyModal = React.forwardRef<TyModalRef, TyModalProps>(
62
81
  ({
63
82
  open,
64
83
  backdrop,
65
84
  closeOnOutsideClick,
66
85
  closeOnEscape,
86
+ preventOutsideClick,
87
+ preventEscape,
88
+ label,
67
89
  onOpen,
68
90
  onClose,
69
91
  onBeforeClose,
@@ -72,7 +94,6 @@ export const TyModal = React.forwardRef<TyModalRef, TyModalProps>(
72
94
  }, ref) => {
73
95
  const elementRef = useRef<HTMLElement>(null);
74
96
 
75
- // Expose imperative methods through ref
76
97
  useImperativeHandle(ref, () => ({
77
98
  show: () => {
78
99
  if (elementRef.current && typeof (elementRef.current as any).show === 'function') {
@@ -87,17 +108,12 @@ export const TyModal = React.forwardRef<TyModalRef, TyModalProps>(
87
108
  element: elementRef.current,
88
109
  }), []);
89
110
 
90
- // Handle modal events.
91
- //
92
- // ty-dropdown, ty-multiselect, ty-date-picker etc. dispatch their own
93
- // `open`/`close` custom events with `bubbles: true, composed: true` when
94
- // their internal popups toggle. Those events bubble up through the
95
- // modal's slotted content and land on the ty-modal host — same event
96
- // type, same listener. Without the `event.target === element` guard, an
97
- // `onClose={() => setIsOpen(false)}` callback would fire every time the
98
- // user closes a dropdown inside the modal, and the React state flip
99
- // would close the modal. Core's modal.ts already applies the same guard
100
- // on its internal <dialog>.onclose; this is the React-layer mirror.
111
+ // Popup lifecycle events (`open`/`close`, plus `beforeclose` on the modal)
112
+ // no longer bubble — core dispatches them non-composed on the element
113
+ // itself, like the native <dialog> events they mirror, so a dropdown or
114
+ // nested modal closing inside this one can't reach these listeners. The
115
+ // `event.target === element` guard stays as belt-and-braces for consumers
116
+ // pinned to an older tyrell-components.
101
117
  useEffect(() => {
102
118
  const element = elementRef.current;
103
119
  if (!element) return;
@@ -117,7 +133,6 @@ export const TyModal = React.forwardRef<TyModalRef, TyModalProps>(
117
133
  if (onBeforeClose) onBeforeClose(event);
118
134
  };
119
135
 
120
- // Listen for custom modal events
121
136
  if (onOpen) {
122
137
  element.addEventListener('open', handleOpen as EventListener);
123
138
  }
@@ -162,11 +177,12 @@ export const TyModal = React.forwardRef<TyModalRef, TyModalProps>(
162
177
  setIf('backdrop', backdrop);
163
178
  setIf('closeOnOutsideClick', closeOnOutsideClick);
164
179
  setIf('closeOnEscape', closeOnEscape);
165
- }, [backdrop, closeOnOutsideClick, closeOnEscape]);
180
+ setIf('preventOutsideClick', preventOutsideClick);
181
+ setIf('preventEscape', preventEscape);
182
+ }, [backdrop, closeOnOutsideClick, closeOnEscape, preventOutsideClick, preventEscape]);
166
183
 
167
- // Convert React props to web component attributes
168
184
  const webComponentProps: Record<string, any> = {
169
- ...props,
185
+ ...hostProps(props),
170
186
  ref: elementRef,
171
187
  };
172
188
 
@@ -183,6 +199,14 @@ export const TyModal = React.forwardRef<TyModalRef, TyModalProps>(
183
199
  if (closeOnEscape !== undefined && !coerceBool(closeOnEscape)) {
184
200
  webComponentProps['close-on-escape'] = 'false';
185
201
  }
202
+ // Presence booleans: render the bare attribute only when true
203
+ if (preventOutsideClick !== undefined && coerceBool(preventOutsideClick)) {
204
+ webComponentProps['prevent-outside-click'] = '';
205
+ }
206
+ if (preventEscape !== undefined && coerceBool(preventEscape)) {
207
+ webComponentProps['prevent-escape'] = '';
208
+ }
209
+ if (label) webComponentProps.label = label;
186
210
 
187
211
  return React.createElement(
188
212
  'ty-modal',
@@ -1,19 +1,19 @@
1
1
  import React, { useEffect, useRef } from 'react';
2
+ import { hostProps } from '../utils/host-props';
2
3
  import { useBooleanProperty } from '../utils/use-boolean-prop';
3
4
 
4
- // Type definitions for Ty Option component
5
5
  export interface TyOptionProps extends React.HTMLAttributes<HTMLElement> {
6
6
  value?: string;
7
7
 
8
8
  /**
9
9
  * Clean display text (native <option label> semantics) — used by ty-select
10
- * for field summaries and by ty-selected-tags chips when the option's
10
+ * for field summaries and by ty-selected-options chips when the option's
11
11
  * children are rich HTML. data-* attributes feed chip templates.
12
12
  */
13
13
  label?: string;
14
14
 
15
- /** Semantic flavor — carried onto ty-selected-tags chips */
16
- flavor?: 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'neutral' | 'info';
15
+ /** Semantic flavor — carried onto ty-selected-options chips */
16
+ flavor?: 'primary' | 'success' | 'danger' | 'warning' | 'neutral' | 'info';
17
17
 
18
18
  disabled?: boolean;
19
19
  selected?: boolean;
@@ -21,12 +21,10 @@ export interface TyOptionProps extends React.HTMLAttributes<HTMLElement> {
21
21
  children?: React.ReactNode;
22
22
  }
23
23
 
24
- // React wrapper for ty-option web component
25
24
  export const TyOption = React.forwardRef<HTMLElement, TyOptionProps>(
26
25
  ({ children, label, flavor, disabled, selected, hidden, ...props }, ref) => {
27
26
  const elementRef = useRef<HTMLElement>(null);
28
27
 
29
- // Handle ref forwarding
30
28
  useEffect(() => {
31
29
  if (ref && elementRef.current) {
32
30
  if (typeof ref === 'function') {
@@ -44,7 +42,7 @@ export const TyOption = React.forwardRef<HTMLElement, TyOptionProps>(
44
42
  return React.createElement(
45
43
  'ty-option',
46
44
  {
47
- ...props,
45
+ ...hostProps(props),
48
46
  ...(label && { label }),
49
47
  ...(flavor && { flavor }),
50
48
  ...(isDisabled && { disabled: "" }),
@@ -1,10 +1,16 @@
1
1
  import React, { useEffect, useRef } from 'react';
2
+ import { hostProps } from '../utils/host-props';
2
3
  import { useBooleanProperty } from '../utils/use-boolean-prop';
3
4
 
4
- // Type definitions for Ty Popup component
5
+ type Placement =
6
+ | 'top-start' | 'top' | 'top-end'
7
+ | 'right-start' | 'right' | 'right-end'
8
+ | 'bottom-start' | 'bottom' | 'bottom-end'
9
+ | 'left-start' | 'left' | 'left-end';
10
+
5
11
  export interface TyPopupProps extends Omit<React.HTMLAttributes<HTMLElement>, 'onClose'> {
6
- /** Preferred placement of the popup relative to anchor parent: "top" | "bottom" | "left" | "right" */
7
- placement?: 'top' | 'bottom' | 'left' | 'right';
12
+ /** Preferred placement of the popup relative to anchor parent: a side plus an optional cross-axis alignment (e.g. "bottom-start") */
13
+ placement?: Placement;
8
14
 
9
15
  /** Distance offset from the anchor in pixels (default: 8) */
10
16
  offset?: number;
@@ -25,14 +31,12 @@ export interface TyPopupProps extends Omit<React.HTMLAttributes<HTMLElement>, 'o
25
31
  children?: React.ReactNode;
26
32
  }
27
33
 
28
- // Programmatic API for popup control
29
34
  export interface TyPopupElement extends HTMLElement {
30
35
  openPopup(): void;
31
36
  closePopup(): void;
32
37
  togglePopup(): void;
33
38
  }
34
39
 
35
- // React wrapper for ty-popup web component
36
40
  export const TyPopup = React.forwardRef<TyPopupElement, TyPopupProps>(
37
41
  ({
38
42
  placement,
@@ -46,7 +50,6 @@ export const TyPopup = React.forwardRef<TyPopupElement, TyPopupProps>(
46
50
  }, ref) => {
47
51
  const elementRef = useRef<TyPopupElement>(null);
48
52
 
49
- // Handle ref forwarding
50
53
  useEffect(() => {
51
54
  if (ref && elementRef.current) {
52
55
  if (typeof ref === 'function') {
@@ -57,11 +60,8 @@ export const TyPopup = React.forwardRef<TyPopupElement, TyPopupProps>(
57
60
  }
58
61
  }, [ref]);
59
62
 
60
- // Listen for popup open/close events.
61
- // Guard with `event.target === element` so bubbled open/close events
62
- // from popup-like descendants (ty-dropdown, ty-multiselect, ty-date-picker
63
- // when slotted inside this popup) don't fire the consumer's onOpen/onClose.
64
- // See TyModal.tsx for the same pattern + rationale.
63
+ // Listen for popup open/close events. See TyModal.tsx for why the
64
+ // `event.target === element` guard is still here.
65
65
  useEffect(() => {
66
66
  const element = elementRef.current;
67
67
  if (!element) return;
@@ -84,13 +84,11 @@ export const TyPopup = React.forwardRef<TyPopupElement, TyPopupProps>(
84
84
  };
85
85
  }, [onOpen, onClose]);
86
86
 
87
- // Convert React props to web component attributes
88
87
  const webComponentProps: Record<string, any> = {
89
- ...props,
88
+ ...hostProps(props),
90
89
  ref: elementRef,
91
90
  };
92
91
 
93
- // Add optional attributes only if they have values
94
92
  if (placement) {
95
93
  webComponentProps.placement = placement;
96
94
  }
@@ -1,6 +1,10 @@
1
1
  import React, { useEffect, useRef } from 'react';
2
+ import { hostProps } from '../utils/host-props';
2
3
  import { useBooleanProperty } from '../utils/use-boolean-prop';
3
4
 
5
+ type BuiltinFlavor = 'primary' | 'success' | 'danger' | 'warning' | 'neutral';
6
+ type ShadedFlavor = BuiltinFlavor | `${BuiltinFlavor}+` | `${BuiltinFlavor}-`;
7
+
4
8
  export interface TyRadioProps extends React.HTMLAttributes<HTMLElement> {
5
9
  /** Form field value (selected by parent ty-radio-group when matches its `value`) */
6
10
  value?: string;
@@ -18,7 +22,7 @@ export interface TyRadioProps extends React.HTMLAttributes<HTMLElement> {
18
22
  size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
19
23
 
20
24
  /** Semantic styling variant — typically inherited from the parent group */
21
- flavor?: 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'neutral';
25
+ flavor?: ShadedFlavor | (string & {});
22
26
 
23
27
  /** Label content (wrap in a `<label>` for click delegation, see ty-radio docs) */
24
28
  children?: React.ReactNode;
@@ -43,7 +47,7 @@ export const TyRadio = React.forwardRef<HTMLElement, TyRadioProps>(
43
47
  const isDisabled = useBooleanProperty(elementRef, 'disabled', disabled);
44
48
 
45
49
  const webComponentProps: Record<string, any> = {
46
- ...props,
50
+ ...hostProps(props),
47
51
  ref: elementRef,
48
52
  };
49
53
 
@@ -1,6 +1,10 @@
1
1
  import React, { useEffect, useRef } from 'react';
2
+ import { hostProps } from '../utils/host-props';
2
3
  import { useBooleanProperty } from '../utils/use-boolean-prop';
3
4
 
5
+ type BuiltinFlavor = 'primary' | 'success' | 'danger' | 'warning' | 'neutral';
6
+ type ShadedFlavor = BuiltinFlavor | `${BuiltinFlavor}+` | `${BuiltinFlavor}-`;
7
+
4
8
  export interface TyRadioGroupProps extends Omit<React.HTMLAttributes<HTMLElement>, 'onChange' | 'onInput'> {
5
9
  /** Currently selected value (matches one child `<TyRadio value="...">`) */
6
10
  value?: string;
@@ -27,7 +31,7 @@ export interface TyRadioGroupProps extends Omit<React.HTMLAttributes<HTMLElement
27
31
  size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
28
32
 
29
33
  /** Group flavor — propagates to all `<TyRadio>` children */
30
- flavor?: 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'neutral';
34
+ flavor?: ShadedFlavor | (string & {});
31
35
 
32
36
  /**
33
37
  * Fires when selection changes (React convention)
@@ -103,7 +107,7 @@ export const TyRadioGroup = React.forwardRef<HTMLElement, TyRadioGroupProps>(
103
107
  const isRequired = useBooleanProperty(elementRef, 'required', required);
104
108
 
105
109
  const webComponentProps: Record<string, any> = {
106
- ...props,
110
+ ...hostProps(props),
107
111
  ref: elementRef,
108
112
  };
109
113
 
@@ -1,6 +1,6 @@
1
1
  import React, { useEffect, useRef } from 'react';
2
+ import { hostProps } from '../utils/host-props';
2
3
 
3
- // Type definitions for Ty ResizeObserver component
4
4
  export interface TyResizeObserverProps extends React.HTMLAttributes<HTMLElement> {
5
5
  /** Required unique identifier for size registry */
6
6
  id: string;
@@ -12,7 +12,6 @@ export interface TyResizeObserverProps extends React.HTMLAttributes<HTMLElement>
12
12
  children?: React.ReactNode;
13
13
  }
14
14
 
15
- // React wrapper for ty-resize-observer web component
16
15
  export const TyResizeObserver = React.forwardRef<HTMLElement, TyResizeObserverProps>(
17
16
  ({
18
17
  children,
@@ -22,7 +21,6 @@ export const TyResizeObserver = React.forwardRef<HTMLElement, TyResizeObserverPr
22
21
  }, ref) => {
23
22
  const elementRef = useRef<HTMLElement>(null);
24
23
 
25
- // Combine refs if needed
26
24
  useEffect(() => {
27
25
  if (ref && elementRef.current) {
28
26
  if (typeof ref === 'function') {
@@ -33,14 +31,12 @@ export const TyResizeObserver = React.forwardRef<HTMLElement, TyResizeObserverPr
33
31
  }
34
32
  }, [ref]);
35
33
 
36
- // Convert React props to web component attributes
37
34
  const webComponentProps: Record<string, any> = {
38
- ...props,
35
+ ...hostProps(props),
39
36
  ref: elementRef,
40
37
  id,
41
38
  };
42
39
 
43
- // Add number attributes
44
40
  if (debounce !== undefined) webComponentProps.debounce = debounce;
45
41
 
46
42
  return React.createElement(
@@ -1,4 +1,5 @@
1
1
  import React, { useEffect, useRef, useImperativeHandle } from 'react';
2
+ import { hostProps } from '../utils/host-props';
2
3
  import { needsPropertyBridge } from '../utils/react-version';
3
4
  import { useBooleanProperty, coerceBool } from '../utils/use-boolean-prop';
4
5
 
@@ -10,7 +11,6 @@ export interface TyScrollNearEdgeDetail {
10
11
  clientHeight: number;
11
12
  }
12
13
 
13
- // Type definitions for Ty ScrollContainer component
14
14
  export interface TyScrollContainerProps extends React.HTMLAttributes<HTMLElement> {
15
15
  /** Maximum height of the scroll container */
16
16
  maxHeight?: string;
@@ -43,7 +43,6 @@ export interface TyScrollContainerProps extends React.HTMLAttributes<HTMLElement
43
43
  children?: React.ReactNode;
44
44
  }
45
45
 
46
- // Ref interface for imperative methods
47
46
  export interface TyScrollContainerRef {
48
47
  /** Force update shadows (useful after dynamic content changes) */
49
48
  updateShadows: () => void;
@@ -63,7 +62,6 @@ export interface TyScrollContainerRef {
63
62
  element: HTMLElement | null;
64
63
  }
65
64
 
66
- // React wrapper for ty-scroll-container web component
67
65
  export const TyScrollContainer = React.forwardRef<TyScrollContainerRef, TyScrollContainerProps>(
68
66
  ({
69
67
  children,
@@ -80,7 +78,6 @@ export const TyScrollContainer = React.forwardRef<TyScrollContainerRef, TyScroll
80
78
  }, ref) => {
81
79
  const elementRef = useRef<HTMLElement>(null);
82
80
 
83
- // Expose imperative methods via ref
84
81
  useImperativeHandle(ref, () => ({
85
82
  updateShadows: () => { (elementRef.current as any)?.updateShadows?.(); },
86
83
  scrollToTop: (smooth = true) => { (elementRef.current as any)?.scrollToTop?.(smooth); },
@@ -108,7 +105,6 @@ export const TyScrollContainer = React.forwardRef<TyScrollContainerRef, TyScroll
108
105
  const isOverflowX = useBooleanProperty(elementRef, 'overflowX', overflowX);
109
106
  const isScrollAnchoring = useBooleanProperty(elementRef, 'scrollAnchoring', scrollAnchoring);
110
107
 
111
- // Custom events → React callbacks
112
108
  useEffect(() => {
113
109
  const el = elementRef.current;
114
110
  if (!el) return;
@@ -124,17 +120,14 @@ export const TyScrollContainer = React.forwardRef<TyScrollContainerRef, TyScroll
124
120
  return () => bound.forEach(([n, h]) => el.removeEventListener(n, h));
125
121
  }, [onNearEnd, onNearStart]);
126
122
 
127
- // Convert React props to web component attributes
128
123
  const webComponentProps: Record<string, any> = {
129
- ...props,
124
+ ...hostProps(props),
130
125
  ref: elementRef,
131
126
  };
132
127
 
133
- // String attributes
134
128
  if (maxHeight) webComponentProps['max-height'] = maxHeight;
135
129
  if (nearEdgeThreshold != null) webComponentProps['near-edge-threshold'] = String(nearEdgeThreshold);
136
130
 
137
- // Boolean attributes
138
131
  if (shadow !== undefined && !coerceBool(shadow)) webComponentProps.shadow = 'false';
139
132
  if (isHideScrollbar) webComponentProps['hide-scrollbar'] = '';
140
133
  if (isCustomScrollbar) webComponentProps['custom-scrollbar'] = '';
@@ -1,8 +1,16 @@
1
1
  import React, { useEffect, useRef, useCallback } from 'react';
2
+ import { hostProps } from '../utils/host-props';
2
3
  import { needsPropertyBridge } from '../utils/react-version';
3
4
  import { useBooleanProperty } from '../utils/use-boolean-prop';
4
5
 
5
- // Type definitions for Ty Select component
6
+ type BuiltinFlavor = 'primary' | 'success' | 'danger' | 'warning' | 'neutral';
7
+ type ShadedFlavor = BuiltinFlavor | `${BuiltinFlavor}+` | `${BuiltinFlavor}-`;
8
+ type Placement =
9
+ | 'top-start' | 'top' | 'top-end'
10
+ | 'right-start' | 'right' | 'right-end'
11
+ | 'bottom-start' | 'bottom' | 'bottom-end'
12
+ | 'left-start' | 'left' | 'left-end';
13
+
6
14
  export interface TySelectItem {
7
15
  value: string;
8
16
  label: string;
@@ -17,7 +25,7 @@ export interface TySelectEventDetail {
17
25
  /** Rich info per selected value — enough to render chips out-of-band */
18
26
  items: TySelectItem[];
19
27
  /** Action that triggered the change */
20
- action: 'add' | 'remove' | 'clear' | 'set';
28
+ action: 'add' | 'remove' | 'clear' | 'set' | 'create';
21
29
  /** The specific value that changed */
22
30
  item: string | null;
23
31
  }
@@ -67,12 +75,40 @@ export interface TySelectProps extends Omit<React.HTMLAttributes<HTMLElement>, '
67
75
  /** Debounce for the search event in ms (0-5000) */
68
76
  debounce?: number;
69
77
 
78
+ /**
79
+ * Enter on unmatched search text mints a new <ty-option> (forces the
80
+ * search row on, same as externalSearch). Listen to onCreate to mutate
81
+ * the value before it's created, or preventDefault() to take over
82
+ * entirely (e.g. after a server round-trip).
83
+ */
84
+ allowCreate?: boolean;
85
+
86
+ /**
87
+ * Opt-in normalizer for the value allowCreate mints — the display label
88
+ * is always kept verbatim. 'slug': lowercase, spaces → '_', strips
89
+ * everything else non-alphanumeric. Default: value === typed text.
90
+ */
91
+ createTransform?: 'slug';
92
+
70
93
  /** Loading state — shows a spinner in the options area (external search in flight) */
71
94
  loading?: boolean;
72
95
 
73
96
  /** Size variant */
97
+ /** Field size — fields come in exactly three; legacy xs/xl map to sm/lg */
74
98
  size?: 'sm' | 'md' | 'lg';
75
99
 
100
+ /** Horizontal popup anchor: "start" (default, trigger's left edge), "center", or "end" (trigger's right edge) — clamped into the viewport either way. Useful with a custom slot="trigger" element. Ignored when `placement` is set. */
101
+ align?: 'start' | 'center' | 'end';
102
+
103
+ /** Full placement, same vocabulary as ty-popup/ty-tooltip: a side plus an optional cross-axis alignment (e.g. "bottom-end"). Takes precedence over `align` when set. */
104
+ placement?: Placement;
105
+
106
+ /** Built-in x clear button in the default/compact trigger, shown once something is selected. Default true. Not shown with slot="trigger" — use the ref's clear() method there instead. */
107
+ clearable?: boolean;
108
+
109
+ /** Visual flavor for the field border — built-in semantic, +/- shade, or a custom flavor backed by --ty-*-X tokens */
110
+ flavor?: ShadedFlavor | (string & {});
111
+
76
112
  /** Callback when selection changes */
77
113
  onChange?: (event: CustomEvent<TySelectEventDetail>) => void;
78
114
 
@@ -81,12 +117,23 @@ export interface TySelectProps extends Omit<React.HTMLAttributes<HTMLElement>, '
81
117
  onOpen?: (event: CustomEvent) => void;
82
118
  onClose?: (event: CustomEvent) => void;
83
119
 
120
+ /**
121
+ * Fired before allowCreate mints a new option — cancelable. Mutate
122
+ * `event.detail.value` to change the id that gets created (e.g. slugify
123
+ * it yourself), or call `event.preventDefault()` to create it yourself.
124
+ */
125
+ onCreate?: (event: CustomEvent<{ value: string; label: string }>) => void;
126
+
84
127
  /** TyOption children (plus optional slot="trigger" / slot="start" / slot="end" elements) */
85
128
  children?: React.ReactNode;
86
129
  }
87
130
 
88
- // React wrapper for ty-select web component
89
- export const TySelect = React.forwardRef<HTMLElement, TySelectProps>(
131
+ /** The underlying custom element, typed with its imperative clear() method — useful with a ref, e.g. for slot="trigger" custom triggers that call it directly instead of relying on the built-in clear button. */
132
+ export interface TySelectElement extends HTMLElement {
133
+ clear(): void;
134
+ }
135
+
136
+ export const TySelect = React.forwardRef<TySelectElement, TySelectProps>(
90
137
  ({
91
138
  value,
92
139
  multiple,
@@ -100,18 +147,24 @@ export const TySelect = React.forwardRef<HTMLElement, TySelectProps>(
100
147
  searchable,
101
148
  externalSearch,
102
149
  debounce,
150
+ allowCreate,
151
+ createTransform,
103
152
  loading,
104
153
  size,
154
+ align,
155
+ placement,
156
+ clearable,
157
+ flavor,
105
158
  onChange,
106
159
  onSearch,
107
160
  onOpen,
108
161
  onClose,
162
+ onCreate,
109
163
  children,
110
164
  ...props
111
165
  }, ref) => {
112
- const elementRef = useRef<HTMLElement>(null);
166
+ const elementRef = useRef<TySelectElement>(null);
113
167
 
114
- // Handle ref forwarding
115
168
  useEffect(() => {
116
169
  if (ref && elementRef.current) {
117
170
  if (typeof ref === 'function') {
@@ -134,7 +187,6 @@ export const TySelect = React.forwardRef<HTMLElement, TySelectProps>(
134
187
  }
135
188
  }, [value]);
136
189
 
137
- // Handle events
138
190
  const handleChange = useCallback((event: Event) => {
139
191
  if (onChange) onChange(event as CustomEvent<TySelectEventDetail>);
140
192
  }, [onChange]);
@@ -145,8 +197,10 @@ export const TySelect = React.forwardRef<HTMLElement, TySelectProps>(
145
197
 
146
198
  const handleOpen = useCallback((event: Event) => { if (onOpen) onOpen(event as CustomEvent); }, [onOpen]);
147
199
  const handleClose = useCallback((event: Event) => { if (onClose) onClose(event as CustomEvent); }, [onClose]);
200
+ const handleCreate = useCallback((event: Event) => {
201
+ if (onCreate) onCreate(event as CustomEvent<{ value: string; label: string }>);
202
+ }, [onCreate]);
148
203
 
149
- // Set up event listeners
150
204
  useEffect(() => {
151
205
  const element = elementRef.current;
152
206
  if (!element) return;
@@ -155,14 +209,16 @@ export const TySelect = React.forwardRef<HTMLElement, TySelectProps>(
155
209
  if (onSearch) element.addEventListener('search', handleSearch);
156
210
  if (onOpen) element.addEventListener('open', handleOpen);
157
211
  if (onClose) element.addEventListener('close', handleClose);
212
+ if (onCreate) element.addEventListener('create', handleCreate);
158
213
 
159
214
  return () => {
160
215
  if (onChange) element.removeEventListener('change', handleChange);
161
216
  if (onSearch) element.removeEventListener('search', handleSearch);
162
217
  if (onOpen) element.removeEventListener('open', handleOpen);
163
218
  if (onClose) element.removeEventListener('close', handleClose);
219
+ if (onCreate) element.removeEventListener('create', handleCreate);
164
220
  };
165
- }, [handleChange, handleSearch, handleOpen, handleClose, onChange, onSearch, onOpen, onClose]);
221
+ }, [handleChange, handleSearch, handleOpen, handleClose, handleCreate, onChange, onSearch, onOpen, onClose, onCreate]);
166
222
 
167
223
  // Imperative property sync for boolean props (see use-boolean-prop.ts).
168
224
  const isMultiple = useBooleanProperty(elementRef, 'multiple', multiple);
@@ -172,14 +228,14 @@ export const TySelect = React.forwardRef<HTMLElement, TySelectProps>(
172
228
  const isRequired = useBooleanProperty(elementRef, 'required', required);
173
229
  const isLoading = useBooleanProperty(elementRef, 'loading', loading);
174
230
  const isExternalSearch = useBooleanProperty(elementRef, 'externalSearch', externalSearch);
231
+ const isAllowCreate = useBooleanProperty(elementRef, 'allowCreate', allowCreate);
232
+ const isClearable = useBooleanProperty(elementRef, 'clearable', clearable);
175
233
 
176
- // Convert React props to web component attributes
177
234
  const webComponentProps: Record<string, any> = {
178
- ...props,
235
+ ...hostProps(props),
179
236
  ref: elementRef,
180
237
  };
181
238
 
182
- // Handle value conversion (array to comma-separated string)
183
239
  if (value !== undefined) {
184
240
  webComponentProps.value = Array.isArray(value) ? value.join(',') : value;
185
241
  }
@@ -191,12 +247,18 @@ export const TySelect = React.forwardRef<HTMLElement, TySelectProps>(
191
247
  if (isRequired) webComponentProps.required = '';
192
248
  if (isLoading) webComponentProps.loading = '';
193
249
  if (isExternalSearch) webComponentProps['external-search'] = '';
250
+ if (isAllowCreate) webComponentProps['allow-create'] = '';
194
251
 
195
252
  if (placeholder) webComponentProps.placeholder = placeholder;
196
253
  if (label) webComponentProps.label = label;
197
254
  if (name) webComponentProps.name = name;
198
255
  if (size) webComponentProps.size = size;
256
+ if (align) webComponentProps.align = align;
257
+ if (placement) webComponentProps.placement = placement;
258
+ if (isClearable) webComponentProps.clearable = '';
259
+ if (flavor) webComponentProps.flavor = flavor;
199
260
  if (debounce !== undefined) webComponentProps.debounce = debounce.toString();
261
+ if (createTransform) webComponentProps['create-transform'] = createTransform;
200
262
  if (searchable === 'always' || searchable === true) webComponentProps.searchable = 'true';
201
263
  else if (searchable === 'never' || searchable === false) webComponentProps.searchable = 'false';
202
264
  // 'auto' / undefined → omit (component default)
@@ -207,9 +269,9 @@ export const TySelect = React.forwardRef<HTMLElement, TySelectProps>(
207
269
 
208
270
  TySelect.displayName = 'TySelect';
209
271
 
210
- // ============================================================================
211
- // TySelectedTags — out-of-band chip display for a TySelect
212
- // ============================================================================
272
+ // TySelectedOptions — out-of-band chip display for a TySelect
273
+ // (ty-selected-tags below is the same element under its original tag name,
274
+ // kept for backward compatibility.)
213
275
 
214
276
  export interface TySelectedTagsProps extends React.HTMLAttributes<HTMLElement> {
215
277
  /** id of the ty-select to display. Falls back to the previous element sibling. */
@@ -218,9 +280,8 @@ export interface TySelectedTagsProps extends React.HTMLAttributes<HTMLElement> {
218
280
  children?: React.ReactNode;
219
281
  }
220
282
 
221
- // React wrapper for ty-selected-tags web component
222
- export const TySelectedTags = React.forwardRef<HTMLElement, TySelectedTagsProps>(
223
- ({ htmlFor, children, ...props }, ref) => {
283
+ const selectedChipsComponent = (tag: 'ty-selected-options' | 'ty-selected-tags') =>
284
+ React.forwardRef<HTMLElement, TySelectedTagsProps>(({ htmlFor, children, ...props }, ref) => {
224
285
  const elementRef = useRef<HTMLElement>(null);
225
286
 
226
287
  useEffect(() => {
@@ -230,11 +291,16 @@ export const TySelectedTags = React.forwardRef<HTMLElement, TySelectedTagsProps>
230
291
  }
231
292
  }, [ref]);
232
293
 
233
- const webComponentProps: Record<string, any> = { ...props, ref: elementRef };
294
+ const webComponentProps: Record<string, any> = { ...hostProps(props), ref: elementRef };
234
295
  if (htmlFor) webComponentProps.for = htmlFor;
235
296
 
236
- return React.createElement('ty-selected-tags', webComponentProps, children);
237
- }
238
- );
297
+ return React.createElement(tag, webComponentProps, children);
298
+ });
299
+
300
+ /** React wrapper for the ty-selected-options web component. */
301
+ export const TySelectedOptions = selectedChipsComponent('ty-selected-options');
302
+ TySelectedOptions.displayName = 'TySelectedOptions';
239
303
 
304
+ /** @deprecated Use `TySelectedOptions` — kept for backward compatibility. */
305
+ export const TySelectedTags = selectedChipsComponent('ty-selected-tags');
240
306
  TySelectedTags.displayName = 'TySelectedTags';