wp-optimize
/
vendor
/
team-updraft
/
lib-onboarding-wizard
/
Wizard
/
Onboarding
/
src
/
components
/
Fields
/
MultiSelectDropdown.tsx
MultiSelectDropdown.tsx in WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance trunk, at vendor/team-updraft/lib-onboarding-wizard/Wizard/Onboarding/src/components/Fields/MultiSelectDropdown.tsx
| 1 | "use client"; |
| 2 | |
| 3 | import { memo, useState, useEffect, useRef } from '@wordpress/element'; |
| 4 | import FieldWrapper from './FieldWrapper'; |
| 5 | import { __ } from '@wordpress/i18n'; |
| 6 | import { |
| 7 | Select, |
| 8 | SelectContent, |
| 9 | SelectItem, |
| 10 | SelectTrigger, |
| 11 | SelectValue, |
| 12 | } from './DropdownInput'; |
| 13 | import Icon from '../../utils/Icon'; |
| 14 | |
| 15 | interface MultiSelectOption { |
| 16 | value: string; |
| 17 | label: string; |
| 18 | icon?: string; |
| 19 | } |
| 20 | |
| 21 | interface MultiSelectFieldProps { |
| 22 | field: { |
| 23 | id: string; |
| 24 | label?: string; |
| 25 | options: MultiSelectOption[]; |
| 26 | is_lock?: boolean; |
| 27 | tooltip?: any; |
| 28 | placeholder?: string; |
| 29 | [key: string]: any; |
| 30 | }; |
| 31 | value: string[]; |
| 32 | onChange: (value: string[]) => void; |
| 33 | fieldStatus?: (id: string, success: boolean) => void; |
| 34 | } |
| 35 | |
| 36 | const MultiSelect = ({ field, value, onChange, fieldStatus } : MultiSelectFieldProps ) => { |
| 37 | const [open, setOpen] = useState(false); |
| 38 | const selectedValues = Array.isArray(value) ? value : []; |
| 39 | const removeButtonRefs = useRef<Record<string, HTMLSpanElement | null>>({}); |
| 40 | |
| 41 | const selectedOptions = selectedValues |
| 42 | .map(val => field.options.find(opt => opt.value === val)) |
| 43 | .filter((opt): opt is MultiSelectOption => opt !== undefined); |
| 44 | |
| 45 | useEffect(() => { |
| 46 | if (fieldStatus) { |
| 47 | const isValid = selectedValues.length > 0; |
| 48 | fieldStatus(field.id, isValid); |
| 49 | } |
| 50 | }, [selectedValues, field.id, fieldStatus]); |
| 51 | |
| 52 | const handleValueChange = (newValue: string) => { |
| 53 | const isSelected = selectedValues.includes(newValue); |
| 54 | let updatedValues; |
| 55 | |
| 56 | if (isSelected) { |
| 57 | updatedValues = selectedValues.filter((item) => item !== newValue); |
| 58 | } else { |
| 59 | updatedValues = [...selectedValues, newValue]; |
| 60 | } |
| 61 | onChange(updatedValues); |
| 62 | }; |
| 63 | |
| 64 | const handleRemoveTag = (e: React.MouseEvent<HTMLSpanElement> | React.KeyboardEvent<HTMLSpanElement>, itemToRemove: string) => { |
| 65 | e.stopPropagation(); |
| 66 | e.preventDefault(); |
| 67 | const updatedValues = selectedValues.filter((item) => item !== itemToRemove); |
| 68 | onChange(updatedValues); |
| 69 | }; |
| 70 | |
| 71 | const handleSelectOpenChange = (newOpen: boolean) => { |
| 72 | const clickedRemoveButton = Object.values(removeButtonRefs.current).some( |
| 73 | (ref) => ref && ref.contains(event.target as Node) |
| 74 | ); |
| 75 | |
| 76 | if (clickedRemoveButton) { |
| 77 | return; |
| 78 | } |
| 79 | setOpen(newOpen); |
| 80 | }; |
| 81 | |
| 82 | return ( |
| 83 | <FieldWrapper |
| 84 | inputId={field.id} |
| 85 | label={field.label} |
| 86 | tooltip={field.tooltip} |
| 87 | > |
| 88 | <Select onValueChange={handleValueChange} value="" open={open} onOpenChange={handleSelectOpenChange} disabled={!!field.is_lock}> |
| 89 | <SelectTrigger className="flex w-full items-center border border-gray-400 rounded-md min-h-[40px] flex-wrap p-2 pr-8"> |
| 90 | <div className="flex flex-wrap flex-1 items-center gap-1"> |
| 91 | {selectedOptions.map((opt) => ( |
| 92 | <div |
| 93 | key={opt.value} |
| 94 | className="flex items-center bg-gray-200 rounded-md px-2 py-1 text-sm" |
| 95 | > |
| 96 | {opt?.icon && ( |
| 97 | <Icon |
| 98 | name={opt.icon} |
| 99 | size={16} |
| 100 | color="gray-700" |
| 101 | className="mr-1" |
| 102 | /> |
| 103 | )} |
| 104 | {opt.label} |
| 105 | <span |
| 106 | ref={(el) => (removeButtonRefs.current[opt.value] = el)} |
| 107 | role="button" |
| 108 | tabIndex={0} |
| 109 | onClick={(e) => handleRemoveTag(e, opt.value)} |
| 110 | onKeyDown={(e) => { |
| 111 | if (e.key === 'Enter' || e.key === ' ') { |
| 112 | handleRemoveTag(e, opt.value); |
| 113 | } |
| 114 | }} |
| 115 | className="ml-2 p-1 text-gray-600 hover:text-gray-900 cursor-pointer" |
| 116 | > |
| 117 | <Icon name="times" size={18} color="gray-600" /> |
| 118 | </span> |
| 119 | </div> |
| 120 | ))} |
| 121 | {selectedValues.length === 0 && ( |
| 122 | <SelectValue placeholder={field.placeholder || __("Select options...", "ONBOARDING_WIZARD_TEXT_DOMAIN")} className="flex-1" /> |
| 123 | )} |
| 124 | {selectedValues.length > 0 && ( |
| 125 | <span className="flex-1 min-w-[50px] h-full" /> |
| 126 | )} |
| 127 | </div> |
| 128 | </SelectTrigger> |
| 129 | <SelectContent> |
| 130 | {field.options.map((option) => { |
| 131 | const isSelected = selectedValues.includes(option.value); |
| 132 | return ( |
| 133 | <SelectItem key={option.value} value={option.value}> |
| 134 | <div className="flex items-center w-full gap-2"> |
| 135 | {option.icon && ( |
| 136 | <Icon |
| 137 | name={option.icon} |
| 138 | size={16} |
| 139 | color="gray-700" |
| 140 | /> |
| 141 | )} |
| 142 | <span>{option.label}</span> |
| 143 | </div> |
| 144 | {isSelected && ( |
| 145 | <span className="absolute right-2 top-1/2 -translate-y-1/2 flex h-3.5 w-3.5 items-center justify-center"> |
| 146 | <Icon name="CheckRoundedIcon" size={16} color="blue" /> |
| 147 | </span> |
| 148 | )} |
| 149 | </SelectItem> |
| 150 | ); |
| 151 | })} |
| 152 | </SelectContent> |
| 153 | </Select> |
| 154 | </FieldWrapper> |
| 155 | ); |
| 156 | }; |
| 157 | |
| 158 | export default memo(MultiSelect); |