goobs-frontend 0.9.11 → 0.9.12
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,17 +1,33 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import React, { useState, useEffect, SyntheticEvent } from 'react'
|
|
3
|
+
import React, { useState, useEffect, SyntheticEvent, useRef } from 'react'
|
|
4
4
|
import {
|
|
5
5
|
Autocomplete,
|
|
6
6
|
InputLabel,
|
|
7
7
|
OutlinedInput,
|
|
8
8
|
FormHelperText,
|
|
9
9
|
FormControl,
|
|
10
|
+
Box,
|
|
11
|
+
Tabs,
|
|
12
|
+
Tab,
|
|
13
|
+
styled as muiStyled,
|
|
14
|
+
Popper,
|
|
10
15
|
} from '@mui/material'
|
|
11
16
|
import { styled } from '@mui/material/styles'
|
|
12
17
|
import { black, white } from '../../../../styles/palette'
|
|
13
18
|
import Typography from '../../../Typography'
|
|
14
19
|
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'
|
|
20
|
+
import HistoryIcon from '@mui/icons-material/History'
|
|
21
|
+
import SearchIcon from '@mui/icons-material/Search'
|
|
22
|
+
import { unstable_useEnhancedEffect as useEnhancedEffect } from '@mui/utils'
|
|
23
|
+
|
|
24
|
+
// Define a history item type with timestamp
|
|
25
|
+
interface HistoryItem {
|
|
26
|
+
text: string
|
|
27
|
+
timestamp: Date
|
|
28
|
+
// Optional formatted date string for display
|
|
29
|
+
formattedDate?: string
|
|
30
|
+
}
|
|
15
31
|
|
|
16
32
|
export interface DropdownOption {
|
|
17
33
|
value: string
|
|
@@ -44,10 +60,12 @@ export interface SearchableDropdownProps {
|
|
|
44
60
|
placeholder?: string
|
|
45
61
|
disabled?: boolean
|
|
46
62
|
width?: string
|
|
47
|
-
// Added style property to allow additional styling (e.g., marginBottom)
|
|
48
63
|
style?: React.CSSProperties
|
|
49
|
-
// New variant property to determine display style
|
|
50
64
|
variant?: 'simple' | 'complex'
|
|
65
|
+
// Update search history type to support timestamps
|
|
66
|
+
searchHistory?: HistoryItem[] | string[]
|
|
67
|
+
onSearch?: (searchTerm: string, timestamp?: Date) => void
|
|
68
|
+
maxHistoryItems?: number
|
|
51
69
|
}
|
|
52
70
|
|
|
53
71
|
const StyledFormControl = styled(FormControl)<{ width?: string }>(
|
|
@@ -99,6 +117,26 @@ interface StyledAutocompleteProps {
|
|
|
99
117
|
variant?: 'simple' | 'complex'
|
|
100
118
|
}
|
|
101
119
|
|
|
120
|
+
// Styled tab component for the dropdown footer
|
|
121
|
+
const StyledTab = muiStyled(Tab)(() => ({
|
|
122
|
+
minHeight: '36px',
|
|
123
|
+
fontSize: '12px',
|
|
124
|
+
padding: '6px 12px',
|
|
125
|
+
color: black.main,
|
|
126
|
+
'&.Mui-selected': {
|
|
127
|
+
color: black.main,
|
|
128
|
+
fontWeight: 'bold',
|
|
129
|
+
},
|
|
130
|
+
}))
|
|
131
|
+
|
|
132
|
+
const StyledTabs = muiStyled(Tabs)(() => ({
|
|
133
|
+
minHeight: '36px',
|
|
134
|
+
borderTop: `1px solid ${black.light}`,
|
|
135
|
+
'& .MuiTabs-indicator': {
|
|
136
|
+
backgroundColor: black.main,
|
|
137
|
+
},
|
|
138
|
+
}))
|
|
139
|
+
|
|
102
140
|
const StyledAutocomplete = styled(
|
|
103
141
|
Autocomplete<DropdownOption, false, false, true>
|
|
104
142
|
)<StyledAutocompleteProps>(props => {
|
|
@@ -236,10 +274,138 @@ const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
|
|
|
236
274
|
width,
|
|
237
275
|
style,
|
|
238
276
|
variant = 'simple', // Default to simple variant
|
|
277
|
+
searchHistory = [], // Default to empty array
|
|
278
|
+
onSearch,
|
|
279
|
+
maxHistoryItems = 5, // Default to showing 5 history items
|
|
239
280
|
}) => {
|
|
240
281
|
const [value, setValue] = useState<DropdownOption | string | null>(null)
|
|
241
282
|
const [inputValue, setInputValue] = useState('')
|
|
242
283
|
const [isFocused, setIsFocused] = useState(false)
|
|
284
|
+
// Update local history state to support timestamps
|
|
285
|
+
const [localHistory, setLocalHistory] = useState<HistoryItem[]>([])
|
|
286
|
+
|
|
287
|
+
// Add state for active tab - 0 for All Options, 1 for History
|
|
288
|
+
const [activeTab, setActiveTab] = useState<number>(0)
|
|
289
|
+
|
|
290
|
+
// Use ref to track if we've done initial history setup to avoid loops
|
|
291
|
+
const initializedRef = React.useRef(false)
|
|
292
|
+
|
|
293
|
+
// Always use controlled open state
|
|
294
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
295
|
+
|
|
296
|
+
// Reference to the tab container
|
|
297
|
+
const tabsRef = useRef<HTMLDivElement | null>(null)
|
|
298
|
+
|
|
299
|
+
// Ref for the input/autocomplete
|
|
300
|
+
const autocompleteRef = useRef<HTMLDivElement>(null)
|
|
301
|
+
|
|
302
|
+
// Use enhanced effect to handle tab clicks without closing dropdown
|
|
303
|
+
useEnhancedEffect(() => {
|
|
304
|
+
// If tabs aren't mounted yet, do nothing
|
|
305
|
+
if (!tabsRef.current) return
|
|
306
|
+
|
|
307
|
+
const tabsElement = tabsRef.current
|
|
308
|
+
|
|
309
|
+
// Prevent any click inside the tabs from bubbling up
|
|
310
|
+
const preventClose = (e: MouseEvent) => {
|
|
311
|
+
e.preventDefault()
|
|
312
|
+
e.stopPropagation()
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Add click handler to the tabs element
|
|
316
|
+
tabsElement.addEventListener('mousedown', preventClose, true)
|
|
317
|
+
tabsElement.addEventListener('click', preventClose, true)
|
|
318
|
+
|
|
319
|
+
// Cleanup
|
|
320
|
+
return () => {
|
|
321
|
+
tabsElement.removeEventListener('mousedown', preventClose, true)
|
|
322
|
+
tabsElement.removeEventListener('click', preventClose, true)
|
|
323
|
+
}
|
|
324
|
+
}, [tabsRef.current])
|
|
325
|
+
|
|
326
|
+
// Function to format date for display
|
|
327
|
+
const formatDate = (date: Date): string => {
|
|
328
|
+
const now = new Date()
|
|
329
|
+
const diff = now.getTime() - date.getTime()
|
|
330
|
+
|
|
331
|
+
// If less than a minute ago
|
|
332
|
+
if (diff < 60 * 1000) {
|
|
333
|
+
return 'Just now'
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// If less than an hour ago
|
|
337
|
+
if (diff < 60 * 60 * 1000) {
|
|
338
|
+
const minutes = Math.floor(diff / (60 * 1000))
|
|
339
|
+
return `${minutes} ${minutes === 1 ? 'minute' : 'minutes'} ago`
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// If less than a day ago
|
|
343
|
+
if (diff < 24 * 60 * 60 * 1000) {
|
|
344
|
+
const hours = Math.floor(diff / (60 * 60 * 1000))
|
|
345
|
+
return `${hours} ${hours === 1 ? 'hour' : 'hours'} ago`
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// If less than a week ago
|
|
349
|
+
if (diff < 7 * 24 * 60 * 60 * 1000) {
|
|
350
|
+
const days = Math.floor(diff / (24 * 60 * 60 * 1000))
|
|
351
|
+
return `${days} ${days === 1 ? 'day' : 'days'} ago`
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Otherwise, format as date
|
|
355
|
+
return date.toLocaleDateString(undefined, {
|
|
356
|
+
year: 'numeric',
|
|
357
|
+
month: 'short',
|
|
358
|
+
day: 'numeric',
|
|
359
|
+
})
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Initialize history with timestamps if using simple strings
|
|
363
|
+
useEffect(() => {
|
|
364
|
+
if (!initializedRef.current && searchHistory && searchHistory.length > 0) {
|
|
365
|
+
// Check if searchHistory items are strings or already HistoryItems
|
|
366
|
+
if (typeof searchHistory[0] === 'string') {
|
|
367
|
+
const historyWithTimestamps = (searchHistory as string[]).map(
|
|
368
|
+
(text, index) => {
|
|
369
|
+
// Create timestamps with slight variations so older items are truly older
|
|
370
|
+
const timestamp = new Date()
|
|
371
|
+
timestamp.setMinutes(timestamp.getMinutes() - (index + 1))
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
text,
|
|
375
|
+
timestamp,
|
|
376
|
+
formattedDate: formatDate(timestamp),
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
)
|
|
380
|
+
setLocalHistory(historyWithTimestamps)
|
|
381
|
+
} else {
|
|
382
|
+
// Already the right format, just update formatted dates
|
|
383
|
+
const formattedHistory = (searchHistory as HistoryItem[]).map(item => ({
|
|
384
|
+
...item,
|
|
385
|
+
formattedDate: formatDate(item.timestamp),
|
|
386
|
+
}))
|
|
387
|
+
setLocalHistory(formattedHistory)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
initializedRef.current = true
|
|
391
|
+
}
|
|
392
|
+
}, [searchHistory])
|
|
393
|
+
|
|
394
|
+
// Refresh formatted dates every minute for "minutes ago" style timestamps
|
|
395
|
+
useEffect(() => {
|
|
396
|
+
const intervalId = setInterval(() => {
|
|
397
|
+
if (localHistory.length > 0) {
|
|
398
|
+
setLocalHistory(prev =>
|
|
399
|
+
prev.map(item => ({
|
|
400
|
+
...item,
|
|
401
|
+
formattedDate: formatDate(item.timestamp),
|
|
402
|
+
}))
|
|
403
|
+
)
|
|
404
|
+
}
|
|
405
|
+
}, 60000) // Update every minute
|
|
406
|
+
|
|
407
|
+
return () => clearInterval(intervalId)
|
|
408
|
+
}, [localHistory.length])
|
|
243
409
|
|
|
244
410
|
useEffect(() => {
|
|
245
411
|
const defaultOption = options.find(option => option.value === defaultValue)
|
|
@@ -260,6 +426,26 @@ const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
|
|
|
260
426
|
setValue(newValue)
|
|
261
427
|
setInputValue(newValue)
|
|
262
428
|
onChange?.(null)
|
|
429
|
+
|
|
430
|
+
// Add to search history if it's a string input
|
|
431
|
+
if (newValue.trim()) {
|
|
432
|
+
const now = new Date()
|
|
433
|
+
|
|
434
|
+
if (onSearch) {
|
|
435
|
+
onSearch(newValue.trim(), now)
|
|
436
|
+
} else {
|
|
437
|
+
// Only update local history if we're not using onSearch callback
|
|
438
|
+
setLocalHistory(prev => {
|
|
439
|
+
const newItem = {
|
|
440
|
+
text: newValue.trim(),
|
|
441
|
+
timestamp: now,
|
|
442
|
+
formattedDate: formatDate(now),
|
|
443
|
+
}
|
|
444
|
+
const filteredPrev = prev.filter(h => h.text !== newItem.text)
|
|
445
|
+
return [newItem, ...filteredPrev].slice(0, maxHistoryItems)
|
|
446
|
+
})
|
|
447
|
+
}
|
|
448
|
+
}
|
|
263
449
|
} else {
|
|
264
450
|
setValue(newValue)
|
|
265
451
|
if (newValue) {
|
|
@@ -268,6 +454,24 @@ const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
|
|
|
268
454
|
newValue.value.replace(/_/g, ' ').slice(1)
|
|
269
455
|
setInputValue(displayText)
|
|
270
456
|
onChange?.(newValue)
|
|
457
|
+
|
|
458
|
+
// Add to search history
|
|
459
|
+
const now = new Date()
|
|
460
|
+
|
|
461
|
+
if (onSearch) {
|
|
462
|
+
onSearch(displayText, now)
|
|
463
|
+
} else if (newValue.value.trim()) {
|
|
464
|
+
// Only update local history if we're not using onSearch callback
|
|
465
|
+
setLocalHistory(prev => {
|
|
466
|
+
const newItem = {
|
|
467
|
+
text: displayText,
|
|
468
|
+
timestamp: now,
|
|
469
|
+
formattedDate: formatDate(now),
|
|
470
|
+
}
|
|
471
|
+
const filteredPrev = prev.filter(h => h.text !== newItem.text)
|
|
472
|
+
return [newItem, ...filteredPrev].slice(0, maxHistoryItems)
|
|
473
|
+
})
|
|
474
|
+
}
|
|
271
475
|
} else {
|
|
272
476
|
setInputValue('')
|
|
273
477
|
onChange?.(null)
|
|
@@ -275,6 +479,34 @@ const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
|
|
|
275
479
|
}
|
|
276
480
|
}
|
|
277
481
|
|
|
482
|
+
// Handler for when the input is submitted (e.g., Enter key)
|
|
483
|
+
const handleInputSubmit = () => {
|
|
484
|
+
if (inputValue.trim()) {
|
|
485
|
+
const now = new Date()
|
|
486
|
+
|
|
487
|
+
if (onSearch) {
|
|
488
|
+
onSearch(inputValue.trim(), now)
|
|
489
|
+
} else {
|
|
490
|
+
// Only update local history if we're not using onSearch callback
|
|
491
|
+
setLocalHistory(prev => {
|
|
492
|
+
const newItem = {
|
|
493
|
+
text: inputValue.trim(),
|
|
494
|
+
timestamp: now,
|
|
495
|
+
formattedDate: formatDate(now),
|
|
496
|
+
}
|
|
497
|
+
const filteredPrev = prev.filter(h => h.text !== newItem.text)
|
|
498
|
+
return [newItem, ...filteredPrev].slice(0, maxHistoryItems)
|
|
499
|
+
})
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const handleKeyDown = (event: React.KeyboardEvent) => {
|
|
505
|
+
if (event.key === 'Enter') {
|
|
506
|
+
handleInputSubmit()
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
278
510
|
const handleFocus = () => {
|
|
279
511
|
setIsFocused(true)
|
|
280
512
|
}
|
|
@@ -283,10 +515,238 @@ const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
|
|
|
283
515
|
if (!value && !inputValue) {
|
|
284
516
|
setIsFocused(false)
|
|
285
517
|
}
|
|
518
|
+
// Save search when blurring
|
|
519
|
+
if (inputValue.trim()) {
|
|
520
|
+
handleInputSubmit()
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Function to handle tab change
|
|
525
|
+
const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {
|
|
526
|
+
// Set tab value
|
|
527
|
+
setActiveTab(newValue)
|
|
528
|
+
|
|
529
|
+
// Keep the dropdown open when switching tabs
|
|
530
|
+
setIsOpen(true)
|
|
531
|
+
|
|
532
|
+
// Prevent any bubbling that might close the dropdown
|
|
533
|
+
_event.preventDefault()
|
|
534
|
+
_event.stopPropagation()
|
|
286
535
|
}
|
|
287
536
|
|
|
288
537
|
const labelId = `${name}-label`
|
|
289
538
|
|
|
539
|
+
// Use a combined history that prioritizes prop history when available but falls back to local history
|
|
540
|
+
const combinedHistory = React.useMemo(() => {
|
|
541
|
+
if (searchHistory?.length) {
|
|
542
|
+
// Convert string[] to HistoryItem[] if needed
|
|
543
|
+
if (typeof searchHistory[0] === 'string') {
|
|
544
|
+
return (searchHistory as string[]).map((text, index) => {
|
|
545
|
+
// Create timestamps with slight variations
|
|
546
|
+
const timestamp = new Date()
|
|
547
|
+
timestamp.setMinutes(timestamp.getMinutes() - (index + 1))
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
text,
|
|
551
|
+
timestamp,
|
|
552
|
+
formattedDate: formatDate(timestamp),
|
|
553
|
+
}
|
|
554
|
+
})
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Already HistoryItem[] format
|
|
558
|
+
return searchHistory as HistoryItem[]
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return localHistory
|
|
562
|
+
}, [searchHistory, localHistory])
|
|
563
|
+
|
|
564
|
+
// Create a combined options array based on active tab and input value
|
|
565
|
+
const getFilteredOptions = React.useCallback(() => {
|
|
566
|
+
const currentInputVal = inputValue.trim()
|
|
567
|
+
|
|
568
|
+
// HISTORY TAB
|
|
569
|
+
if (activeTab === 1) {
|
|
570
|
+
if (combinedHistory.length === 0) {
|
|
571
|
+
// Show a placeholder message if no history
|
|
572
|
+
return [
|
|
573
|
+
{
|
|
574
|
+
value: 'No search history',
|
|
575
|
+
uniqueKey: 'no-history',
|
|
576
|
+
attribute1: 'Try searching for something first',
|
|
577
|
+
},
|
|
578
|
+
]
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Map history items to dropdown options
|
|
582
|
+
return combinedHistory.map(item => {
|
|
583
|
+
// Check if this history item matches any of the original options
|
|
584
|
+
const matchingOption = options.find(
|
|
585
|
+
opt =>
|
|
586
|
+
opt.value.toLowerCase() === item.text.toLowerCase() ||
|
|
587
|
+
(
|
|
588
|
+
opt.value.replace(/_/g, ' ').charAt(0).toUpperCase() +
|
|
589
|
+
opt.value.replace(/_/g, ' ').slice(1)
|
|
590
|
+
).toLowerCase() === item.text.toLowerCase()
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
if (matchingOption) {
|
|
594
|
+
// If we have a matching original option, use its attributes
|
|
595
|
+
return {
|
|
596
|
+
value: item.text,
|
|
597
|
+
uniqueKey: `history-${item.text}`,
|
|
598
|
+
attribute1: 'Search History',
|
|
599
|
+
attribute2: item.formattedDate || formatDate(item.timestamp),
|
|
600
|
+
attribute3: matchingOption.attribute1,
|
|
601
|
+
attribute4: matchingOption.attribute2,
|
|
602
|
+
attribute5: matchingOption.attribute3,
|
|
603
|
+
attribute6: matchingOption.attribute4,
|
|
604
|
+
}
|
|
605
|
+
} else {
|
|
606
|
+
// Otherwise just use the basic history item with timestamp
|
|
607
|
+
return {
|
|
608
|
+
value: item.text,
|
|
609
|
+
uniqueKey: `history-${item.text}`,
|
|
610
|
+
attribute1: 'Search History',
|
|
611
|
+
attribute2: item.formattedDate || formatDate(item.timestamp),
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
})
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// ALL OPTIONS TAB (activeTab === 0)
|
|
618
|
+
// If input is empty, return all options
|
|
619
|
+
if (!currentInputVal) {
|
|
620
|
+
return options
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Filter options based on current input
|
|
624
|
+
const filteredOpts = options.filter(opt =>
|
|
625
|
+
opt.value.toLowerCase().includes(currentInputVal.toLowerCase())
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
// If no matches found, add the current input as a custom option
|
|
629
|
+
if (filteredOpts.length === 0) {
|
|
630
|
+
return [
|
|
631
|
+
{
|
|
632
|
+
value: currentInputVal,
|
|
633
|
+
uniqueKey: `current-${currentInputVal}`,
|
|
634
|
+
attribute1: 'Search',
|
|
635
|
+
},
|
|
636
|
+
]
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Check if the input exactly matches any option
|
|
640
|
+
const exactMatch = filteredOpts.some(
|
|
641
|
+
opt => opt.value.toLowerCase() === currentInputVal.toLowerCase()
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
// If no exact match, add the current input as the first option
|
|
645
|
+
if (!exactMatch) {
|
|
646
|
+
return [
|
|
647
|
+
{
|
|
648
|
+
value: currentInputVal,
|
|
649
|
+
uniqueKey: `current-${currentInputVal}`,
|
|
650
|
+
attribute1: 'Search',
|
|
651
|
+
},
|
|
652
|
+
...filteredOpts,
|
|
653
|
+
]
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return filteredOpts
|
|
657
|
+
}, [inputValue, combinedHistory, options, activeTab])
|
|
658
|
+
|
|
659
|
+
// Create the footer component for the dropdown with tabs
|
|
660
|
+
const ListboxFooter = React.forwardRef<HTMLDivElement>((_, ref) => (
|
|
661
|
+
<Box
|
|
662
|
+
ref={(node: HTMLDivElement | null) => {
|
|
663
|
+
// Set both refs
|
|
664
|
+
if (ref) {
|
|
665
|
+
if (typeof ref === 'function') {
|
|
666
|
+
ref(node)
|
|
667
|
+
} else {
|
|
668
|
+
ref.current = node
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
tabsRef.current = node
|
|
672
|
+
}}
|
|
673
|
+
sx={{
|
|
674
|
+
position: 'sticky',
|
|
675
|
+
bottom: 0,
|
|
676
|
+
backgroundColor: white.main,
|
|
677
|
+
zIndex: 2,
|
|
678
|
+
borderTop: `1px solid ${black.light}`,
|
|
679
|
+
}}
|
|
680
|
+
onClick={e => {
|
|
681
|
+
e.preventDefault()
|
|
682
|
+
e.stopPropagation()
|
|
683
|
+
}}
|
|
684
|
+
onMouseDown={e => {
|
|
685
|
+
e.preventDefault()
|
|
686
|
+
e.stopPropagation()
|
|
687
|
+
}}
|
|
688
|
+
>
|
|
689
|
+
<StyledTabs
|
|
690
|
+
value={activeTab}
|
|
691
|
+
onChange={handleTabChange}
|
|
692
|
+
centered
|
|
693
|
+
sx={{
|
|
694
|
+
pointerEvents: 'all',
|
|
695
|
+
}}
|
|
696
|
+
>
|
|
697
|
+
<StyledTab
|
|
698
|
+
icon={<SearchIcon fontSize="small" />}
|
|
699
|
+
label="ALL OPTIONS"
|
|
700
|
+
iconPosition="start"
|
|
701
|
+
sx={{
|
|
702
|
+
pointerEvents: 'all',
|
|
703
|
+
}}
|
|
704
|
+
/>
|
|
705
|
+
<StyledTab
|
|
706
|
+
icon={<HistoryIcon fontSize="small" />}
|
|
707
|
+
label="HISTORY"
|
|
708
|
+
iconPosition="start"
|
|
709
|
+
sx={{
|
|
710
|
+
pointerEvents: 'all',
|
|
711
|
+
}}
|
|
712
|
+
/>
|
|
713
|
+
</StyledTabs>
|
|
714
|
+
</Box>
|
|
715
|
+
))
|
|
716
|
+
|
|
717
|
+
// Add display name to ListboxFooter
|
|
718
|
+
ListboxFooter.displayName = 'ListboxFooter'
|
|
719
|
+
|
|
720
|
+
// Custom Listbox component that adds the footer
|
|
721
|
+
const CustomListbox = React.forwardRef<
|
|
722
|
+
HTMLElement,
|
|
723
|
+
React.HTMLAttributes<HTMLElement>
|
|
724
|
+
>((props, ref) => {
|
|
725
|
+
const { children, ...other } = props
|
|
726
|
+
return (
|
|
727
|
+
<div
|
|
728
|
+
ref={ref as React.Ref<HTMLDivElement>}
|
|
729
|
+
onClick={e => {
|
|
730
|
+
e.stopPropagation()
|
|
731
|
+
return false
|
|
732
|
+
}}
|
|
733
|
+
style={{ pointerEvents: 'auto' }}
|
|
734
|
+
>
|
|
735
|
+
<ul {...other}>{children}</ul>
|
|
736
|
+
<ListboxFooter />
|
|
737
|
+
</div>
|
|
738
|
+
)
|
|
739
|
+
})
|
|
740
|
+
|
|
741
|
+
// Add display name to CustomListbox
|
|
742
|
+
CustomListbox.displayName = 'CustomListbox'
|
|
743
|
+
|
|
744
|
+
// Memoize the filtered options to prevent recreation on every render
|
|
745
|
+
const filteredOptions = React.useMemo(
|
|
746
|
+
() => getFilteredOptions(),
|
|
747
|
+
[getFilteredOptions]
|
|
748
|
+
)
|
|
749
|
+
|
|
290
750
|
return (
|
|
291
751
|
<StyledFormControl
|
|
292
752
|
error={error}
|
|
@@ -308,7 +768,7 @@ const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
|
|
|
308
768
|
</StyledInputLabel>
|
|
309
769
|
<StyledAutocomplete
|
|
310
770
|
id={name}
|
|
311
|
-
options={
|
|
771
|
+
options={filteredOptions}
|
|
312
772
|
freeSolo
|
|
313
773
|
value={value}
|
|
314
774
|
onChange={handleChange}
|
|
@@ -321,6 +781,18 @@ const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
|
|
|
321
781
|
onFocus={handleFocus}
|
|
322
782
|
onBlur={handleBlur}
|
|
323
783
|
forcePopupIcon
|
|
784
|
+
open={isOpen}
|
|
785
|
+
onOpen={() => {
|
|
786
|
+
setIsOpen(true)
|
|
787
|
+
}}
|
|
788
|
+
onClose={event => {
|
|
789
|
+
// Don't close when clicking tabs
|
|
790
|
+
if (tabsRef.current?.contains(event.target as Node)) {
|
|
791
|
+
return
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
setIsOpen(false)
|
|
795
|
+
}}
|
|
324
796
|
popupIcon={
|
|
325
797
|
<ArrowDropDownIcon
|
|
326
798
|
sx={{ color: disabled ? 'rgba(0, 0, 0, 0.38)' : black.main }}
|
|
@@ -328,8 +800,69 @@ const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
|
|
|
328
800
|
}
|
|
329
801
|
disablePortal={false}
|
|
330
802
|
ListboxProps={{
|
|
331
|
-
style: {
|
|
803
|
+
style: {
|
|
804
|
+
maxHeight: '300px',
|
|
805
|
+
overflowY: 'auto',
|
|
806
|
+
pointerEvents: 'all',
|
|
807
|
+
},
|
|
808
|
+
}}
|
|
809
|
+
ListboxComponent={CustomListbox}
|
|
810
|
+
componentsProps={{
|
|
811
|
+
popper: {
|
|
812
|
+
onClick: e => {
|
|
813
|
+
// Prevent clicks in the popper from closing the dropdown
|
|
814
|
+
e.stopPropagation()
|
|
815
|
+
},
|
|
816
|
+
style: {
|
|
817
|
+
pointerEvents: 'all',
|
|
818
|
+
// Ensure clicks inside the popper don't close it
|
|
819
|
+
inset: '0px auto auto 0px',
|
|
820
|
+
zIndex: 9999,
|
|
821
|
+
},
|
|
822
|
+
},
|
|
823
|
+
paper: {
|
|
824
|
+
onClick: e => {
|
|
825
|
+
// Prevent clicks on the paper from closing the dropdown
|
|
826
|
+
e.stopPropagation()
|
|
827
|
+
},
|
|
828
|
+
style: { pointerEvents: 'all' },
|
|
829
|
+
},
|
|
332
830
|
}}
|
|
831
|
+
ref={autocompleteRef}
|
|
832
|
+
PopperComponent={props => (
|
|
833
|
+
<Popper
|
|
834
|
+
{...props}
|
|
835
|
+
onClick={e => {
|
|
836
|
+
e.stopPropagation()
|
|
837
|
+
}}
|
|
838
|
+
style={{
|
|
839
|
+
...props.style,
|
|
840
|
+
pointerEvents: 'all',
|
|
841
|
+
zIndex: 9999,
|
|
842
|
+
}}
|
|
843
|
+
modifiers={[
|
|
844
|
+
{
|
|
845
|
+
name: 'preventOverflow',
|
|
846
|
+
enabled: true,
|
|
847
|
+
options: {
|
|
848
|
+
altAxis: true,
|
|
849
|
+
altBoundary: true,
|
|
850
|
+
tether: true,
|
|
851
|
+
rootBoundary: 'document',
|
|
852
|
+
padding: 8,
|
|
853
|
+
},
|
|
854
|
+
},
|
|
855
|
+
{
|
|
856
|
+
name: 'offset',
|
|
857
|
+
options: {
|
|
858
|
+
offset: [0, 0],
|
|
859
|
+
},
|
|
860
|
+
},
|
|
861
|
+
]}
|
|
862
|
+
>
|
|
863
|
+
{props.children}
|
|
864
|
+
</Popper>
|
|
865
|
+
)}
|
|
333
866
|
disabled={disabled}
|
|
334
867
|
backgroundcolor={backgroundcolor}
|
|
335
868
|
outlinecolor={outlinecolor}
|
|
@@ -337,10 +870,7 @@ const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
|
|
|
337
870
|
inputfontcolor={inputfontcolor}
|
|
338
871
|
placeholdercolor={placeholdercolor}
|
|
339
872
|
variant={variant}
|
|
340
|
-
filterOptions={
|
|
341
|
-
const input = state.inputValue.toLowerCase()
|
|
342
|
-
return opts.filter(o => o.value.toLowerCase().includes(input))
|
|
343
|
-
}}
|
|
873
|
+
filterOptions={opts => opts} // We're handling filtering ourselves
|
|
344
874
|
getOptionLabel={(option: DropdownOption | string) => {
|
|
345
875
|
if (typeof option === 'string') {
|
|
346
876
|
return option
|
|
@@ -373,24 +903,63 @@ const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
|
|
|
373
903
|
// Use the uniqueKey prop if available, otherwise fall back to the provided key
|
|
374
904
|
const optionKey = option.uniqueKey || key
|
|
375
905
|
|
|
906
|
+
// Check if this is a history item
|
|
907
|
+
const isHistoryItem = option.uniqueKey?.startsWith('history-')
|
|
908
|
+
const isCurrentInput = option.uniqueKey?.startsWith('current-')
|
|
909
|
+
const isNoHistoryPlaceholder = option.uniqueKey === 'no-history'
|
|
910
|
+
|
|
376
911
|
return (
|
|
377
912
|
<li key={optionKey} {...restLiProps} style={liStyle}>
|
|
378
913
|
{/* Main value - both variants */}
|
|
379
914
|
<Typography
|
|
380
915
|
fontvariant="merriparagraph"
|
|
381
|
-
text={
|
|
916
|
+
text={
|
|
917
|
+
isNoHistoryPlaceholder
|
|
918
|
+
? option.value
|
|
919
|
+
: isCurrentInput
|
|
920
|
+
? `Search: "${option.value}"`
|
|
921
|
+
: isHistoryItem
|
|
922
|
+
? `History: ${option.value.replace(/_/g, ' ')}`
|
|
923
|
+
: option.value.replace(/_/g, ' ')
|
|
924
|
+
}
|
|
382
925
|
fontcolor={black.main}
|
|
383
926
|
sx={{
|
|
384
927
|
fontSize: '14px',
|
|
385
|
-
fontWeight:
|
|
928
|
+
fontWeight: isCurrentInput
|
|
929
|
+
? '500'
|
|
930
|
+
: isHistoryItem
|
|
931
|
+
? '400'
|
|
932
|
+
: variant === 'complex'
|
|
933
|
+
? '500'
|
|
934
|
+
: 'normal',
|
|
935
|
+
fontStyle: isHistoryItem ? 'italic' : 'normal',
|
|
386
936
|
lineHeight: '20px',
|
|
387
937
|
width: '100%',
|
|
388
938
|
textAlign: 'left',
|
|
389
939
|
}}
|
|
390
940
|
/>
|
|
391
941
|
|
|
942
|
+
{/* For history items, show the timestamp */}
|
|
943
|
+
{isHistoryItem && option.attribute2 && (
|
|
944
|
+
<Typography
|
|
945
|
+
fontvariant="merriparagraph"
|
|
946
|
+
text={option.attribute2}
|
|
947
|
+
fontcolor="rgba(0, 0, 0, 0.6)"
|
|
948
|
+
sx={{
|
|
949
|
+
fontSize: '11px',
|
|
950
|
+
lineHeight: '14px',
|
|
951
|
+
width: '100%',
|
|
952
|
+
textAlign: 'left',
|
|
953
|
+
fontStyle: 'italic',
|
|
954
|
+
}}
|
|
955
|
+
/>
|
|
956
|
+
)}
|
|
957
|
+
|
|
392
958
|
{/* For simple variant - show attribute1 and attribute2 on one line */}
|
|
393
959
|
{variant === 'simple' &&
|
|
960
|
+
!isHistoryItem &&
|
|
961
|
+
!isCurrentInput &&
|
|
962
|
+
!isNoHistoryPlaceholder &&
|
|
394
963
|
(option.attribute1 || option.attribute2) && (
|
|
395
964
|
<Typography
|
|
396
965
|
fontvariant="merriparagraph"
|
|
@@ -408,26 +977,68 @@ const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
|
|
|
408
977
|
)}
|
|
409
978
|
|
|
410
979
|
{/* For complex variant - show attributes on separate lines */}
|
|
411
|
-
{variant === 'complex' &&
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
980
|
+
{variant === 'complex' &&
|
|
981
|
+
!isHistoryItem &&
|
|
982
|
+
!isCurrentInput &&
|
|
983
|
+
!isNoHistoryPlaceholder && (
|
|
984
|
+
<>
|
|
985
|
+
{/* First line of attributes */}
|
|
986
|
+
{(option.attribute1 || option.attribute2) && (
|
|
987
|
+
<Typography
|
|
988
|
+
fontvariant="merriparagraph"
|
|
989
|
+
text={[option.attribute1, option.attribute2]
|
|
990
|
+
.filter(Boolean)
|
|
991
|
+
.join(' | ')}
|
|
992
|
+
fontcolor="rgba(0, 0, 0, 0.6)"
|
|
993
|
+
sx={{
|
|
994
|
+
fontSize: '12px',
|
|
995
|
+
lineHeight: '16px',
|
|
996
|
+
width: '100%',
|
|
997
|
+
textAlign: 'left',
|
|
998
|
+
}}
|
|
999
|
+
/>
|
|
1000
|
+
)}
|
|
1001
|
+
|
|
1002
|
+
{/* Second line of attributes */}
|
|
1003
|
+
{(option.attribute3 || option.attribute4) && (
|
|
1004
|
+
<Typography
|
|
1005
|
+
fontvariant="merriparagraph"
|
|
1006
|
+
text={[option.attribute3, option.attribute4]
|
|
1007
|
+
.filter(Boolean)
|
|
1008
|
+
.join(' | ')}
|
|
1009
|
+
fontcolor="rgba(0, 0, 0, 0.6)"
|
|
1010
|
+
sx={{
|
|
1011
|
+
fontSize: '12px',
|
|
1012
|
+
lineHeight: '16px',
|
|
1013
|
+
width: '100%',
|
|
1014
|
+
textAlign: 'left',
|
|
1015
|
+
}}
|
|
1016
|
+
/>
|
|
1017
|
+
)}
|
|
429
1018
|
|
|
430
|
-
|
|
1019
|
+
{/* Third line of attributes */}
|
|
1020
|
+
{(option.attribute5 || option.attribute6) && (
|
|
1021
|
+
<Typography
|
|
1022
|
+
fontvariant="merriparagraph"
|
|
1023
|
+
text={[option.attribute5, option.attribute6]
|
|
1024
|
+
.filter(Boolean)
|
|
1025
|
+
.join(' | ')}
|
|
1026
|
+
fontcolor="rgba(0, 0, 0, 0.6)"
|
|
1027
|
+
sx={{
|
|
1028
|
+
fontSize: '12px',
|
|
1029
|
+
lineHeight: '16px',
|
|
1030
|
+
width: '100%',
|
|
1031
|
+
textAlign: 'left',
|
|
1032
|
+
}}
|
|
1033
|
+
/>
|
|
1034
|
+
)}
|
|
1035
|
+
</>
|
|
1036
|
+
)}
|
|
1037
|
+
|
|
1038
|
+
{/* For history items, show additional attributes from original options */}
|
|
1039
|
+
{isHistoryItem && variant === 'complex' && (
|
|
1040
|
+
<>
|
|
1041
|
+
{/* Show attribute3/4 as first additional line for history */}
|
|
431
1042
|
{(option.attribute3 || option.attribute4) && (
|
|
432
1043
|
<Typography
|
|
433
1044
|
fontvariant="merriparagraph"
|
|
@@ -440,11 +1051,12 @@ const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
|
|
|
440
1051
|
lineHeight: '16px',
|
|
441
1052
|
width: '100%',
|
|
442
1053
|
textAlign: 'left',
|
|
1054
|
+
fontStyle: 'italic',
|
|
443
1055
|
}}
|
|
444
1056
|
/>
|
|
445
1057
|
)}
|
|
446
1058
|
|
|
447
|
-
{/*
|
|
1059
|
+
{/* Show attribute5/6 as second additional line for history */}
|
|
448
1060
|
{(option.attribute5 || option.attribute6) && (
|
|
449
1061
|
<Typography
|
|
450
1062
|
fontvariant="merriparagraph"
|
|
@@ -457,11 +1069,43 @@ const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
|
|
|
457
1069
|
lineHeight: '16px',
|
|
458
1070
|
width: '100%',
|
|
459
1071
|
textAlign: 'left',
|
|
1072
|
+
fontStyle: 'italic',
|
|
460
1073
|
}}
|
|
461
1074
|
/>
|
|
462
1075
|
)}
|
|
463
1076
|
</>
|
|
464
1077
|
)}
|
|
1078
|
+
|
|
1079
|
+
{/* For current input search, show a simpler display */}
|
|
1080
|
+
{isCurrentInput && option.attribute1 && (
|
|
1081
|
+
<Typography
|
|
1082
|
+
fontvariant="merriparagraph"
|
|
1083
|
+
text={option.attribute1}
|
|
1084
|
+
fontcolor="rgba(0, 0, 0, 0.6)"
|
|
1085
|
+
sx={{
|
|
1086
|
+
fontSize: '12px',
|
|
1087
|
+
lineHeight: '16px',
|
|
1088
|
+
width: '100%',
|
|
1089
|
+
textAlign: 'left',
|
|
1090
|
+
}}
|
|
1091
|
+
/>
|
|
1092
|
+
)}
|
|
1093
|
+
|
|
1094
|
+
{/* For no history placeholder, show the instructions */}
|
|
1095
|
+
{isNoHistoryPlaceholder && option.attribute1 && (
|
|
1096
|
+
<Typography
|
|
1097
|
+
fontvariant="merriparagraph"
|
|
1098
|
+
text={option.attribute1}
|
|
1099
|
+
fontcolor="rgba(0, 0, 0, 0.6)"
|
|
1100
|
+
sx={{
|
|
1101
|
+
fontSize: '12px',
|
|
1102
|
+
lineHeight: '16px',
|
|
1103
|
+
width: '100%',
|
|
1104
|
+
textAlign: 'left',
|
|
1105
|
+
fontStyle: 'italic',
|
|
1106
|
+
}}
|
|
1107
|
+
/>
|
|
1108
|
+
)}
|
|
465
1109
|
</li>
|
|
466
1110
|
)
|
|
467
1111
|
}}
|
|
@@ -471,6 +1115,7 @@ const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
|
|
|
471
1115
|
inputProps={{
|
|
472
1116
|
...params.inputProps,
|
|
473
1117
|
'aria-labelledby': labelId,
|
|
1118
|
+
onKeyDown: handleKeyDown,
|
|
474
1119
|
}}
|
|
475
1120
|
placeholder={placeholder}
|
|
476
1121
|
error={error}
|
|
@@ -580,3 +580,167 @@ export const AllAttributesDisplay: Story = {
|
|
|
580
580
|
expect(hasAllLevels).toBe(true)
|
|
581
581
|
},
|
|
582
582
|
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* 15) With Search History
|
|
586
|
+
* Uses userEvent => keep `async`.
|
|
587
|
+
*/
|
|
588
|
+
export const WithSearchHistory: Story = {
|
|
589
|
+
args: {
|
|
590
|
+
label: 'Search with History',
|
|
591
|
+
options: complexSampleOptions,
|
|
592
|
+
placeholder: 'Search with history...',
|
|
593
|
+
variant: 'complex',
|
|
594
|
+
// Provide pre-populated search history
|
|
595
|
+
searchHistory: [
|
|
596
|
+
{ text: 'apple', timestamp: new Date(Date.now() - 5 * 60000) }, // 5 minutes ago
|
|
597
|
+
{ text: 'broccoli', timestamp: new Date(Date.now() - 60 * 60000) }, // 1 hour ago
|
|
598
|
+
{
|
|
599
|
+
text: 'custom search',
|
|
600
|
+
timestamp: new Date(Date.now() - 24 * 60 * 60000),
|
|
601
|
+
}, // 1 day ago
|
|
602
|
+
],
|
|
603
|
+
maxHistoryItems: 5,
|
|
604
|
+
},
|
|
605
|
+
play: async ({ canvasElement }) => {
|
|
606
|
+
const canvas = within(canvasElement)
|
|
607
|
+
|
|
608
|
+
// Open the dropdown
|
|
609
|
+
const input = canvas.getByRole('combobox')
|
|
610
|
+
await userEvent.click(input)
|
|
611
|
+
|
|
612
|
+
// Switch to history tab
|
|
613
|
+
const historyTab = canvas.getByRole('tab', { name: /HISTORY/i })
|
|
614
|
+
await userEvent.click(historyTab)
|
|
615
|
+
|
|
616
|
+
// Get dropdown options after switching to history tab
|
|
617
|
+
const listboxItems = getDropdownOptions()
|
|
618
|
+
|
|
619
|
+
// Check if history items are displayed
|
|
620
|
+
const hasAppleHistory = listboxItems.some(
|
|
621
|
+
item => item.textContent && item.textContent.includes('apple')
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
const hasTimestamp = listboxItems.some(
|
|
625
|
+
item =>
|
|
626
|
+
item.textContent &&
|
|
627
|
+
(item.textContent.includes('minutes ago') ||
|
|
628
|
+
item.textContent.includes('hour ago') ||
|
|
629
|
+
item.textContent.includes('day ago'))
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
expect(hasAppleHistory).toBe(true)
|
|
633
|
+
expect(hasTimestamp).toBe(true)
|
|
634
|
+
|
|
635
|
+
// Try entering a new search term
|
|
636
|
+
await userEvent.clear(input)
|
|
637
|
+
await userEvent.type(input, 'new search')
|
|
638
|
+
await userEvent.tab() // Blur to trigger adding to history
|
|
639
|
+
|
|
640
|
+
// Reopen and check history tab again
|
|
641
|
+
await userEvent.click(input)
|
|
642
|
+
await userEvent.click(historyTab)
|
|
643
|
+
|
|
644
|
+
// Should now include our new search
|
|
645
|
+
const updatedListboxItems = getDropdownOptions()
|
|
646
|
+
const hasNewSearch = updatedListboxItems.some(
|
|
647
|
+
item => item.textContent && item.textContent.includes('new search')
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
expect(hasNewSearch).toBe(true)
|
|
651
|
+
},
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* 16) With Search Callback
|
|
656
|
+
* Uses userEvent => keep `async`.
|
|
657
|
+
*/
|
|
658
|
+
export const WithSearchCallback: Story = {
|
|
659
|
+
args: {
|
|
660
|
+
label: 'Search with Callback',
|
|
661
|
+
options: sampleOptions,
|
|
662
|
+
placeholder: 'Search with callback...',
|
|
663
|
+
variant: 'simple',
|
|
664
|
+
// This callback will be triggered when searches are performed
|
|
665
|
+
onSearch: (searchTerm: string, timestamp?: Date) => {
|
|
666
|
+
console.log(
|
|
667
|
+
`Search term: ${searchTerm}, Time: ${timestamp?.toISOString() || 'No timestamp'}`
|
|
668
|
+
)
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
play: async ({ canvasElement }) => {
|
|
672
|
+
const canvas = within(canvasElement)
|
|
673
|
+
|
|
674
|
+
// Perform a search
|
|
675
|
+
const input = canvas.getByRole('combobox')
|
|
676
|
+
await userEvent.click(input)
|
|
677
|
+
await userEvent.type(input, 'test search')
|
|
678
|
+
await userEvent.tab() // Blur to trigger search
|
|
679
|
+
|
|
680
|
+
// Since we can't easily verify the callback in Storybook tests,
|
|
681
|
+
// we'll just ensure the component functions correctly
|
|
682
|
+
expect(input).toHaveValue('test search')
|
|
683
|
+
},
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* 17) Tab Navigation
|
|
688
|
+
* Uses userEvent => keep `async`.
|
|
689
|
+
*/
|
|
690
|
+
export const TabNavigation: Story = {
|
|
691
|
+
args: {
|
|
692
|
+
label: 'Tab Navigation Demo',
|
|
693
|
+
options: sampleOptions,
|
|
694
|
+
placeholder: 'Explore tabs...',
|
|
695
|
+
variant: 'simple',
|
|
696
|
+
// Provide some search history
|
|
697
|
+
searchHistory: [
|
|
698
|
+
'previous search one',
|
|
699
|
+
'previous search two',
|
|
700
|
+
'previous search three',
|
|
701
|
+
],
|
|
702
|
+
maxHistoryItems: 10,
|
|
703
|
+
},
|
|
704
|
+
play: async ({ canvasElement }) => {
|
|
705
|
+
const canvas = within(canvasElement)
|
|
706
|
+
|
|
707
|
+
// Open the dropdown
|
|
708
|
+
const input = canvas.getByRole('combobox')
|
|
709
|
+
await userEvent.click(input)
|
|
710
|
+
|
|
711
|
+
// First check that we're on the default "ALL OPTIONS" tab
|
|
712
|
+
// and can see the regular options
|
|
713
|
+
const allOptionsTab = canvas.getByRole('tab', { name: /ALL OPTIONS/i })
|
|
714
|
+
expect(allOptionsTab).toHaveAttribute('aria-selected', 'true')
|
|
715
|
+
|
|
716
|
+
// We should see the original options
|
|
717
|
+
const initialListboxItems = getDropdownOptions()
|
|
718
|
+
const hasAppleOption = initialListboxItems.some(
|
|
719
|
+
item => item.textContent && item.textContent.includes('apple')
|
|
720
|
+
)
|
|
721
|
+
expect(hasAppleOption).toBe(true)
|
|
722
|
+
|
|
723
|
+
// Now switch to the HISTORY tab
|
|
724
|
+
const historyTab = canvas.getByRole('tab', { name: /HISTORY/i })
|
|
725
|
+
await userEvent.click(historyTab)
|
|
726
|
+
expect(historyTab).toHaveAttribute('aria-selected', 'true')
|
|
727
|
+
|
|
728
|
+
// Now we should see the history items
|
|
729
|
+
const historyListboxItems = getDropdownOptions()
|
|
730
|
+
const hasHistoryItems = historyListboxItems.some(
|
|
731
|
+
item => item.textContent && item.textContent.includes('previous search')
|
|
732
|
+
)
|
|
733
|
+
expect(hasHistoryItems).toBe(true)
|
|
734
|
+
|
|
735
|
+
// Now switch back to ALL OPTIONS tab
|
|
736
|
+
await userEvent.click(allOptionsTab)
|
|
737
|
+
expect(allOptionsTab).toHaveAttribute('aria-selected', 'true')
|
|
738
|
+
|
|
739
|
+
// We should see the original options again
|
|
740
|
+
const finalListboxItems = getDropdownOptions()
|
|
741
|
+
const hasAppleOptionAgain = finalListboxItems.some(
|
|
742
|
+
item => item.textContent && item.textContent.includes('apple')
|
|
743
|
+
)
|
|
744
|
+
expect(hasAppleOptionAgain).toBe(true)
|
|
745
|
+
},
|
|
746
|
+
}
|