@tcn/ui 0.10.0 → 0.11.0

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 (165) hide show
  1. package/dist/form/field/h_field/h_field.d.ts.map +1 -1
  2. package/dist/form/field/h_field/h_field.js +33 -35
  3. package/dist/form/field/h_field/h_field.js.map +1 -1
  4. package/dist/form/field/v_field/v_field.d.ts.map +1 -1
  5. package/dist/form/field/v_field/v_field.js +34 -36
  6. package/dist/form/field/v_field/v_field.js.map +1 -1
  7. package/dist/frame.css +1 -1
  8. package/dist/inputs/color_input/color_input.d.ts.map +1 -1
  9. package/dist/inputs/color_input/color_input.js +47 -46
  10. package/dist/inputs/color_input/color_input.js.map +1 -1
  11. package/dist/inputs/combo_box/combo_box.d.ts.map +1 -1
  12. package/dist/inputs/combo_box/combo_box.js +61 -58
  13. package/dist/inputs/combo_box/combo_box.js.map +1 -1
  14. package/dist/inputs/index.d.ts +1 -0
  15. package/dist/inputs/index.d.ts.map +1 -1
  16. package/dist/inputs/index.js +34 -31
  17. package/dist/inputs/index.js.map +1 -1
  18. package/dist/inputs/input/input.js +9 -9
  19. package/dist/inputs/input/input.js.map +1 -1
  20. package/dist/inputs/input_group/input_group.d.ts +5 -0
  21. package/dist/inputs/input_group/input_group.d.ts.map +1 -0
  22. package/dist/inputs/input_group/input_group.js +20 -0
  23. package/dist/inputs/input_group/input_group.js.map +1 -0
  24. package/dist/inputs/phone_number_input/countries_phone_information.d.ts +2 -2
  25. package/dist/inputs/phone_number_input/countries_phone_information.d.ts.map +1 -1
  26. package/dist/inputs/phone_number_input/countries_phone_information.js +5 -353
  27. package/dist/inputs/phone_number_input/countries_phone_information.js.map +1 -1
  28. package/dist/inputs/phone_number_input/phone_number_context.d.ts +24 -0
  29. package/dist/inputs/phone_number_input/phone_number_context.d.ts.map +1 -0
  30. package/dist/inputs/phone_number_input/phone_number_context.js +23 -0
  31. package/dist/inputs/phone_number_input/phone_number_context.js.map +1 -0
  32. package/dist/inputs/phone_number_input/phone_number_country_select_adapter.d.ts +19 -0
  33. package/dist/inputs/phone_number_input/phone_number_country_select_adapter.d.ts.map +1 -0
  34. package/dist/inputs/phone_number_input/phone_number_country_select_adapter.js +77 -0
  35. package/dist/inputs/phone_number_input/phone_number_country_select_adapter.js.map +1 -0
  36. package/dist/inputs/phone_number_input/phone_number_input.d.ts +16 -14
  37. package/dist/inputs/phone_number_input/phone_number_input.d.ts.map +1 -1
  38. package/dist/inputs/phone_number_input/phone_number_input.js +104 -274
  39. package/dist/inputs/phone_number_input/phone_number_input.js.map +1 -1
  40. package/dist/inputs/phone_number_input/phone_number_input_adapter.d.ts +6 -0
  41. package/dist/inputs/phone_number_input/phone_number_input_adapter.d.ts.map +1 -0
  42. package/dist/inputs/phone_number_input/phone_number_input_adapter.js +95 -0
  43. package/dist/inputs/phone_number_input/phone_number_input_adapter.js.map +1 -0
  44. package/dist/inputs/phone_number_input/sip_input.d.ts +12 -0
  45. package/dist/inputs/phone_number_input/sip_input.d.ts.map +1 -0
  46. package/dist/inputs/phone_number_input/sip_input.js +111 -0
  47. package/dist/inputs/phone_number_input/sip_input.js.map +1 -0
  48. package/dist/inputs/select/select.d.ts.map +1 -1
  49. package/dist/inputs/select/select.js +3 -2
  50. package/dist/inputs/select/select.js.map +1 -1
  51. package/dist/inputs/suggestions/suggestion_list.d.ts +4 -1
  52. package/dist/inputs/suggestions/suggestion_list.d.ts.map +1 -1
  53. package/dist/inputs/suggestions/suggestion_list.js +120 -111
  54. package/dist/inputs/suggestions/suggestion_list.js.map +1 -1
  55. package/dist/inputs/textarea/textarea.js +8 -8
  56. package/dist/inputs/textarea/textarea.js.map +1 -1
  57. package/dist/inputs/unit_input/unit_input.d.ts.map +1 -1
  58. package/dist/inputs/unit_input/unit_input.js +39 -39
  59. package/dist/inputs/unit_input/unit_input.js.map +1 -1
  60. package/dist/overlay/frame/frame.d.ts +8 -4
  61. package/dist/overlay/frame/frame.d.ts.map +1 -1
  62. package/dist/overlay/frame/frame.js +87 -23
  63. package/dist/overlay/frame/frame.js.map +1 -1
  64. package/dist/overlay/popper/base/dismissal_decorator.js.map +1 -1
  65. package/dist/overlay/popper/legacy/popper.d.ts.map +1 -1
  66. package/dist/overlay/popper/legacy/popper.js +52 -50
  67. package/dist/overlay/popper/legacy/popper.js.map +1 -1
  68. package/dist/phone_number_input.css +1 -1
  69. package/dist/stacks/box/bottom_resize_handle.d.ts +1 -1
  70. package/dist/stacks/box/bottom_resize_handle.d.ts.map +1 -1
  71. package/dist/stacks/box/bottom_resize_handle.js.map +1 -1
  72. package/dist/stacks/box/box.d.ts +2 -2
  73. package/dist/stacks/box/box.d.ts.map +1 -1
  74. package/dist/stacks/box/box.js.map +1 -1
  75. package/dist/stacks/box/end_resize_handle.d.ts +1 -1
  76. package/dist/stacks/box/end_resize_handle.d.ts.map +1 -1
  77. package/dist/stacks/box/end_resize_handle.js.map +1 -1
  78. package/dist/stacks/box/left_resize_handle.d.ts +1 -1
  79. package/dist/stacks/box/left_resize_handle.d.ts.map +1 -1
  80. package/dist/stacks/box/left_resize_handle.js.map +1 -1
  81. package/dist/stacks/box/resize_handlers.d.ts +2 -2
  82. package/dist/stacks/box/resize_handlers.d.ts.map +1 -1
  83. package/dist/stacks/box/resize_handlers.js +32 -32
  84. package/dist/stacks/box/resize_handlers.js.map +1 -1
  85. package/dist/stacks/box/right_resize_handle.d.ts +1 -1
  86. package/dist/stacks/box/right_resize_handle.d.ts.map +1 -1
  87. package/dist/stacks/box/right_resize_handle.js.map +1 -1
  88. package/dist/stacks/box/start_resize_handle.d.ts +1 -1
  89. package/dist/stacks/box/start_resize_handle.d.ts.map +1 -1
  90. package/dist/stacks/box/start_resize_handle.js +4 -4
  91. package/dist/stacks/box/start_resize_handle.js.map +1 -1
  92. package/dist/stacks/box/top_resize_handle.d.ts +1 -1
  93. package/dist/stacks/box/top_resize_handle.d.ts.map +1 -1
  94. package/dist/stacks/box/top_resize_handle.js +4 -4
  95. package/dist/stacks/box/top_resize_handle.js.map +1 -1
  96. package/dist/stacks/h_collapsible_box.js +18 -18
  97. package/dist/stacks/h_collapsible_box.js.map +1 -1
  98. package/dist/stacks/v_collapsible_box.js +18 -18
  99. package/dist/stacks/v_collapsible_box.js.map +1 -1
  100. package/dist/suggestion_list.css +1 -1
  101. package/dist/surfaces/window/window.d.ts +1 -1
  102. package/dist/surfaces/window/window.d.ts.map +1 -1
  103. package/dist/surfaces/window/window.js +20 -10
  104. package/dist/surfaces/window/window.js.map +1 -1
  105. package/dist/themes/stylesheets/reset.css +1 -1
  106. package/dist/themes/stylesheets/reset.js +8 -1
  107. package/dist/themes/stylesheets/reset.js.map +1 -1
  108. package/dist/themes/themes/ergo/ergo_theme.css +1 -1
  109. package/dist/themes/themes/ergo/ergo_theme.js +183 -18
  110. package/dist/themes/themes/ergo/ergo_theme.js.map +1 -1
  111. package/dist/typography/body_text/body_text.d.ts.map +1 -1
  112. package/dist/typography/body_text/body_text.js +12 -10
  113. package/dist/typography/body_text/body_text.js.map +1 -1
  114. package/dist/utils/dnd/hooks/use_drag_container.d.ts.map +1 -1
  115. package/dist/utils/dnd/hooks/use_drag_container.js +22 -19
  116. package/dist/utils/dnd/hooks/use_drag_container.js.map +1 -1
  117. package/package.json +4 -2
  118. package/src/form/field/h_field/h_field.tsx +0 -4
  119. package/src/form/field/v_field/v_field.stories.tsx +8 -0
  120. package/src/form/field/v_field/v_field.tsx +1 -4
  121. package/src/form/field_set/field_set.stories.tsx +2 -1
  122. package/src/inputs/__docs__/inputs.mdx +81 -0
  123. package/src/inputs/__docs__/inputs.stories.tsx +268 -0
  124. package/src/inputs/color_input/color_input.tsx +17 -17
  125. package/src/inputs/combo_box/combo_box.tsx +17 -13
  126. package/src/inputs/index.ts +2 -0
  127. package/src/inputs/input/input.tsx +1 -1
  128. package/src/inputs/input_group/input_group.tsx +26 -0
  129. package/src/inputs/phone_number_input/countries_phone_information.ts +6 -353
  130. package/src/inputs/phone_number_input/phone_number_context.tsx +32 -0
  131. package/src/inputs/phone_number_input/phone_number_country_select_adapter.tsx +126 -0
  132. package/src/inputs/phone_number_input/phone_number_input.module.css +5 -63
  133. package/src/inputs/phone_number_input/phone_number_input.stories.tsx +180 -150
  134. package/src/inputs/phone_number_input/phone_number_input.tsx +133 -400
  135. package/src/inputs/phone_number_input/phone_number_input_adapter.tsx +123 -0
  136. package/src/inputs/phone_number_input/sip_input.tsx +147 -0
  137. package/src/inputs/select/select.tsx +13 -14
  138. package/src/inputs/suggestions/suggestion_list.module.css +1 -0
  139. package/src/inputs/suggestions/suggestion_list.stories.tsx +12 -8
  140. package/src/inputs/suggestions/suggestion_list.tsx +24 -3
  141. package/src/inputs/textarea/textarea.tsx +1 -1
  142. package/src/inputs/unit_input/unit_input.tsx +17 -17
  143. package/src/overlay/frame/frame.module.css +2 -4
  144. package/src/overlay/frame/frame.stories.tsx +13 -10
  145. package/src/overlay/frame/frame.tsx +123 -15
  146. package/src/overlay/popper/base/dismissal_decorator.tsx +1 -1
  147. package/src/overlay/popper/legacy/popper.tsx +5 -1
  148. package/src/stacks/box/bottom_resize_handle.tsx +6 -1
  149. package/src/stacks/box/box.tsx +12 -2
  150. package/src/stacks/box/end_resize_handle.tsx +6 -1
  151. package/src/stacks/box/left_resize_handle.tsx +6 -1
  152. package/src/stacks/box/resize_handlers.ts +20 -8
  153. package/src/stacks/box/right_resize_handle.tsx +6 -1
  154. package/src/stacks/box/start_resize_handle.tsx +7 -2
  155. package/src/stacks/box/top_resize_handle.tsx +7 -2
  156. package/src/stacks/h_collapsible_box.tsx +2 -2
  157. package/src/stacks/v_collapsible_box.tsx +2 -2
  158. package/src/surfaces/window/window.tsx +14 -4
  159. package/src/themes/stories/controls_fieldset.tsx +1 -1
  160. package/src/themes/stylesheets/reset.css +8 -1
  161. package/src/themes/themes/ergo/ergo_theme.css +183 -18
  162. package/src/typography/body_text/body_text.tsx +2 -0
  163. package/src/utils/dnd/__stories__/draggable.stories.tsx +14 -8
  164. package/src/utils/dnd/hooks/use_drag_container.ts +13 -3
  165. package/src/inputs/phone_number_input/__tests__/utils.test.ts +0 -52
