@inceptionbg/iui 2.0.20 → 2.0.21

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.
@@ -0,0 +1,258 @@
1
+ import './select.scss';
2
+
3
+ import { useState, useRef, useEffect, ChangeEvent, FC, UIEvent } from 'react';
4
+ import clsx from 'clsx';
5
+ import { InputWrapper } from '../InputWrapper';
6
+ import { createPortal } from 'react-dom';
7
+ import { rootDir } from '../../../utils/rootDir';
8
+ import { useMenuPosition } from '../../Menu/hooks/useMenuPosition';
9
+ import { IMenuPlacement } from '../../../types/IMenu';
10
+
11
+ export interface OptionType {
12
+ label: string;
13
+ value: any;
14
+ }
15
+
16
+ // interface LoadOptionsParams {
17
+ // inputValue: string;
18
+ // loadedOptions: OptionType[];
19
+ // page: number;
20
+ // }
21
+
22
+ interface LoadOptionsResponse {
23
+ options: OptionType[];
24
+ hasMore: boolean;
25
+ }
26
+
27
+ interface CustomSelectProps {
28
+ label: string;
29
+ value?: any;
30
+ setValue: (option: OptionType | null) => void;
31
+ required?: boolean;
32
+ disabled?: boolean;
33
+ isMulti?: boolean;
34
+ isAsync?: boolean;
35
+ isCreatable?: boolean;
36
+ options?: OptionType[];
37
+ placeholder?: string;
38
+ loadOptions?: (
39
+ inputValue: string,
40
+ loadedOptions: OptionType[],
41
+ params: { page: number }
42
+ ) => Promise<LoadOptionsResponse>;
43
+ onCreateOption?: (inputValue: string) => Promise<OptionType>;
44
+ isClearable: boolean;
45
+ onClearInput?: () => void;
46
+ helperText?: string;
47
+ errorText?: string;
48
+ error?: boolean;
49
+ menuPlacement?: IMenuPlacement;
50
+ className?: string;
51
+ // minWidth?: number;
52
+ }
53
+
54
+ export const Select: FC<CustomSelectProps> = ({
55
+ label,
56
+ value,
57
+ setValue,
58
+ onCreateOption,
59
+ required,
60
+ disabled,
61
+ options = [],
62
+ loadOptions,
63
+ placeholder = 'Select...',
64
+ isMulti,
65
+ isAsync = false,
66
+ isCreatable = false,
67
+ isClearable = true,
68
+ onClearInput,
69
+ helperText,
70
+ errorText,
71
+ error,
72
+ menuPlacement = 'bottom-left',
73
+ className,
74
+ // minWidth,
75
+ }) => {
76
+ const [inputValue, setInputValue] = useState<string | null>(null);
77
+ const [dropdownVisible, setDropdownVisible] = useState(false);
78
+ const [filteredOptions, setFilteredOptions] = useState<OptionType[]>([]);
79
+ const [page, setPage] = useState(0);
80
+ const [index, setIndex] = useState<number | null>(null);
81
+ const [hasMore, setHasMore] = useState(true);
82
+
83
+ const containerRef = useRef<HTMLDivElement>(null);
84
+ const dropdownRef = useRef<HTMLDivElement>(null);
85
+
86
+ const menuStyle = useMenuPosition({
87
+ isOpen: dropdownVisible,
88
+ placement: menuPlacement,
89
+ containerRef,
90
+ menuRef: dropdownRef,
91
+ withMinWidth: true,
92
+ });
93
+
94
+ useEffect(() => {
95
+ if (!isAsync) {
96
+ const filtered = inputValue
97
+ ? options.filter(opt =>
98
+ opt.label.toLowerCase().includes(inputValue.toLowerCase())
99
+ )
100
+ : options;
101
+ setFilteredOptions(filtered);
102
+ }
103
+ }, [inputValue, options, isAsync]);
104
+
105
+ useEffect(() => {
106
+ if (isAsync && loadOptions) {
107
+ loadOptions(inputValue ?? '', filteredOptions, { page }).then(
108
+ ({ options: newOptions, hasMore }) => {
109
+ setFilteredOptions(prev => [...prev, ...newOptions]);
110
+ setHasMore(hasMore);
111
+ }
112
+ );
113
+ }
114
+ // eslint-disable-next-line react-hooks/exhaustive-deps
115
+ }, [page]);
116
+
117
+ const handleOptionClick = (option: OptionType) => {
118
+ setValue(option);
119
+ setInputValue(null);
120
+ setTimeout(() => {
121
+ setIndex(null);
122
+ // setDropdownVisible(false);
123
+ });
124
+ };
125
+
126
+ const handleInput = (e: ChangeEvent<HTMLInputElement>) => {
127
+ const inputValue = e.target.value;
128
+ setInputValue(
129
+ e.target.defaultValue === value?.label
130
+ ? // @ts-ignore
131
+ (e.nativeEvent?.data ?? '')
132
+ : inputValue
133
+ );
134
+ // setFilteredOptions([]);
135
+ setPage(0);
136
+ if (isAsync && loadOptions) {
137
+ loadOptions(inputValue, [], { page: 0 }).then(({ options, hasMore }) => {
138
+ setFilteredOptions(options);
139
+ setHasMore(hasMore);
140
+ });
141
+ }
142
+ };
143
+
144
+ const handleScroll = (e: UIEvent<HTMLDivElement>) => {
145
+ const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
146
+ if (scrollHeight - scrollTop === clientHeight && hasMore) {
147
+ setPage(prev => prev + 1);
148
+ }
149
+ };
150
+
151
+ const handleCreate = async () => {
152
+ if (onCreateOption && inputValue?.trim()) {
153
+ const newOption = await onCreateOption(inputValue);
154
+ setFilteredOptions(prev => [newOption, ...prev]);
155
+ setValue(newOption);
156
+ setInputValue(null);
157
+ setDropdownVisible(false);
158
+ }
159
+ };
160
+
161
+ return (
162
+ <div
163
+ className="customSelect"
164
+ onFocus={() => setDropdownVisible(true)}
165
+ onBlur={() => {
166
+ setDropdownVisible(false);
167
+ setInputValue(null);
168
+ }}
169
+ >
170
+ <InputWrapper
171
+ label={label}
172
+ required={required}
173
+ disabled={disabled}
174
+ // endText={endText}
175
+ // endButton={endButton}
176
+ onClearInput={
177
+ isClearable && value
178
+ ? () => {
179
+ setValue(null);
180
+ setInputValue('');
181
+ // setSelectedOption(null);
182
+ }
183
+ : undefined
184
+ }
185
+ helperText={helperText}
186
+ errorText={errorText}
187
+ error={error}
188
+ inputFieldRef={containerRef}
189
+ className={className}
190
+ >
191
+ <input
192
+ value={inputValue ?? value?.label ?? ''}
193
+ onChange={handleInput}
194
+ required={required}
195
+ disabled={disabled}
196
+ // onFocus={e => e.currentTarget.select()}
197
+ // autoFocus={autoFocus}
198
+ placeholder={value?.label || placeholder}
199
+ onKeyDown={e => {
200
+ if (e.key === 'ArrowUp') {
201
+ e.preventDefault();
202
+ if (index === null || index === 0) {
203
+ setIndex(filteredOptions.length - 1);
204
+ } else {
205
+ setIndex(prev => prev! - 1);
206
+ }
207
+ } else if (e.key === 'ArrowDown') {
208
+ e.preventDefault();
209
+ if (index === null || index === filteredOptions.length - 1) {
210
+ setIndex(0);
211
+ } else {
212
+ setIndex(prev => prev! + 1);
213
+ dropdownRef.current?.scrollTo({ top: 20 * ((index ?? 0) + 1) });
214
+ }
215
+ } else if (e.key === 'Enter' && index !== null) {
216
+ e.preventDefault();
217
+ e.currentTarget.blur();
218
+ handleOptionClick(filteredOptions[index]);
219
+ } else if (e.key === 'Escape') {
220
+ e.preventDefault();
221
+ e.currentTarget.blur();
222
+ }
223
+ }}
224
+ />
225
+ </InputWrapper>
226
+
227
+ {dropdownVisible
228
+ ? createPortal(
229
+ <div
230
+ ref={dropdownRef}
231
+ className="select-dropdown"
232
+ onScroll={handleScroll}
233
+ style={menuStyle}
234
+ >
235
+ {filteredOptions.map((option, i) => (
236
+ <div
237
+ key={i}
238
+ className={clsx('option', { hover: index === i })}
239
+ onMouseDown={() => handleOptionClick(option)}
240
+ >
241
+ {option.label}
242
+ </div>
243
+ ))}
244
+ {!filteredOptions.length && <div className="option">No options</div>}
245
+ {isCreatable &&
246
+ inputValue &&
247
+ !filteredOptions.some(opt => opt.label === inputValue) && (
248
+ <div className="option createOption" onClick={handleCreate}>
249
+ {`Create ${inputValue}`}
250
+ </div>
251
+ )}
252
+ </div>,
253
+ rootDir
254
+ )
255
+ : null}
256
+ </div>
257
+ );
258
+ };
@@ -0,0 +1,42 @@
1
+ .customSelect {
2
+ position: relative;
3
+ width: 300px;
4
+ font-family: sans-serif;
5
+
6
+ .inputWrapper {
7
+ input {
8
+ width: 100%;
9
+ padding: 8px 12px;
10
+ border: 1px solid #ccc;
11
+ border-radius: 4px;
12
+ font-size: 14px;
13
+ }
14
+ }
15
+ }
16
+
17
+ .select-dropdown {
18
+ position: absolute;
19
+ z-index: 1000;
20
+ // width: 100%;
21
+ min-width: 200px;
22
+ max-height: 200px;
23
+ overflow-y: auto;
24
+ background: #fff;
25
+ border: var(--border);
26
+ border-radius: 8px;
27
+ // border-radius: 0 0 8px 8px;
28
+ margin-top: 2px;
29
+ .option {
30
+ padding: 8px 12px;
31
+ cursor: pointer;
32
+
33
+ &.hover,
34
+ &:hover {
35
+ background-color: #f5f5f5;
36
+ }
37
+ }
38
+ .createOption {
39
+ font-weight: bold;
40
+ color: #007bff;
41
+ }
42
+ }
@@ -103,31 +103,98 @@ const setAbsolutePosition = (
103
103
  ) => {
104
104
  const containerRect = containerEl.getBoundingClientRect();
105
105
  const tooltipRect = targetEl.getBoundingClientRect();
106
+ const margin = 16; // Distance from container
107
+ const screenPadding = 8; // Minimum distance from screen edges
106
108
 
107
109
  if (position === 'bottom' || position === 'top') {
108
- const bottom = Math.floor(containerRect.bottom + 16);
109
- const top = Math.floor(containerRect.top - tooltipRect.height - 16);
110
- const leftCenter = Math.floor(
110
+ const bottom = Math.floor(containerRect.bottom + margin);
111
+ const top = Math.floor(containerRect.top - tooltipRect.height - margin);
112
+ let leftCenter = Math.floor(
111
113
  containerRect.left + containerRect.width / 2 - tooltipRect.width / 2
112
114
  );
115
+
116
+ // Check if tooltip overflows horizontally and adjust
117
+ if (leftCenter < screenPadding) {
118
+ targetEl.style.width = `${tooltipRect.width + leftCenter}px`;
119
+ leftCenter = screenPadding;
120
+ } else if (leftCenter + tooltipRect.width > window.innerWidth - screenPadding) {
121
+ leftCenter = window.innerWidth - tooltipRect.width - screenPadding;
122
+ }
123
+
113
124
  const reverse =
114
125
  position === 'top' ? top < 0 : bottom + tooltipRect.height > window.innerHeight;
115
126
 
116
127
  setNewPosition(reverse ? (position === 'bottom' ? 'top' : 'bottom') : '');
117
128
 
118
- targetEl.style.top = `${
119
- position === 'bottom' ? (reverse ? top : bottom) : reverse ? bottom : top
120
- }px`;
129
+ let finalTop =
130
+ position === 'bottom' ? (reverse ? top : bottom) : reverse ? bottom : top;
131
+
132
+ // Ensure tooltip doesn't go above screen
133
+ if (finalTop < screenPadding) {
134
+ finalTop = screenPadding;
135
+ }
136
+ // Ensure tooltip doesn't go below screen
137
+ if (finalTop + tooltipRect.height > window.innerHeight - screenPadding) {
138
+ finalTop = window.innerHeight - tooltipRect.height - screenPadding;
139
+ }
140
+
141
+ targetEl.style.top = `${finalTop}px`;
121
142
  targetEl.style.left = `${leftCenter}px`;
122
143
  } else if (position === 'right') {
123
- targetEl.style.top =
124
- Math.floor(containerRect.top + containerRect.height / 2 - tooltipRect.height / 2) +
125
- 'px';
126
- targetEl.style.left = Math.floor(containerRect.right + 16) + 'px';
144
+ let finalTop = Math.floor(
145
+ containerRect.top + containerRect.height / 2 - tooltipRect.height / 2
146
+ );
147
+ let finalLeft = Math.floor(containerRect.right + margin);
148
+
149
+ // Check if tooltip overflows vertically and adjust
150
+ if (finalTop < screenPadding) {
151
+ finalTop = screenPadding;
152
+ } else if (finalTop + tooltipRect.height > window.innerHeight - screenPadding) {
153
+ finalTop = window.innerHeight - tooltipRect.height - screenPadding;
154
+ }
155
+
156
+ // Check if tooltip overflows to the right and flip to left
157
+ if (finalLeft + tooltipRect.width > window.innerWidth - screenPadding) {
158
+ finalLeft = Math.floor(containerRect.left - tooltipRect.width - margin);
159
+ setNewPosition('left');
160
+
161
+ // If it still overflows on the left, position it at the edge
162
+ if (finalLeft < screenPadding) {
163
+ finalLeft = screenPadding;
164
+ }
165
+ } else {
166
+ setNewPosition('');
167
+ }
168
+
169
+ targetEl.style.top = `${finalTop}px`;
170
+ targetEl.style.left = `${finalLeft}px`;
127
171
  } else if (position === 'left') {
128
- targetEl.style.top =
129
- Math.floor(containerRect.top + containerRect.height / 2 - tooltipRect.height / 2) +
130
- 'px';
131
- targetEl.style.left = Math.floor(containerRect.left - tooltipRect.width - 16) + 'px';
172
+ let finalTop = Math.floor(
173
+ containerRect.top + containerRect.height / 2 - tooltipRect.height / 2
174
+ );
175
+ let finalLeft = Math.floor(containerRect.left - tooltipRect.width - margin);
176
+
177
+ // Check if tooltip overflows vertically and adjust
178
+ if (finalTop < screenPadding) {
179
+ finalTop = screenPadding;
180
+ } else if (finalTop + tooltipRect.height > window.innerHeight - screenPadding) {
181
+ finalTop = window.innerHeight - tooltipRect.height - screenPadding;
182
+ }
183
+
184
+ // Check if tooltip overflows to the left and flip to right
185
+ if (finalLeft < screenPadding) {
186
+ finalLeft = Math.floor(containerRect.right + margin);
187
+ setNewPosition('right');
188
+
189
+ // If it still overflows on the right, position it at the edge
190
+ if (finalLeft + tooltipRect.width > window.innerWidth - screenPadding) {
191
+ finalLeft = window.innerWidth - tooltipRect.width - screenPadding;
192
+ }
193
+ } else {
194
+ setNewPosition('');
195
+ }
196
+
197
+ targetEl.style.top = `${finalTop}px`;
198
+ targetEl.style.left = `${finalLeft}px`;
132
199
  }
133
200
  };
@@ -61,16 +61,18 @@ export const FormWrapper: FC<IFormWrapper> = ({
61
61
  variant={e.variant}
62
62
  color={e.color}
63
63
  onClick={e.onClick}
64
+ type="button"
64
65
  />
65
66
  )
66
67
  )}
