goobs-frontend 0.9.10 → 0.9.11

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "goobs-frontend",
3
- "version": "0.9.10",
3
+ "version": "0.9.11",
4
4
  "type": "module",
5
5
  "description": "A comprehensive React-based libary that extends the functionality of Material-UI",
6
6
  "license": "MIT",
@@ -19,7 +19,7 @@ interface ColumnHeaderRowProps {
19
19
  // The entire columns array if we need them on mobile
20
20
  allColumns: ColumnDef[]
21
21
 
22
- // The chosen overflow column or mobile column
22
+ // The chosen "overflow" column or mobile column
23
23
  selectedOverflowField: string
24
24
  setSelectedOverflowField: React.Dispatch<React.SetStateAction<string>>
25
25
  }
@@ -77,7 +77,7 @@ const ColumnHeaderRow: React.FC<ColumnHeaderRowProps> = ({
77
77
  boxSizing: 'border-box',
78
78
  overflow: 'visible',
79
79
  position: 'relative',
80
- zIndex: 10,
80
+ zIndex: 100, // Increased z-index for mobile dropdown
81
81
  // If you want no left padding on mobile header as well:
82
82
  paddingLeft: 0,
83
83
  }}
@@ -93,6 +93,10 @@ const ColumnHeaderRow: React.FC<ColumnHeaderRowProps> = ({
93
93
  shrunkfontcolor="black"
94
94
  unshrunkfontcolor="black"
95
95
  shrunklabelposition="aboveNotch"
96
+ style={{
97
+ marginBottom: 0,
98
+ marginTop: 0,
99
+ }}
96
100
  />
97
101
  </TableCell>
98
102
  </TableRow>
@@ -133,12 +137,14 @@ const ColumnHeaderRow: React.FC<ColumnHeaderRowProps> = ({
133
137
  <TableCell
134
138
  key="overflow-header"
135
139
  sx={{
136
- width: 275,
140
+ width: 275, // Increased width for dropdown (was 200)
141
+ minWidth: 275,
137
142
  boxSizing: 'border-box',
138
143
  overflow: 'visible',
139
144
  position: 'relative',
140
- zIndex: 10,
145
+ zIndex: 100, // Increased z-index to ensure dropdown appears above other elements
141
146
  paddingLeft: 0, // <-- remove left padding here
147
+ height: '55px',
142
148
  }}
143
149
  >
144
150
  <SearchableDropdown
@@ -154,7 +160,11 @@ const ColumnHeaderRow: React.FC<ColumnHeaderRowProps> = ({
154
160
  inputfontcolor="black"
155
161
  shrunkfontcolor="black"
156
162
  unshrunkfontcolor="black"
157
- shrunklabelposition="aboveNotch"
163
+ shrunklabelposition="onNotch"
164
+ style={{
165
+ marginBottom: 0,
166
+ marginTop: 0,
167
+ }}
158
168
  />
159
169
  </TableCell>
160
170
  )
@@ -52,7 +52,7 @@ function Table({
52
52
  })
53
53
 
54
54
  // Decide which columns to render in the <TableHead /> for desktop.
55
- // On mobile, we skip the __overflow__ approach and just show the single dropdown.
55
+ // On mobile, we skip the "__overflow__" approach and just show the single dropdown.
56
56
  const finalDesktopColumns = !isMobile
57
57
  ? overflowDesktopColumns.length > 0
58
58
  ? [
@@ -63,16 +63,23 @@ function Table({
63
63
  : []
64
64
 
65
65
  return (
66
- // The main wrapper. Key: allow horizontal scroll if table is too wide.
67
- <Box sx={{ width: '100%', overflowX: 'auto' }}>
66
+ // The main wrapper - Using overflowX: 'hidden' to prevent horizontal scrollbar
67
+ <Box sx={{ width: '100%', overflowX: 'hidden' }}>
68
68
  {/* We set the "ref" here so that useComputeTableResize can measure width. */}
69
- <TableContainer ref={containerRef} sx={{ overflowX: 'auto' }}>
69
+ <TableContainer
70
+ ref={containerRef}
71
+ sx={{
72
+ overflowX: 'visible', // Changed from 'auto' to 'visible'
73
+ }}
74
+ >
70
75
  <MuiTable
71
76
  sx={{
72
- // Let columns expand to their set widths
77
+ // Set width to 100% to fit container
78
+ width: '100%',
79
+ // Keep tableLayout as 'auto' to respect column widths
73
80
  tableLayout: 'auto',
74
81
  // Force the table's minimum width to accommodate large columns
75
- minWidth: 'fit-content',
82
+ minWidth: isMobile ? 'auto' : 'fit-content',
76
83
  }}
77
84
  >
78
85
  {/* Table Header */}
@@ -87,7 +94,7 @@ function Table({
87
94
  finalDesktopColumns={finalDesktopColumns}
88
95
  // Overflow columns (desktop)
89
96
  overflowDesktopColumns={overflowDesktopColumns}
90
- // Current selected column for overflow or mobile
97
+ // Current "selected" column for overflow or mobile
91
98
  selectedOverflowField={selectedOverflowField}
92
99
  setSelectedOverflowField={setSelectedOverflowField}
93
100
  // The entire columns array so we can present them all on mobile
@@ -93,8 +93,8 @@ function DataGrid({
93
93
  flexDirection: 'column',
94
94
  // Increase or remove height if you want more vertical space:
95
95
  height: 'calc(100vh - 60px)',
96
- // The key: allow horizontal scroll so columns with big widths can be scrolled
97
- overflow: 'auto',
96
+ // Add overflow hidden at the DataGrid level to prevent horizontal scrollbars
97
+ overflow: 'hidden',
98
98
  backgroundColor: woad.main,
99
99
  }}
100
100
  >
@@ -132,12 +132,11 @@ function DataGrid({
132
132
  display: 'flex',
133
133
  flexDirection: 'column',
134
134
  alignItems: 'flex-start',
135
+ // Ensure this container doesn't create scrollbars
136
+ overflow: 'hidden',
135
137
  }}
136
138
  >
137
- {/*
138
- This is the actual <Table/> component (not the file).
139
- Just leaving it as-is, but inside it we do a horizontal scroll and tableLayout: 'auto'.
140
- */}
139
+ {/* Table component */}
141
140
  <Table
142
141
  columns={columns}
143
142
  rows={visibleRows}
@@ -86,8 +86,8 @@ export function useComputeTableResize({
86
86
  return 60
87
87
  }
88
88
  const header = col.headerName || col.field
89
- // +40 as a buffer for padding, sorting icons, etc.
90
- return measureTextWidth(header) + 40
89
+ // +60 as a larger buffer for padding, sorting icons, etc. to prevent premature overflow
90
+ return measureTextWidth(header) + 60
91
91
  },
92
92
  [measureTextWidth]
93
93
  )
@@ -102,14 +102,17 @@ export function useComputeTableResize({
102
102
  if (!containerRef.current) return
103
103
  const containerWidth = containerRef.current.offsetWidth
104
104
 
105
+ // Add a buffer zone to make transitions smoother
106
+ const COLUMN_TRANSITION_BUFFER = 50 // pixels of buffer
107
+
105
108
  // Only consider columns that are visible
106
109
  const visibleCols = columns.filter(
107
110
  col => columnVisibility[col.field] !== false
108
111
  )
109
112
 
110
113
  let usedWidth = checkboxSelection ? 50 : 0
111
- // ~180 px for the "overflow" column if needed
112
- const overflowReservedWidth = showOverflowDropdown ? 180 : 0
114
+ // Increase overflow column width for better usability (was 180)
115
+ const overflowReservedWidth = showOverflowDropdown ? 275 : 0
113
116
 
114
117
  const canFit: ColumnDef[] = []
115
118
  let theOverflow: ColumnDef[] = []
@@ -120,28 +123,43 @@ export function useComputeTableResize({
120
123
 
121
124
  if (col.width != null) {
122
125
  // If the developer explicitly set a width, forcibly add to canFit
123
- canFit.push(col)
124
- usedWidth += needed
125
- continue
126
- }
127
-
128
- // Otherwise, do the old "fit" check
129
- if (usedWidth + needed + overflowReservedWidth <= containerWidth) {
130
- canFit.push(col)
131
- usedWidth += needed
126
+ // only if we have enough space with our buffer
127
+ if (
128
+ usedWidth +
129
+ needed +
130
+ overflowReservedWidth +
131
+ COLUMN_TRANSITION_BUFFER <=
132
+ containerWidth
133
+ ) {
134
+ canFit.push(col)
135
+ usedWidth += needed
136
+ } else {
137
+ // Not enough space, all remaining columns go to overflow
138
+ theOverflow = visibleCols.slice(i)
139
+ break
140
+ }
132
141
  } else {
133
- // everything else is overflow
134
- theOverflow = visibleCols.slice(i)
135
-
136
- // If we can't fit i-th column, let's see if we can also move
137
- // the last fitted column to overflow
138
- if (theOverflow.length > 0 && canFit.length > 1) {
139
- const lastFitted = canFit.pop()
140
- if (lastFitted) {
141
- theOverflow = [lastFitted, ...theOverflow]
142
+ // Standard "does it fit?" check with buffer to prevent flickering/jumping
143
+ if (
144
+ usedWidth + needed + overflowReservedWidth <=
145
+ containerWidth - COLUMN_TRANSITION_BUFFER
146
+ ) {
147
+ canFit.push(col)
148
+ usedWidth += needed
149
+ } else {
150
+ // everything else is overflow
151
+ theOverflow = visibleCols.slice(i)
152
+
153
+ // If we can't fit i-th column, let's see if we can also move
154
+ // the last fitted column to overflow to make more room
155
+ if (theOverflow.length > 0 && canFit.length > 1) {
156
+ const lastFitted = canFit.pop()
157
+ if (lastFitted) {
158
+ theOverflow = [lastFitted, ...theOverflow]
159
+ }
142
160
  }
161
+ break
143
162
  }
144
- break
145
163
  }
146
164
  }
147
165
 
@@ -17,6 +17,11 @@ export interface DropdownOption {
17
17
  value: string
18
18
  attribute1?: string
19
19
  attribute2?: string
20
+ attribute3?: string // New attribute for complex variant
21
+ attribute4?: string // New attribute for complex variant
22
+ attribute5?: string // Additional attribute for complex variant
23
+ attribute6?: string // Additional attribute for complex variant
24
+ uniqueKey?: string // Add uniqueKey for React key usage
20
25
  }
21
26
 
22
27
  export interface SearchableDropdownProps {
@@ -41,6 +46,8 @@ export interface SearchableDropdownProps {
41
46
  width?: string
42
47
  // Added style property to allow additional styling (e.g., marginBottom)
43
48
  style?: React.CSSProperties
49
+ // New variant property to determine display style
50
+ variant?: 'simple' | 'complex'
44
51
  }
45
52
 
46
53
  const StyledFormControl = styled(FormControl)<{ width?: string }>(
@@ -89,6 +96,7 @@ interface StyledAutocompleteProps {
89
96
  placeholdercolor?: string
90
97
  shrunklabelposition?: 'onNotch' | 'aboveNotch'
91
98
  disabled?: boolean
99
+ variant?: 'simple' | 'complex'
92
100
  }
93
101
 
94
102
  const StyledAutocomplete = styled(
@@ -102,6 +110,7 @@ const StyledAutocomplete = styled(
102
110
  placeholdercolor,
103
111
  shrunklabelposition,
104
112
  disabled,
113
+ variant,
105
114
  } = props
106
115
 
107
116
  return {
@@ -162,16 +171,22 @@ const StyledAutocomplete = styled(
162
171
  '& .MuiAutocomplete-input': {
163
172
  padding: '8px 14px',
164
173
  },
174
+ // Improve dropdown menu positioning and styling
165
175
  '& .MuiAutocomplete-popper': {
166
176
  width: '100% !important',
177
+ zIndex: 9999, // Ensure high z-index for the popup
167
178
  '& .MuiPaper-root': {
168
179
  width: '100%',
169
180
  marginTop: '4px',
181
+ maxHeight: '300px', // Increase max height for better usability
182
+ overflowY: 'auto',
183
+ boxShadow: '0px 5px 15px rgba(0, 0, 0, 0.2)', // Enhanced shadow
184
+ border: `1px solid ${black.light}`, // Add border to dropdown container
170
185
  },
171
186
  '& .MuiAutocomplete-listbox': {
172
- padding: '4px 0',
187
+ padding: '0', // Remove default padding for cleaner lines
173
188
  '& .MuiAutocomplete-option': {
174
- padding: '8px 14px',
189
+ padding: variant === 'complex' ? '10px 14px' : '8px 14px',
175
190
  display: 'flex',
176
191
  flexDirection: 'column',
177
192
  alignItems: 'flex-start',
@@ -180,6 +195,15 @@ const StyledAutocomplete = styled(
180
195
  width: '100%',
181
196
  textAlign: 'left',
182
197
  },
198
+ '&:last-child': {
199
+ borderBottom: 'none', // Remove border from last item to avoid double borders
200
+ },
201
+ },
202
+ '& .MuiAutocomplete-option[aria-selected="true"]': {
203
+ backgroundColor: `${black.main}08`,
204
+ },
205
+ '& .MuiAutocomplete-option:hover': {
206
+ backgroundColor: `${black.main}15`, // Slightly darker hover state
183
207
  },
184
208
  },
185
209
  },
@@ -210,7 +234,8 @@ const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
210
234
  placeholder,
211
235
  disabled = false,
212
236
  width,
213
- style, // destructure the style prop
237
+ style,
238
+ variant = 'simple', // Default to simple variant
214
239
  }) => {
215
240
  const [value, setValue] = useState<DropdownOption | string | null>(null)
216
241
  const [inputValue, setInputValue] = useState('')
@@ -267,7 +292,7 @@ const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
267
292
  error={error}
268
293
  disabled={disabled}
269
294
  width={width}
270
- style={style} // pass the style prop here
295
+ style={style}
271
296
  >
272
297
  <StyledInputLabel
273
298
  id={labelId}
@@ -302,12 +327,16 @@ const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
302
327
  />
303
328
  }
304
329
  disablePortal={false}
330
+ ListboxProps={{
331
+ style: { maxHeight: '300px', overflowY: 'auto' },
332
+ }}
305
333
  disabled={disabled}
306
334
  backgroundcolor={backgroundcolor}
307
335
  outlinecolor={outlinecolor}
308
336
  fontcolor={fontcolor}
309
337
  inputfontcolor={inputfontcolor}
310
338
  placeholdercolor={placeholdercolor}
339
+ variant={variant}
311
340
  filterOptions={(opts, state) => {
312
341
  const input = state.inputValue.toLowerCase()
313
342
  return opts.filter(o => o.value.toLowerCase().includes(input))
@@ -328,45 +357,110 @@ const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
328
357
  const { key, ...restLiProps } = liProps as {
329
358
  key: string
330
359
  } & React.HTMLAttributes<HTMLLIElement>
360
+
361
+ // Common styles for both variants
362
+ const liStyle = {
363
+ color: black.main,
364
+ padding: variant === 'complex' ? '10px 14px' : '8px 14px',
365
+ display: 'flex',
366
+ flexDirection: 'column' as const,
367
+ alignItems: 'flex-start' as const,
368
+ gap: variant === 'complex' ? '4px' : '2px',
369
+ width: '100%',
370
+ borderBottom: `1px solid ${black.light}`,
371
+ }
372
+
373
+ // Use the uniqueKey prop if available, otherwise fall back to the provided key
374
+ const optionKey = option.uniqueKey || key
375
+
331
376
  return (
332
- <li
333
- key={key}
334
- {...restLiProps}
335
- style={{
336
- color: black.main,
337
- padding: '8px 14px',
338
- display: 'flex',
339
- flexDirection: 'column',
340
- alignItems: 'flex-start',
341
- gap: '2px',
342
- width: '100%',
343
- }}
344
- >
377
+ <li key={optionKey} {...restLiProps} style={liStyle}>
378
+ {/* Main value - both variants */}
345
379
  <Typography
346
380
  fontvariant="merriparagraph"
347
381
  text={option.value.replace(/_/g, ' ')}
348
382
  fontcolor={black.main}
349
383
  sx={{
350
384
  fontSize: '14px',
385
+ fontWeight: variant === 'complex' ? '500' : 'normal',
351
386
  lineHeight: '20px',
352
387
  width: '100%',
353
388
  textAlign: 'left',
354
389
  }}
355
390
  />
356
- {(option.attribute1 || option.attribute2) && (
357
- <Typography
358
- fontvariant="merriparagraph"
359
- text={[option.attribute1, option.attribute2]
360
- .filter(Boolean)
361
- .join(' | ')}
362
- fontcolor="rgba(0, 0, 0, 0.6)"
363
- sx={{
364
- fontSize: '12px',
365
- lineHeight: '16px',
366
- width: '100%',
367
- textAlign: 'left',
368
- }}
369
- />
391
+
392
+ {/* For simple variant - show attribute1 and attribute2 on one line */}
393
+ {variant === 'simple' &&
394
+ (option.attribute1 || option.attribute2) && (
395
+ <Typography
396
+ fontvariant="merriparagraph"
397
+ text={[option.attribute1, option.attribute2]
398
+ .filter(Boolean)
399
+ .join(' | ')}
400
+ fontcolor="rgba(0, 0, 0, 0.6)"
401
+ sx={{
402
+ fontSize: '12px',
403
+ lineHeight: '16px',
404
+ width: '100%',
405
+ textAlign: 'left',
406
+ }}
407
+ />
408
+ )}
409
+
410
+ {/* For complex variant - show attributes on separate lines */}
411
+ {variant === 'complex' && (
412
+ <>
413
+ {/* First line of attributes */}
414
+ {(option.attribute1 || option.attribute2) && (
415
+ <Typography
416
+ fontvariant="merriparagraph"
417
+ text={[option.attribute1, option.attribute2]
418
+ .filter(Boolean)
419
+ .join(' | ')}
420
+ fontcolor="rgba(0, 0, 0, 0.6)"
421
+ sx={{
422
+ fontSize: '12px',
423
+ lineHeight: '16px',
424
+ width: '100%',
425
+ textAlign: 'left',
426
+ }}
427
+ />
428
+ )}
429
+
430
+ {/* Second line of attributes */}
431
+ {(option.attribute3 || option.attribute4) && (
432
+ <Typography
433
+ fontvariant="merriparagraph"
434
+ text={[option.attribute3, option.attribute4]
435
+ .filter(Boolean)
436
+ .join(' | ')}
437
+ fontcolor="rgba(0, 0, 0, 0.6)"
438
+ sx={{
439
+ fontSize: '12px',
440
+ lineHeight: '16px',
441
+ width: '100%',
442
+ textAlign: 'left',
443
+ }}
444
+ />
445
+ )}
446
+
447
+ {/* Third line of attributes */}
448
+ {(option.attribute5 || option.attribute6) && (
449
+ <Typography
450
+ fontvariant="merriparagraph"
451
+ text={[option.attribute5, option.attribute6]
452
+ .filter(Boolean)
453
+ .join(' | ')}
454
+ fontcolor="rgba(0, 0, 0, 0.6)"
455
+ sx={{
456
+ fontSize: '12px',
457
+ lineHeight: '16px',
458
+ width: '100%',
459
+ textAlign: 'left',
460
+ }}
461
+ />
462
+ )}
463
+ </>
370
464
  )}
371
465
  </li>
372
466
  )
@@ -16,6 +16,79 @@ const sampleOptions = [
16
16
  { value: 'broccoli', attribute1: 'Vegetable', attribute2: 'Green' },
17
17
  ]
18
18
 
19
+ /**
20
+ * Sample options with the complex variant attributes
21
+ */
22
+ const complexSampleOptions = [
23
+ {
24
+ value: 'apple',
25
+ attribute1: 'Fruit',
26
+ attribute2: 'Green or Red',
27
+ attribute3: 'High Fiber',
28
+ attribute4: 'Seasonal: Fall',
29
+ attribute5: 'Origin: Worldwide',
30
+ attribute6: 'Storage: Cool, Dry',
31
+ },
32
+ {
33
+ value: 'banana',
34
+ attribute1: 'Fruit',
35
+ attribute2: 'Yellow',
36
+ attribute3: 'High Potassium',
37
+ attribute4: 'Year-round',
38
+ attribute5: 'Origin: Tropical',
39
+ attribute6: 'Storage: Room Temp',
40
+ },
41
+ {
42
+ value: 'carrot',
43
+ attribute1: 'Vegetable',
44
+ attribute2: 'Orange',
45
+ attribute3: 'High Vitamin A',
46
+ attribute4: 'Year-round',
47
+ attribute5: 'Origin: Middle East',
48
+ attribute6: 'Storage: Refrigerated',
49
+ },
50
+ {
51
+ value: 'potato',
52
+ attribute1: 'Vegetable',
53
+ attribute2: 'Brown',
54
+ attribute3: 'High Starch',
55
+ attribute4: 'Year-round',
56
+ attribute5: 'Origin: South America',
57
+ attribute6: 'Storage: Dark, Cool',
58
+ },
59
+ {
60
+ value: 'avocado',
61
+ attribute1: 'Fruit',
62
+ attribute2: 'Green',
63
+ attribute3: 'Healthy Fats',
64
+ attribute4: 'Seasonal: Spring',
65
+ attribute5: 'Origin: Mexico',
66
+ attribute6: 'Storage: Room Temp',
67
+ },
68
+ {
69
+ value: 'broccoli',
70
+ attribute1: 'Vegetable',
71
+ attribute2: 'Green',
72
+ attribute3: 'High Vitamin K',
73
+ attribute4: 'Seasonal: Winter',
74
+ attribute5: 'Origin: Mediterranean',
75
+ attribute6: 'Storage: Refrigerated',
76
+ },
77
+ ]
78
+
79
+ /**
80
+ * Helper function to safely access dropdown options in tests
81
+ */
82
+ const getDropdownOptions = () => {
83
+ const listboxElement = document.querySelector('[role="listbox"]')
84
+ if (listboxElement && listboxElement instanceof HTMLElement) {
85
+ const listbox = within(listboxElement)
86
+ return listbox.getAllByRole('option')
87
+ }
88
+ console.log('Listbox not found')
89
+ return []
90
+ }
91
+
19
92
  /**
20
93
  * Storybook metadata
21
94
  */
@@ -34,6 +107,11 @@ const meta: Meta<typeof SearchableDropdown> = {
34
107
  control: 'select',
35
108
  options: ['onNotch', 'aboveNotch'],
36
109
  },
110
+ variant: {
111
+ control: 'select',
112
+ options: ['simple', 'complex'],
113
+ description: 'Dropdown display variant',
114
+ },
37
115
  },
38
116
  }
39
117
  export default meta
@@ -49,12 +127,15 @@ export const Basic: Story = {
49
127
  label: 'Basic SearchableDropdown',
50
128
  options: sampleOptions,
51
129
  placeholder: 'Start typing...',
130
+ variant: 'simple',
52
131
  },
53
132
  play: async ({ canvasElement }) => {
54
133
  const canvas = within(canvasElement)
55
134
 
56
- // Check for the label
57
- expect(canvas.getByText('Basic SearchableDropdown')).toBeInTheDocument()
135
+ // Check for the label using a more specific query
136
+ expect(
137
+ canvas.getByRole('combobox', { name: 'Basic SearchableDropdown' })
138
+ ).toBeInTheDocument()
58
139
 
59
140
  // Click into the input
60
141
  const input = canvas.getByRole('combobox')
@@ -62,8 +143,15 @@ export const Basic: Story = {
62
143
 
63
144
  // Type a partial match
64
145
  await userEvent.type(input, 'car')
65
- // 'carrot' should appear
66
- expect(canvas.getByText('carrot')).toBeInTheDocument()
146
+
147
+ // Get dropdown options
148
+ const listboxItems = getDropdownOptions()
149
+
150
+ // Check if any option contains "carrot"
151
+ const hasCarrot = listboxItems.some(
152
+ item => item.textContent && item.textContent.includes('carrot')
153
+ )
154
+ expect(hasCarrot).toBe(true)
67
155
  },
68
156
  }
69
157
 
@@ -76,22 +164,23 @@ export const WithDefaultValue: Story = {
76
164
  label: 'Dropdown with Default Value',
77
165
  options: sampleOptions,
78
166
  defaultValue: 'banana',
167
+ variant: 'simple',
79
168
  },
80
169
  play: ({ canvasElement }) => {
81
170
  const canvas = within(canvasElement)
82
- // Expect the combobox to show the default item
171
+ // Expect the combobox to show the default item with first letter capitalized
83
172
  const input = canvas.getByRole('combobox')
84
- expect(input).toHaveValue('banana')
173
+ expect(input).toHaveValue('Banana')
85
174
  },
86
175
  }
87
176
 
88
177
  /**
89
- * 3) Options with Complex Attributes
178
+ * 3) Simple Variant with Attributes
90
179
  * Uses userEvent => keep `async`.
91
180
  */
92
- export const ComplexAttributes: Story = {
181
+ export const SimpleVariant: Story = {
93
182
  args: {
94
- label: 'Complex Attributes',
183
+ label: 'Simple Variant',
95
184
  options: [
96
185
  {
97
186
  value: 'item1',
@@ -110,20 +199,151 @@ export const ComplexAttributes: Story = {
110
199
  },
111
200
  ],
112
201
  placeholder: 'Search items...',
202
+ variant: 'simple',
203
+ },
204
+ play: async ({ canvasElement }) => {
205
+ const canvas = within(canvasElement)
206
+ // Open the dropdown
207
+ const input = canvas.getByRole('combobox')
208
+ await userEvent.click(input)
209
+
210
+ // Get dropdown options
211
+ const listboxItems = getDropdownOptions()
212
+
213
+ // Check if the items we're looking for exist
214
+ const hasItem1 = listboxItems.some(
215
+ item => item.textContent && item.textContent.includes('item1')
216
+ )
217
+ const hasItem3 = listboxItems.some(
218
+ item => item.textContent && item.textContent.includes('item3')
219
+ )
220
+
221
+ expect(hasItem1).toBe(true)
222
+ expect(hasItem3).toBe(true)
223
+ },
224
+ }
225
+
226
+ /**
227
+ * 4) Complex Variant with Additional Attributes
228
+ * Uses userEvent => keep `async`.
229
+ */
230
+ export const ComplexVariant: Story = {
231
+ args: {
232
+ label: 'Complex Variant',
233
+ options: [
234
+ {
235
+ value: 'item1',
236
+ attribute1: 'Primary attribute #1',
237
+ attribute2: 'Secondary attribute #1',
238
+ attribute3: 'Tertiary attribute #1',
239
+ attribute4: 'Additional info #1',
240
+ attribute5: 'Extended data #1',
241
+ attribute6: 'Final info #1',
242
+ },
243
+ {
244
+ value: 'item2',
245
+ attribute1: 'Primary attribute #2',
246
+ attribute2: 'Secondary attribute #2',
247
+ attribute3: 'Tertiary attribute #2',
248
+ attribute4: 'Additional info #2',
249
+ attribute5: 'Extended data #2',
250
+ attribute6: 'Final info #2',
251
+ },
252
+ {
253
+ value: 'item3',
254
+ attribute1: 'Primary attribute #3',
255
+ attribute2: 'Secondary attribute #3',
256
+ attribute3: 'Tertiary attribute #3',
257
+ attribute4: 'Additional info #3',
258
+ attribute5: 'Extended data #3',
259
+ attribute6: 'Final info #3',
260
+ },
261
+ ],
262
+ placeholder: 'Search complex items...',
263
+ variant: 'complex',
113
264
  },
114
265
  play: async ({ canvasElement }) => {
115
266
  const canvas = within(canvasElement)
116
267
  // Open the dropdown
117
268
  const input = canvas.getByRole('combobox')
118
269
  await userEvent.click(input)
119
- // item1, item2, item3 should be visible
120
- expect(canvas.getByText('item1')).toBeInTheDocument()
121
- expect(canvas.getByText('item3')).toBeInTheDocument()
270
+
271
+ // Get dropdown options
272
+ const listboxItems = getDropdownOptions()
273
+
274
+ // Check if the items and their attributes are present in any of the option items
275
+ const hasItem1 = listboxItems.some(
276
+ item => item.textContent && item.textContent.includes('item1')
277
+ )
278
+ const hasAttributes = listboxItems.some(
279
+ item =>
280
+ item.textContent &&
281
+ item.textContent.includes('Primary attribute') &&
282
+ item.textContent.includes('Secondary attribute')
283
+ )
284
+ const hasExtendedAttributes = listboxItems.some(
285
+ item =>
286
+ item.textContent &&
287
+ item.textContent.includes('Extended data') &&
288
+ item.textContent.includes('Final info')
289
+ )
290
+
291
+ expect(hasItem1).toBe(true)
292
+ expect(hasAttributes).toBe(true)
293
+ expect(hasExtendedAttributes).toBe(true)
122
294
  },
123
295
  }
124
296
 
125
297
  /**
126
- * 4) Error State
298
+ * 5) Complex Data Example
299
+ * Uses userEvent => keep `async`.
300
+ */
301
+ export const ComplexDataExample: Story = {
302
+ args: {
303
+ label: 'Food Items',
304
+ options: complexSampleOptions,
305
+ placeholder: 'Search foods...',
306
+ variant: 'complex',
307
+ },
308
+ play: async ({ canvasElement }) => {
309
+ const canvas = within(canvasElement)
310
+ // Open the dropdown
311
+ const input = canvas.getByRole('combobox')
312
+ await userEvent.click(input)
313
+ await userEvent.type(input, 'a')
314
+
315
+ // Get dropdown options
316
+ const listboxItems = getDropdownOptions()
317
+
318
+ // Check if avocado and its attributes are present
319
+ const hasAvocado = listboxItems.some(
320
+ item => item.textContent && item.textContent.includes('avocado')
321
+ )
322
+ const hasFruitGreen = listboxItems.some(
323
+ item =>
324
+ item.textContent &&
325
+ item.textContent.includes('Fruit') &&
326
+ item.textContent.includes('Green')
327
+ )
328
+ const hasHealthyFats = listboxItems.some(
329
+ item => item.textContent && item.textContent.includes('Healthy Fats')
330
+ )
331
+ const hasOriginStorage = listboxItems.some(
332
+ item =>
333
+ item.textContent &&
334
+ (item.textContent.includes('Origin: Mexico') ||
335
+ item.textContent.includes('Storage: Room Temp'))
336
+ )
337
+
338
+ expect(hasAvocado).toBe(true)
339
+ expect(hasFruitGreen).toBe(true)
340
+ expect(hasHealthyFats).toBe(true)
341
+ expect(hasOriginStorage).toBe(true)
342
+ },
343
+ }
344
+
345
+ /**
346
+ * 6) Error State
127
347
  * No user interactions => remove `async`.
128
348
  */
129
349
  export const ErrorState: Story = {
@@ -132,6 +352,7 @@ export const ErrorState: Story = {
132
352
  options: sampleOptions,
133
353
  error: true,
134
354
  helperText: 'Something went wrong!',
355
+ variant: 'simple',
135
356
  },
136
357
  play: ({ canvasElement }) => {
137
358
  const canvas = within(canvasElement)
@@ -141,7 +362,7 @@ export const ErrorState: Story = {
141
362
  }
142
363
 
143
364
  /**
144
- * 5) Required Dropdown
365
+ * 7) Required Dropdown
145
366
  * No user interactions => remove `async`.
146
367
  */
147
368
  export const RequiredField: Story = {
@@ -149,16 +370,19 @@ export const RequiredField: Story = {
149
370
  label: 'Required Dropdown',
150
371
  options: sampleOptions,
151
372
  required: true,
373
+ variant: 'simple',
152
374
  },
153
375
  play: ({ canvasElement }) => {
154
376
  const canvas = within(canvasElement)
155
- // Check that label is present
156
- expect(canvas.getByText('Required Dropdown')).toBeInTheDocument()
377
+ // Check that label is present with a more specific query
378
+ expect(
379
+ canvas.getByRole('combobox', { name: 'Required Dropdown' })
380
+ ).toBeInTheDocument()
157
381
  },
158
382
  }
159
383
 
160
384
  /**
161
- * 6) Custom Colors
385
+ * 8) Custom Colors
162
386
  * Uses userEvent => keep `async`.
163
387
  */
164
388
  export const CustomColors: Story = {
@@ -174,6 +398,35 @@ export const CustomColors: Story = {
174
398
  shrunklabelposition: 'onNotch',
175
399
  placeholdercolor: '#42a5f5',
176
400
  placeholder: 'Enter something...',
401
+ variant: 'simple',
402
+ },
403
+ play: async ({ canvasElement }) => {
404
+ const canvas = within(canvasElement)
405
+ // Interact just to ensure no errors
406
+ const input = canvas.getByRole('combobox')
407
+ await userEvent.click(input)
408
+ expect(input).toBeInTheDocument()
409
+ },
410
+ }
411
+
412
+ /**
413
+ * 9) Custom Colors - Complex Variant
414
+ * Uses userEvent => keep `async`.
415
+ */
416
+ export const CustomColorsComplex: Story = {
417
+ args: {
418
+ label: 'Custom Colors - Complex',
419
+ options: complexSampleOptions,
420
+ backgroundcolor: '#f0f8ff',
421
+ outlinecolor: '#ff5722',
422
+ fontcolor: '#4caf50',
423
+ inputfontcolor: '#e91e63',
424
+ shrunkfontcolor: '#673ab7',
425
+ unshrunkfontcolor: '#9c27b0',
426
+ shrunklabelposition: 'onNotch',
427
+ placeholdercolor: '#42a5f5',
428
+ placeholder: 'Enter something...',
429
+ variant: 'complex',
177
430
  },
178
431
  play: async ({ canvasElement }) => {
179
432
  const canvas = within(canvasElement)
@@ -185,7 +438,7 @@ export const CustomColors: Story = {
185
438
  }
186
439
 
187
440
  /**
188
- * 7) Searching & Selecting
441
+ * 10) Searching & Selecting
189
442
  * Uses userEvent => keep `async`.
190
443
  */
191
444
  export const SearchAndSelect: Story = {
@@ -193,6 +446,7 @@ export const SearchAndSelect: Story = {
193
446
  label: 'Search & Select',
194
447
  options: sampleOptions,
195
448
  placeholder: 'Find an item...',
449
+ variant: 'simple',
196
450
  },
197
451
  play: async ({ canvasElement }) => {
198
452
  const canvas = within(canvasElement)
@@ -202,20 +456,28 @@ export const SearchAndSelect: Story = {
202
456
  await userEvent.click(input)
203
457
  await userEvent.type(input, 'avo')
204
458
 
205
- // 'avocado' should appear
206
- const avocadoOption = canvas.getByText('avocado')
207
- expect(avocadoOption).toBeInTheDocument()
459
+ // Get dropdown options
460
+ const listboxItems = getDropdownOptions()
208
461
 
209
- // Click the option
210
- await userEvent.click(avocadoOption)
462
+ // Find the avocado option
463
+ const avocadoOption = listboxItems.find(
464
+ item => item.textContent && item.textContent.includes('avocado')
465
+ )
211
466
 
212
- // Now the combobox value should be "avocado"
213
- expect(input).toHaveValue('avocado')
467
+ expect(avocadoOption).toBeTruthy()
468
+
469
+ // Click the option if found
470
+ if (avocadoOption) {
471
+ await userEvent.click(avocadoOption)
472
+
473
+ // Now the combobox value should be "Avocado" (with capitalization)
474
+ expect(input).toHaveValue('Avocado')
475
+ }
214
476
  },
215
477
  }
216
478
 
217
479
  /**
218
- * 8) No Options scenario
480
+ * 11) No Options scenario
219
481
  * Uses userEvent => keep `async`.
220
482
  */
221
483
  export const NoOptions: Story = {
@@ -223,6 +485,7 @@ export const NoOptions: Story = {
223
485
  label: 'Empty Dropdown',
224
486
  options: [],
225
487
  placeholder: 'No items available...',
488
+ variant: 'simple',
226
489
  },
227
490
  play: async ({ canvasElement }) => {
228
491
  const canvas = within(canvasElement)
@@ -233,3 +496,87 @@ export const NoOptions: Story = {
233
496
  expect(canvas.queryByText('apple')).not.toBeInTheDocument()
234
497
  },
235
498
  }
499
+
500
+ /**
501
+ * 12) Disabled State
502
+ * No user interactions => remove `async`.
503
+ */
504
+ export const DisabledState: Story = {
505
+ args: {
506
+ label: 'Disabled Dropdown',
507
+ options: sampleOptions,
508
+ placeholder: 'Cannot select',
509
+ disabled: true,
510
+ variant: 'simple',
511
+ },
512
+ play: ({ canvasElement }) => {
513
+ const canvas = within(canvasElement)
514
+ const input = canvas.getByRole('combobox')
515
+ expect(input).toBeDisabled()
516
+ },
517
+ }
518
+
519
+ /**
520
+ * 13) Complex Variant Disabled
521
+ * No user interactions => remove `async`.
522
+ */
523
+ export const ComplexVariantDisabled: Story = {
524
+ args: {
525
+ label: 'Complex Variant Disabled',
526
+ options: complexSampleOptions,
527
+ placeholder: 'Cannot select',
528
+ disabled: true,
529
+ variant: 'complex',
530
+ },
531
+ play: ({ canvasElement }) => {
532
+ const canvas = within(canvasElement)
533
+ const input = canvas.getByRole('combobox')
534
+ expect(input).toBeDisabled()
535
+ },
536
+ }
537
+
538
+ /**
539
+ * 14) All Attributes Display
540
+ * Uses userEvent => keep `async`.
541
+ */
542
+ export const AllAttributesDisplay: Story = {
543
+ args: {
544
+ label: 'All Attributes Display',
545
+ options: [
546
+ {
547
+ value: 'complete item',
548
+ attribute1: 'First level',
549
+ attribute2: 'Second level',
550
+ attribute3: 'Third level',
551
+ attribute4: 'Fourth level',
552
+ attribute5: 'Fifth level',
553
+ attribute6: 'Sixth level',
554
+ },
555
+ ],
556
+ placeholder: 'View all attributes...',
557
+ variant: 'complex',
558
+ },
559
+ play: async ({ canvasElement }) => {
560
+ const canvas = within(canvasElement)
561
+ // Open the dropdown
562
+ const input = canvas.getByRole('combobox')
563
+ await userEvent.click(input)
564
+
565
+ // Get dropdown options
566
+ const listboxItems = getDropdownOptions()
567
+
568
+ // Check if all attribute levels are displayed
569
+ const hasAllLevels = listboxItems.some(
570
+ item =>
571
+ item.textContent &&
572
+ item.textContent.includes('First level') &&
573
+ item.textContent.includes('Second level') &&
574
+ item.textContent.includes('Third level') &&
575
+ item.textContent.includes('Fourth level') &&
576
+ item.textContent.includes('Fifth level') &&
577
+ item.textContent.includes('Sixth level')
578
+ )
579
+
580
+ expect(hasAllLevels).toBe(true)
581
+ },
582
+ }
@@ -48,7 +48,6 @@ const CustomToolbar: FC<CustomToolbarProps> = ({
48
48
  flexWrap: 'wrap',
49
49
  gap: 2,
50
50
  width: '100%',
51
- mb: 2,
52
51
  }}
53
52
  >
54
53
  {/* Left half: Buttons + Searchbar */}
@@ -92,7 +91,6 @@ const CustomToolbar: FC<CustomToolbarProps> = ({
92
91
  flexWrap: 'wrap',
93
92
  gap: 2,
94
93
  width: '100%',
95
- mb: 2,
96
94
  }}
97
95
  >
98
96
  {/* Left half: Buttons */}
@@ -31,7 +31,7 @@ const LeftCenter: FC<Partial<SearchbarProps>> = props => {
31
31
  height: '55px',
32
32
  }}
33
33
  >
34
- <Box sx={{ marginBottom: '10px' }}>
34
+ <Box sx={{ marginBottom: '7px' }}>
35
35
  <Searchbar
36
36
  shrunklabelposition={shrunklabelposition}
37
37
  shrunkfontcolor={shrunkfontcolor}
package/src/index.ts CHANGED
@@ -57,14 +57,14 @@ import PasswordField, { PasswordFieldProps } from './components/Field/Password'
57
57
  import PhoneNumberField from './components/Field/PhoneNumber'
58
58
  import Searchbar, { SearchbarProps } from './components/Field/Search'
59
59
  import TextField, { TextFieldProps } from './components/Field/Text'
60
+ import SearchableDropdown, {
61
+ SearchableDropdownProps,
62
+ DropdownOption,
63
+ } from './components/Field/Dropdown/Searchable'
60
64
 
61
65
  // Add FormDataGrid import
62
66
  import FormDataGrid from './components/Form/DataGrid'
63
67
  import type { FormDataGridProps } from './components/Form/DataGrid'
64
- import type {
65
- SearchableDropdownProps,
66
- DropdownOption,
67
- } from './components/Field/Dropdown/Searchable'
68
68
 
69
69
  // Animations
70
70
  import { Animation } from './components/Content/Structure/animations'
@@ -199,6 +199,7 @@ export type { RawCustomer }
199
199
  /* Named Type Exports */
200
200
  /* -------------------------------------------------------------------------- */
201
201
 
202
+ export { SearchableDropdown }
202
203
  // 1) Form DataGrid
203
204
  export type { FormDataGridProps }
204
205
  export type { CustomDialogProps }