@@ -0,0 +1,147 @@
1
+ import React, { useLayoutEffect } from 'react';
2
+ import { NotebookIcon } from '@tcn/icons/notebook_icon.js';
3
+ import { Input } from '../input/input.js';
4
+ import { Button } from '../../actions/index.js';
5
+ import { InputGroup } from '../input_group/input_group.js';
6
+ import clsx from 'clsx';
7
+ import { usePhoneContext } from './phone_number_context.js';
8
+ import type { CountryCode } from 'libphonenumber-js';
9
+ import {
10
+ PhoneNumberCountrySelectAdapter,
11
+ type CountryOption,
12
+ } from './phone_number_country_select_adapter.js';
13
+ import { SuggestionList } from '../suggestions/suggestion_list.js';
14
+
15
+ export interface SipInputProps {
16
+ onChange: (value: string) => void;
17
+ countries?: CountryCode[];
18
+ disabled?: boolean;
19
+ name?: string;
20
+ 'aria-label'?: string;
21
+ autoFocus?: boolean;
22
+ placeholder?: string;
23
+ }
24
+
25
+ export function SipInput({
26
+ disabled,
27
+ countries,
28
+ name,
29
+ 'aria-label': ariaLabel,
30
+ autoFocus,
31
+ placeholder,
32
+ }: SipInputProps) {
33
+ const inputRef = React.useRef<HTMLInputElement>(null);
34
+ const [phoneBookElement, setPhoneBookElement] =
35
+ React.useState<HTMLButtonElement | null>(null);
36
+ const isPhoneBookOpen = phoneBookElement != null;
37
+
38
+ const {
39
+ phoneBook: phoneBookOptions,
40
+ setValue,
41
+ setCountry,
42
+ ariaPhoneBookButtonLabel,
43
+ sipAddress,
44
+ setSipAddress,
45
+ focusNumberInput,
46
+ setFocusNumberInput,
47
+ } = usePhoneContext();
48
+
49
+ const showPhoneBook = phoneBookOptions.length > 0;
50
+
51
+ function togglePhoneBook(e: React.MouseEvent<HTMLButtonElement>) {
52
+ if (isPhoneBookOpen) {
53
+ setPhoneBookElement(null);
54
+ } else {
55
+ setPhoneBookElement(e.currentTarget);
56
+ }
57
+ }
58
+
59
+ function closePhoneBook() {
60
+ setPhoneBookElement(null);
61
+ }
62
+
63
+ function handlePhoneBookOptionSelect(value: string) {
64
+ closePhoneBook();
65
+ setFocusNumberInput(true);
66
+ setValue(value);
67
+ }
68
+
69
+ const options: CountryOption[] =
70
+ countries?.map(country => ({
71
+ value: country,
72
+ label: country,
73
+ })) || [];
74
+
75
+ function selectCountry(countryCode?: string) {
76
+ if (countryCode !== 'SIP') {
77
+ setSipAddress(sipAddress);
78
+ setFocusNumberInput(true);
79
+ setValue('');
80
+ setCountry((countryCode as CountryCode) || 'US');
81
+ }
82
+ }
83
+
84
+ function updateSipValue(value: string) {
85
+ setSipAddress(value);
86
+ setValue(`sip:${value}`);
87
+ }
88
+
89
+ useLayoutEffect(() => {
90
+ const input = inputRef.current;
91
+
92
+ if (input == null || !focusNumberInput) {
93
+ return;
94
+ }
95
+
96
+ requestAnimationFrame(() => {
97
+ if (input.value.length > 0) {
98
+ input.select();
99
+ } else {
100
+ input.focus();
101
+ }
102
+ });
103
+ }, [focusNumberInput]);
104
+
105
+ return (
106
+ <>
107
+ <InputGroup>
108
+ <PhoneNumberCountrySelectAdapter
109
+ value="SIP"
110
+ onChange={selectCountry}
111
+ options={options}
112
+ disabled={disabled}
113
+ />
114
+ <Input
115
+ ref={inputRef}
116
+ value={sipAddress}
117
+ disabled={disabled}
118
+ onChange={updateSipValue}
119
+ name={name}
120
+ aria-label={ariaLabel}
121
+ autoFocus={autoFocus}
122
+ placeholder={placeholder}
123
+ />
124
+ {showPhoneBook && (
125
+ <Button
126
+ disabled={disabled}
127
+ className={clsx('tcn-input-group-slot', 'tcn-phone-number-phone-book')}
128
+ aria-label={ariaPhoneBookButtonLabel}
129
+ onClick={togglePhoneBook}
130
+ size="md"
131
+ >
132
+ <NotebookIcon size="md" />
133
+ </Button>
134
+ )}
135
+ </InputGroup>
136
+ <SuggestionList
137
+ open={isPhoneBookOpen}
138
+ anchorElement={phoneBookElement}
139
+ onOptionSelect={handlePhoneBookOptionSelect}
140
+ onClose={closePhoneBook}
141
+ noSuggestionMessage="No phone numbers found"
142
+ >
143
+ {phoneBookOptions}
144
+ </SuggestionList>
145
+ </>
146
+ );
147
+ }
@@ -130,7 +130,7 @@ export const Select = React.forwardRef(function Select(
130
130
  <>
131
131
  <Button
132
132
  ref={forkedRef}
133
- className={clsx(className, 'tcn-select', styles.select)}
133
+ className={clsx(className, 'tcn-select', 'tcn-control', styles.select)}
134
134
  width="100%"
135
135
  {...props}
136
136
  hAlign="start"
@@ -144,19 +144,18 @@ export const Select = React.forwardRef(function Select(
144
144
  {selectedLabel}
145
145
  </span>
146
146
  </Button>
147
- {isSuggestionsOpen && (
148
- <SuggestionList
149
- anchorElement={anchorElement}
150
- onClose={handleClose}
151
- value={''}
152
- scrollToValue={value || ''}
153
- trimCustomInput={trimCustomInput}
154
- onOptionSelect={handleSelection}
155
- noSuggestionMessage={noOptionMatchedMessage}
156
- >
157
- {children}
158
- </SuggestionList>
159
- )}
147
+ <SuggestionList
148
+ open={isSuggestionsOpen}
149
+ anchorElement={anchorElement}
150
+ onClose={handleClose}
151
+ value={''}
152
+ scrollToValue={value || ''}
153
+ trimCustomInput={trimCustomInput}
154
+ onOptionSelect={handleSelection}
155
+ noSuggestionMessage={noOptionMatchedMessage}
156
+ >
157
+ {children}
158
+ </SuggestionList>
160
159
  </>
161
160
  );
162
161
  });
@@ -6,6 +6,7 @@
6
6
  overflow-y: auto;
7
7
  overflow-x: hidden;
8
8
  background-color: #fff;
9
+ overscroll-behavior: none;
9
10
  }