67
68
  {!noAccess && (
68
69
  <Button
69
70
  label={submitButton.label ?? t('Save')}
70
- icon={submitButton.icon}
71
+ // icon={submitButton.icon}
71
72
  disabled={isLoading || submitButton.disabled}
72
73
  variant={submitButton.variant}
73
74
  color={submitButton.color}
75
+ type="submit"
74
76
  />
75
77
  )}
76
78
  </div>
@@ -28,6 +28,7 @@ export interface IPageLayoutProps {
28
28
  noAccess?: boolean;
29
29
  isInitiallyLoading?: boolean;
30
30
  isLoading?: boolean;
31
+ hideFooter?: boolean;
31
32
  children?: ReactNode;
32
33
  }
33
34
 
@@ -40,6 +41,7 @@ export const PageLayout: FC<IPageLayoutProps> = ({
40
41
  noAccess,
41
42
  isLoading,
42
43
  isInitiallyLoading,
44
+ hideFooter,
43
45
  children,
44
46
  }) => {
45
47
  const [isMoreActionsOpen, setIsMoreActionsOpen] = useState(false);
@@ -56,9 +58,11 @@ export const PageLayout: FC<IPageLayoutProps> = ({
56
58
  return null;
57
59
  }
58
60
 
59
- return isInitiallyLoading ? (
60
- <Loader isLoading isFullPage />
61
- ) : (
61
+ if (isInitiallyLoading) {
62
+ return <Loader isLoading isFullPage />;
63
+ }
64
+
65
+ return (
62
66
  <Loader isLoading={!!isLoading}>
63
67
  <div className="page-container">
64
68
  {breadcrumbs && (
@@ -138,9 +142,13 @@ export const PageLayout: FC<IPageLayoutProps> = ({
138
142
  </div>
139
143
  )}
140
144
  <div className="page-content">{children}</div>
141
- <footer className="page-footer">
142
- <Trans i18nKey="FooterText" />
143
- </footer>
145
+ {!hideFooter && (
146
+ <footer className="page-footer">
147
+ <p className="page-footer-text">
148
+ <Trans i18nKey="FooterText" />
149
+ </p>
150
+ </footer>
151
+ )}
144
152
  </div>
145
153
  </Loader>
146
154
  );
@@ -14,6 +14,9 @@
14
14
  .w-300 {
15
15
  width: 300px !important;
16
16
  }
17
+ .w-600 {
18
+ width: 600px !important;
19
+ }
17
20
 
18
21
  .full-height {
19
22
  height: 100% !important;
@@ -44,4 +44,7 @@
44
44
  height: 12px;
45
45
  width: 14px;
46
46
  }
47
+ .iui-accordion-content {
48
+ padding: 8px 24px 16px;
49
+ }
47
50
  }
@@ -32,14 +32,6 @@
32
32
  max-height: calc(100vh - var(--header-height));
33
33
  }
34
34
 
35
- .page-content {
36
- display: flex;
37
- flex-direction: column;
38
- padding: 1.5rem 1.5rem 1rem 1.5rem;
39
- flex: 1;
40
- overflow-y: auto;
41
- }
42
-
43
35
  .page-header {
44
36
  display: flex;
45
37
  justify-content: space-between;
@@ -57,9 +49,50 @@
57
49
  }
58
50
  }
59
51
 
52
+ .page-content {
53
+ display: flex;
54
+ flex-direction: column;
55
+ padding: 1.5rem 1.5rem 1rem 1.5rem;
56
+ flex: 1;
57
+ overflow-y: auto;
58
+ }
59
+ .scrollable-content {
60
+ display: flex;
61
+ flex-direction: column;
62
+ flex: 1;
63
+ overflow-y: auto;
64
+ margin-right: -1.5rem;
65
+ padding-right: 1.5rem;
66
+ padding-bottom: 1rem;
67
+ }
68
+
60
69
  .page-footer {
61
- text-align: center;
62
- padding: 16px;
63
- font-size: 12px;
64
- white-space: pre-line;
70
+ display: flex;
71
+ align-items: center;
72
+ justify-content: center;
73
+ gap: 16px;
74
+ padding: 16px 24px;
75
+ margin: 0 -1.5rem -1rem;
76
+ &.footer-actions {
77
+ box-shadow: var(--container-shadow);
78
+ z-index: 2;
79
+ }
80
+ .page-footer-text {
81
+ text-align: center;
82
+ font-size: 12px;
83
+ white-space: pre-line;
84
+ }
85
+ }
86
+
87
+ .page-footer-text {
88
+ display: flex;
89
+ align-items: center;
90
+ justify-content: center;
91
+ gap: 16px;
92
+ padding: 16px 24px;
93
+ .page-footer-text {
94
+ text-align: center;
95
+ font-size: 12px;
96
+ white-space: pre-line;
97
+ }
65
98
  }
@@ -94,6 +94,7 @@ table {
94
94
  }
95
95
  .clickable-cell:hover {
96
96
  background-color: var(--primary-100) !important;
97
+ cursor: pointer;
97
98
  }
98
99
  // Item Actions
99
100
  .table-item-actions {