10
11
 
11
12
  :where(.input) {
@@ -34,13 +34,14 @@ export const SuggestionList: Story = {
34
34
  ref={setAnchorElement}
35
35
  value={value}
36
36
  onChange={e => setValue(e.target.value)}
37
- onFocus={() => setIsOpen(true)}
37
+ onClick={() => setIsOpen(true)}
38
38
  placeholder="Search fruits..."
39
39
  style={{ width: '100%', padding: '8px' }}
40
40
  />
41
- {isOpen && anchorElement && (
41
+ {anchorElement && (
42
42
  <SuggestionListComponent
43
43
  value={value}
44
+ open={isOpen}
44
45
  anchorElement={anchorElement}
45
46
  onChange={setValue}
46
47
  onOptionSelect={selectedValue => {
@@ -95,13 +96,14 @@ export const WithCustomContent: Story = {
95
96
  ref={setAnchorElement}
96
97
  value={label}
97
98
  onChange={e => setLabel(e.target.value)}
98
- onFocus={() => setIsOpen(true)}
99
+ onClick={() => setIsOpen(true)}
99
100
  placeholder="Search cities..."
100
101
  style={{ width: '100%', padding: '8px' }}
101
102
  />
102
- {isOpen && anchorElement && (
103
+ {anchorElement && (
103
104
  <SuggestionListComponent
104
105
  value={label}
106
+ open={isOpen}
105
107
  anchorElement={anchorElement}
106
108
  onChange={setLabel}
107
109
  onOptionSelect={(_, label) => {
@@ -160,11 +162,11 @@ export const WithDisabledOptions: Story = {
160
162
  ref={setAnchorElement}
161
163
  value={value}
162
164
  onChange={e => setValue(e.target.value)}
163
- onFocus={() => setIsOpen(true)}
165
+ onClick={() => setIsOpen(true)}
164
166
  placeholder="Search fruits..."
165
167
  style={{ width: '100%', padding: '8px' }}
166
168
  />
167
- {isOpen && anchorElement && (
169
+ {anchorElement && (
168
170
  <SuggestionListComponent
169
171
  value={value}
170
172
  anchorElement={anchorElement}
@@ -173,6 +175,7 @@ export const WithDisabledOptions: Story = {
173
175
  setValue(selectedValue);
174
176
  setIsOpen(false);
175
177
  }}
178
+ open={isOpen}
176
179
  onClose={() => setIsOpen(false)}
177
180
  >
178
181
  <Option value="apple" label="Apple" keywords={['fruit', 'red', 'sweet']}>
@@ -333,13 +336,14 @@ export const PerformanceTest: Story = {
333
336
  ref={setAnchorElement}
334
337
  value={label}
335
338
  onChange={e => setLabel(e.target.value)}
336
- onFocus={() => setIsOpen(true)}
339
+ onClick={() => setIsOpen(true)}
337
340
  placeholder="Search 1000 random items..."
338
341
  style={{ width: '100%', padding: '8px' }}
339
342
  />
340
- {isOpen && anchorElement && (
343
+ {anchorElement && (
341
344
  <SuggestionListComponent
342
345
  value={label}
346
+ open={isOpen}
343
347
  anchorElement={anchorElement}
344
348
  onChange={setLabel}
345
349
  onOptionSelect={(_, label) => {
@@ -18,8 +18,10 @@ const BATCH_SIZE = 50;
18
18
  export interface SuggestionListProps
19
19
  extends Omit<React.HTMLAttributes<HTMLInputElement>, 'onChange'> {
20
20
  value?: string;
21
+ initialSearchValue?: string;
21
22
  scrollToValue?: string;
22
23
  anchorElement: HTMLElement | null;
24
+ open?: boolean;
23
25
  children?: React.ReactNode;
24
26
  onChange?: (value: string) => void;
25
27
  onOptionSelect?: (
@@ -31,6 +33,7 @@ export interface SuggestionListProps
31
33
  noSuggestionMessage?: React.ReactNode;
32
34
  trimCustomInput?: boolean;
33
35
  haveValueAsOption?: boolean;
36
+ restoreFocus?: boolean;
34
37
  onClose?: (
35
38
  inputValue: string,
36
39
  cursorStartPosition: number | null,
@@ -40,8 +43,10 @@ export interface SuggestionListProps
40
43
 
41
44
  export function SuggestionList({
42
45
  value = '',
46
+ initialSearchValue: searchValue,
43
47
  scrollToValue,
44
48
  anchorElement,
49
+ open = false,
45
50
  children,
46
51
  onOptionSelect,
47
52
  noSuggestionMessage = '-- No Matches --',
@@ -51,6 +56,7 @@ export function SuggestionList({
51
56
  onKeyDown,
52
57
  trimCustomInput = false,
53
58
  haveValueAsOption = false,
59
+ restoreFocus = true,
54
60
  ...props
55
61
  }: SuggestionListProps) {
56
62
  // Extract valid Option components from children
@@ -70,6 +76,7 @@ export function SuggestionList({
70
76
  });
71
77
 
72
78
  const keyPressedDownRef = useRef('');
79
+ const KeyDownRegistered = useRef(false);
73
80
  const [maxResults, setMaxResults] = useState(MAX_RESULTS);
74
81
  const [focusedIndex, setFocusedIndex] = useState(selectedIndex);
75
82
  const [internalValue, setInternalValue] = useState(value);
@@ -82,6 +89,11 @@ export function SuggestionList({
82
89
  const [suggestionsHeight, setSuggestionsHeight] = useState<string | undefined>();
83
90
 
84
91
  function handleKeyUp(event: React.KeyboardEvent<HTMLInputElement>) {
92
+ if (!KeyDownRegistered.current) {
93
+ return;
94
+ }
95
+ KeyDownRegistered.current = false;
96
+
85
97
  const key = event.key;
86
98
  const input = event.currentTarget;
87
99
 
@@ -156,7 +168,7 @@ export function SuggestionList({
156
168
  function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
157
169
  const key = event.key;
158
170
  keyPressedDownRef.current = key;
159
-
171
+ KeyDownRegistered.current = true;
160
172
  // We handle these key events on keydown to be responsive navigation.
161
173
  switch (key) {
162
174
  case 'ArrowDown': {
@@ -339,12 +351,16 @@ export function SuggestionList({
339
351
  useLayoutEffect(() => {
340
352
  const input = internalInputRef.current;
341
353
 
354
+ if (!open) {
355
+ return;
356
+ }
357
+
342
358
  if (input != null && input.value.length > 0) {
343
359
  input.select();
344
360
  } else if (input != null) {
345
361
  input.focus();
346
362
  }
347
- }, []);
363
+ }, [open]);
348
364
 
349
365
  useLayoutEffect(() => {
350
366
  if (anchorElement != null) {
@@ -373,14 +389,19 @@ export function SuggestionList({
373
389
  setFocusedIndex(internalValue === '' ? selectedIndex : -1);
374
390
  }, [internalValue, selectedIndex]);
375
391
 
392
+ useLayoutEffect(() => {
393
+ setInternalValue(searchValue ?? '');
394
+ }, [searchValue]);
395
+
376
396
  return (
377
397
  <Popper
378
- open
398
+ open={open}
379
399
  anchorElement={anchorElement}
380
400
  onClose={handleUseClose}
381
401
  verticalAnchor="top"
382
402
  verticalOrigin="top"
383
403
  verticalOffset={-4}
404
+ restoreFocus={restoreFocus}
384
405
  >
385
406
  <VStack
386
407
  minHeight={`calc(${suggestionsHeight}, 8px)`}
@@ -20,7 +20,7 @@ export const Textarea = React.forwardRef(function Textarea(
20
20
  return (
21
21
  <textarea
22
22
  style={{ width, height, ...style }}
23
- className={clsx(className, styles.textarea, 'tcn-textarea')}
23
+ className={clsx(className, styles.textarea, 'tcn-textarea', 'tcn-control')}
24
24
  data-is-disabled={props.disabled || false}
25
25
  ref={ref}
26
26
  onChange={e => {
@@ -1,13 +1,13 @@
1
1
  import React, { useRef } from 'react';
2
2
  import { clsx } from 'clsx';
3
3
  import { useForkRef } from '../../utils/index.js';
4
- import { HStack, type HStackProps } from '../../stacks/h_stack.js';
4
+ import { type HStackProps } from '../../stacks/h_stack.js';
5
5
  import { Input } from '../input/input.js';
6
6
  import { Select } from '../select/select.js';
7
7
  import { OptionProps } from '../options/option.js';
8
8
  import styles from './unit_input.module.css';
9
9
 
10
- import { ZStack } from '../../stacks/z_stack.js';
10
+ import { InputGroup } from '../input_group/input_group.js';
11
11
 
12
12
  function getDisplayValue(value: string) {
13
13
  return value
@@ -80,25 +80,25 @@ export const UnitInput = React.forwardRef(function UnitInput(
80
80
  }
81
81
 
82
82
  return (
83
- <HStack
83
+ <InputGroup
84
84
  ref={ref}
85
85
  className={clsx(styles['unit-input'], 'tcn-unit-input')}
86
86
  height="auto"
87
87
  {...props}
88
88
  >
89
- <ZStack width="flex">
90
- <Input
91
- className={clsx(styles['unit-input-number'], 'tcn-unit-input-number')}
92
- ref={forkedInputRef}
93
- onChange={valueHandler}
94
- disabled={disabled}
95
- style={{
96
- borderEndEndRadius: 0,
97
- borderStartEndRadius: 0,
98
- textAlign: 'start',
99
- }}
100
- />
101
- </ZStack>
89
+ <Input
90
+ width="flex"
91
+ className={clsx(styles['unit-input-number'], 'tcn-unit-input-number')}
92
+ ref={forkedInputRef}
93
+ onChange={valueHandler}
94
+ disabled={disabled}
95
+ style={{
96
+ borderEndEndRadius: 0,
97
+ borderStartEndRadius: 0,
98
+ textAlign: 'start',
99
+ }}
100
+ />
101
+
102
102
  <Select
103
103
  className={clsx(styles['unit-input-select'], 'tcn-unit-input-select')}
104
104
  ref={unitRef}
@@ -110,6 +110,6 @@ export const UnitInput = React.forwardRef(function UnitInput(
110
110
  >
111
111
  {children}
112
112
  </Select>
113
- </HStack>
113
+ </InputGroup>
114
114
  );
115
115
  });
@@ -1,7 +1,5 @@
1
1
  @layer tcn-system {
2
- :where(.frame) {
3
- padding: 0;
4
- border: none;
5
- background: none;
2
+ :where(.frame-dialog) {
3
+ overflow: hidden;
6
4
  }
7
5
  }
@@ -1,9 +1,9 @@
1
+ import { Header, Scaffold } from '../../layouts/index.js';
1
2
  import { ZStack } from '../../stacks/z_stack.js';
2
- import { Frame, type FrameOwnProps } from './frame.js';
3
- import { DragHandle } from '../../utils/dnd/handle.js';
4
- import { Title } from '../../typography/title/title.js';
5
3
  import { BodyText } from '../../typography/index.js';
6
- import { Header } from '../../layouts/index.js';
4
+ import { Title } from '../../typography/title/title.js';
5
+ import { DragHandle } from '../../utils/dnd/handle.js';
6
+ import { Frame, type FrameOwnProps } from './frame.js';
7
7
  import styles from './frame_stories.module.css';
8
8
  export default {
9
9
  title: 'Overlays/Floating/Frame',
@@ -14,6 +14,7 @@ export default {
14
14
  isOpen: true,
15
15
  draggable: true,
16
16
  veil: false,
17
+ resizable: true,
17
18
  },
18
19
  };
19
20
 
@@ -26,14 +27,16 @@ export const FrameStory = (args: Omit<FrameOwnProps, 'children'>) => {
26
27
  className={styles['sb-frame-container']}
27
28
  {...args}
28
29
  >
29
- <Header className={styles['sb-frame-header']}>
30
- <Title> This is a frame</Title>
31
- </Header>
32
- <DragHandle>
30
+ <Scaffold>
33
31
  <Header className={styles['sb-frame-header']}>
34
- <BodyText> You can drag here.</BodyText>
32
+ <Title> This is a frame</Title>
35
33
  </Header>
36
- </DragHandle>
34
+ <DragHandle>
35
+ <Header className={styles['sb-frame-header']}>
36
+ <BodyText> You can drag here.</BodyText>
37
+ </Header>
38
+ </DragHandle>
39
+ </Scaffold>
37
40
  </Frame>
38
41
  </ZStack>
39
42
  );
@@ -1,50 +1,158 @@
1
- import React from 'react';
2
- import { ZStack, type ZStackProps } from '../../stacks/index.js';
1
+ import { clsx } from 'clsx';
2
+ import React, { useCallback } from 'react';
3
+ import { flushSync } from 'react-dom';
4
+ import { Box, ZStack, type BoxProps } from '../../stacks/index.js';
5
+ import { useDragContainer } from '../../utils/dnd/context.js';
3
6
  import { Draggable } from '../../utils/dnd/draggable/draggable.js';
4
7
  import { Portal } from '../portal/portal.js';
5
8
 
6
9
  // Styles
7
- import { clsx } from 'clsx';
8
10
  import styles from './frame.module.css';
9
-
10
11
  export interface FrameOwnProps {
11
12
  isOpen?: boolean;
12
- children?: React.ReactNode;
13
13
  draggable?: boolean;
14
14
  veil?: boolean;
15
+ resizable?: boolean;
15
16
  }
16
17
 
17
- export type FrameProps = ZStackProps & FrameOwnProps;
18
+ export type FrameProps = Omit<
19
+ BoxProps,
20
+ | 'enableResizeOnLeft'
21
+ | 'enableResizeOnRight'
22
+ | 'enableResizeOnTop'
23
+ | 'enableResizeOnBottom'
24
+ > &
25
+ FrameOwnProps;
18
26
 
19
- export const Frame = React.forwardRef<HTMLDialogElement, FrameProps>(function Frame(
27
+ export const Frame = React.forwardRef<HTMLElement, FrameProps>(function Frame(
20
28
  {
21
29
  children,
22
30
  isOpen = false,
23
31
  draggable = true,
24
32
  veil = false,
33
+ resizable = true,
25
34
  className,
26
35
  ...rest
27
36
  }: FrameProps,
28
37
  ref
29
38
  ) {
30
- if (!isOpen) {
31
- return null;
32
- }
33
-
39
+ if (!isOpen) return null;
34
40
  return (
35
41
  <Portal>
36
42
  <ZStack width="100%" height="100%" data-is-veil={veil} className="tcn-frame-veil">
37
43
  <Draggable draggable={draggable}>
38
- <ZStack
39
- as="dialog"
44
+ <FrameDialog
45
+ className={className}
40
46
  ref={ref}
41
- className={clsx(styles['frame'], 'tcn-frame', className)}
47
+ enableResizeOnLeft={resizable}
48
+ enableResizeOnRight={resizable}
49
+ enableResizeOnTop={resizable}
50
+ enableResizeOnBottom={resizable}
51
+ draggable={draggable}
42
52
  {...rest}
43
53
  >
44
54
  {children}
45
- </ZStack>
55
+ </FrameDialog>
46
56
  </Draggable>
47
57
  </ZStack>
48
58
  </Portal>
49
59
  );
50
60
  });
61
+ interface FrameDialogProps extends BoxProps {}
62
+
63
+ export const FrameDialog = React.forwardRef<HTMLElement, FrameDialogProps>(
64
+ function FrameDialog(
65
+ {
66
+ as = 'div',
67
+ role = 'dialog',
68
+ children,
69
+ className,
70
+ draggable,
71
+ onWidthResize,
72
+ onHeightResize,
73
+ ...rest
74
+ }: FrameDialogProps,
75
+ ref: React.Ref<HTMLElement>
76
+ ) {
77
+ const drag = useDragContainer();
78
+
79
+ const handleWidthResize = React.useCallback(
80
+ (
81
+ width: number,
82
+ origin: 'left' | 'right',
83
+ totalDelta: number,
84
+ currentDelta: number
85
+ ) => {
86
+ if (!draggable) {
87
+ return;
88
+ }
89
+ // TODO: add clamp logic
90
+ if (origin === 'right') {
91
+ flushSync(() => {
92
+ drag.setPosition(prev => ({
93
+ x: prev.x + currentDelta / 2,
94
+ y: prev.y,
95
+ }));
96
+ });
97
+ }
98
+ if (origin === 'left') {
99
+ flushSync(() => {
100
+ drag.setPosition(prev => ({
101
+ x: prev.x - currentDelta / 2,
102
+ y: prev.y,
103
+ }));
104
+ });
105
+ }
106
+
107
+ onWidthResize?.(width, origin, totalDelta, currentDelta);
108
+ },
109
+ [onWidthResize, drag, draggable]
110
+ );
111
+
112
+ const handleHeightResize = useCallback(
113
+ (
114
+ height: number,
115
+ origin: 'top' | 'bottom',
116
+ totalDelta: number,
117
+ currentDelta: number
118
+ ) => {
119
+ if (!draggable) {
120
+ return;
121
+ }
122
+ // TODO: add clamp logic
123
+ if (origin === 'bottom') {
124
+ flushSync(() => {
125
+ drag.setPosition(prev => ({
126
+ x: prev.x,
127
+ y: prev.y + currentDelta / 2,
128
+ }));
129
+ });
130
+ }
131
+ if (origin === 'top') {
132
+ flushSync(() => {
133
+ drag.setPosition(prev => ({
134
+ x: prev.x,
135
+ y: prev.y - currentDelta / 2,
136
+ }));
137
+ });
138
+ }
139
+ onHeightResize?.(height, origin, totalDelta, currentDelta);
140
+ },
141
+ [onHeightResize, drag, draggable]
142
+ );
143
+
144
+ return (
145
+ <Box
146
+ className={clsx(styles['frame-dialog'], 'tcn-frame-dialog', className)}
147
+ ref={ref}
148
+ onWidthResize={handleWidthResize}
149
+ onHeightResize={handleHeightResize}
150
+ as={as}
151
+ role={role}
152
+ {...rest}
153
+ >
154
+ {children}
155
+ </Box>
156
+ );
157
+ }
158
+ );
@@ -36,7 +36,7 @@ export const PopperDismissalDecorator = forwardRef<
36
36
 
37
37
  function buildExceptionHandler(dismissal: PopperDismissal) {
38
38
  if (dismissal in dismissals) {
39
- return target => isException?.(dismissal, target) ?? false;
39
+ return (target: HTMLElement) => isException?.(dismissal, target) ?? false;
40
40
  }
41
41
  return () => false;
42
42
  }