@bikdotai/bik-widgets 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/.eslintrc +22 -0
  2. package/.eslintrc.js +19 -0
  3. package/.github/workflows/main.yml +293 -0
  4. package/.prettierignore +13 -0
  5. package/.prettierrc +10 -0
  6. package/README.md +128 -0
  7. package/cypress/apiHelper/endpoints.ts +21 -0
  8. package/cypress/apiHelper/executor.ts +42 -0
  9. package/cypress/e2e/bottomDrawer.cy.ts +49 -0
  10. package/cypress/e2e/directReward.cy.ts +67 -0
  11. package/cypress/e2e/scratchTheCard.cy.ts +68 -0
  12. package/cypress/e2e/stw.cy.ts +82 -0
  13. package/cypress/e2e/waRedirection.cy.ts +46 -0
  14. package/cypress/fixtures/payloads.ts +330 -0
  15. package/cypress/support/commands.ts +37 -0
  16. package/cypress/support/e2e.ts +20 -0
  17. package/cypress.staging.config.ts +23 -0
  18. package/jsconfig.json +6 -0
  19. package/localtest.sh +10 -0
  20. package/log-server.js +86 -0
  21. package/package.json +79 -0
  22. package/postcss.config.js +8 -0
  23. package/src/Globals.d.ts +2 -0
  24. package/src/assets/lottie/santa.json +11722 -0
  25. package/src/assets/svg/CalendarClockIcon.tsx +30 -0
  26. package/src/assets/svg/CalendarIcon.tsx +24 -0
  27. package/src/assets/svg/CheckIcon.tsx +17 -0
  28. package/src/assets/svg/ChevronIcon.tsx +21 -0
  29. package/src/assets/svg/Close.tsx +39 -0
  30. package/src/assets/svg/Confetti.tsx +140 -0
  31. package/src/assets/svg/Copy.tsx +26 -0
  32. package/src/assets/svg/DropdownCheckIcon.tsx +35 -0
  33. package/src/assets/svg/ErrorIcon.tsx +27 -0
  34. package/src/assets/svg/RadioIcon.tsx +25 -0
  35. package/src/assets/svg/UncheckedCheckboxIcon.tsx +28 -0
  36. package/src/assets/svg/UncheckedRadioIcon.tsx +26 -0
  37. package/src/assets/svg/info.tsx +30 -0
  38. package/src/assets/svg/qrcode.svg +14 -0
  39. package/src/bootstrap.tsx +8 -0
  40. package/src/components/CtaCard/index.tsx +37 -0
  41. package/src/components/CtaCard/preview.module.css +32 -0
  42. package/src/components/CtaCard/style.module.css +32 -0
  43. package/src/components/EmailInput/emailInputBox.tsx +95 -0
  44. package/src/components/Fab/index.tsx +224 -0
  45. package/src/components/Fab/preview.module.css +28 -0
  46. package/src/components/Fab/style.module.css +37 -0
  47. package/src/components/Icons/Call.tsx +26 -0
  48. package/src/components/Icons/Cross.tsx +24 -0
  49. package/src/components/Icons/Gmail.tsx +61 -0
  50. package/src/components/Icons/Instagram.tsx +60 -0
  51. package/src/components/Icons/LiveChat.tsx +43 -0
  52. package/src/components/Icons/Messenger.tsx +57 -0
  53. package/src/components/Icons/Send.tsx +22 -0
  54. package/src/components/Icons/Whatsapp.tsx +24 -0
  55. package/src/components/Shimmer/index.tsx +12 -0
  56. package/src/components/Shimmer/style.module.css +37 -0
  57. package/src/components/SmsInput/smsInputBox.tsx +135 -0
  58. package/src/components/UserDetailsV2/userDetailsV2.desktop.module.css +52 -0
  59. package/src/components/UserDetailsV2/userDetailsV2.mobile.module.css +52 -0
  60. package/src/components/UserDetailsV2/userDetailsV2.module.css +81 -0
  61. package/src/components/UserDetailsV2/userDetailsV2.tsx +527 -0
  62. package/src/components/WhatsappInput/Spinner.tsx +26 -0
  63. package/src/components/WhatsappInput/whatsappInput.module.css +106 -0
  64. package/src/components/WhatsappInput/whatsappInputBox.tsx +155 -0
  65. package/src/components/WhatsappInput/whatsappInputPreviewDesktop.module.css +71 -0
  66. package/src/components/WhatsappInput/whatsappInputPreviewMobile.module.css +65 -0
  67. package/src/components/checkbox/checkbox.module.css +19 -0
  68. package/src/components/checkbox/checkbox.tsx +88 -0
  69. package/src/components/countryCodePicker/countriesDropdown.module.css +77 -0
  70. package/src/components/countryCodePicker/countriesDropdown.tsx +81 -0
  71. package/src/components/couponDetails/coupon.module.css +208 -0
  72. package/src/components/couponDetails/coupon.tsx +210 -0
  73. package/src/components/couponDetails/couponPreviewDesktop.module.css +158 -0
  74. package/src/components/couponDetails/couponPreviewMobile.module.css +164 -0
  75. package/src/components/index.ts +3 -0
  76. package/src/components/inputComponents/Checkbox.module.css +197 -0
  77. package/src/components/inputComponents/Checkbox.tsx +85 -0
  78. package/src/components/inputComponents/DatePicker.module.css +565 -0
  79. package/src/components/inputComponents/DatePicker.tsx +278 -0
  80. package/src/components/inputComponents/Dropdown.module.css +796 -0
  81. package/src/components/inputComponents/Dropdown.tsx +630 -0
  82. package/src/components/inputComponents/InputBox.module.css +401 -0
  83. package/src/components/inputComponents/InputBox.tsx +209 -0
  84. package/src/components/selectedCountry/selectedCountry.module.css +76 -0
  85. package/src/components/selectedCountry/selectedCountry.tsx +76 -0
  86. package/src/components/selectedCountry/selectedCountryPreviewDesktop.module.css +56 -0
  87. package/src/components/selectedCountry/selectedCountryPreviewMobile.module.css +57 -0
  88. package/src/components/userDetailsForm/RenderCustomFields.tsx +333 -0
  89. package/src/components/userDetailsForm/userDetailsForm.tsx +675 -0
  90. package/src/hooks/index.ts +4 -0
  91. package/src/hooks/useExitIntent.ts +452 -0
  92. package/src/hooks/useIsMobile.tsx +21 -0
  93. package/src/hooks/useMessageEvent.ts +8 -0
  94. package/src/hooks/useTriggeredIntentDetails.ts +43 -0
  95. package/src/hooks/useUrlListerner.ts +30 -0
  96. package/src/hooks/useWebSocketLogger.ts +59 -0
  97. package/src/hooks/useWindowEvent.ts +8 -0
  98. package/src/icons/copyIcon.tsx +26 -0
  99. package/src/icons/crossIconDesktop.tsx +20 -0
  100. package/src/icons/crossIconMobile.tsx +20 -0
  101. package/src/index.html +30 -0
  102. package/src/index.ts +32 -0
  103. package/src/index.tsx +1 -0
  104. package/src/repo/widgetRepo.ts +21 -0
  105. package/src/types/customFields.ts +73 -0
  106. package/src/utilities/cookie.ts +70 -0
  107. package/src/utilities/customFieldTypeMapping.ts +67 -0
  108. package/src/utilities/customFieldValidation.ts +201 -0
  109. package/src/utilities/encryption.ts +21 -0
  110. package/src/utilities/exitIntentUtils.ts +31 -0
  111. package/src/utilities/global.css +11 -0
  112. package/src/utilities/languageUtilities.ts +235 -0
  113. package/src/utilities/localRunner.js +26 -0
  114. package/src/utilities/localRunner.ts +27 -0
  115. package/src/utilities/localStorage.ts +40 -0
  116. package/src/utilities/script.tsx +15 -0
  117. package/src/utilities/stringUtils.ts +5 -0
  118. package/src/utilities/styleUtils.ts +134 -0
  119. package/src/utilities/variables.ts +11 -0
  120. package/src/utilities/widgetUtils.js +342 -0
  121. package/src/utilities/widgetUtils.ts +313 -0
  122. package/src/widgets/BottomDrawer/config.ts +41 -0
  123. package/src/widgets/BottomDrawer/index.tsx +116 -0
  124. package/src/widgets/BottomDrawer/modal.tsx +286 -0
  125. package/src/widgets/BottomDrawer/preview.module.css +122 -0
  126. package/src/widgets/BottomDrawer/previewMobile.module.css +124 -0
  127. package/src/widgets/BottomDrawer/style.module.css +279 -0
  128. package/src/widgets/CaptivateBanner/captivateBanner.tsx +200 -0
  129. package/src/widgets/CaptivateBanner/config.ts +72 -0
  130. package/src/widgets/CaptivateBanner/index.tsx +204 -0
  131. package/src/widgets/CaptivateBanner/previewDesktop.module.css +51 -0
  132. package/src/widgets/CaptivateBanner/previewMobile.module.css +51 -0
  133. package/src/widgets/CaptivateBanner/style.module.css +77 -0
  134. package/src/widgets/CaptivateBanner/utils.ts +104 -0
  135. package/src/widgets/CentrallyAlignedPopup/config.ts +42 -0
  136. package/src/widgets/CentrallyAlignedPopup/index.tsx +109 -0
  137. package/src/widgets/CentrallyAlignedPopup/modal.tsx +269 -0
  138. package/src/widgets/CentrallyAlignedPopup/preview.module.css +153 -0
  139. package/src/widgets/CentrallyAlignedPopup/previewMobile.module.css +153 -0
  140. package/src/widgets/CentrallyAlignedPopup/style.module.css +283 -0
  141. package/src/widgets/DirectReward/components/couponDetails.tsx +265 -0
  142. package/src/widgets/DirectReward/components/userDetails.tsx +117 -0
  143. package/src/widgets/DirectReward/config.ts +186 -0
  144. package/src/widgets/DirectReward/directReward.tsx +350 -0
  145. package/src/widgets/DirectReward/index.tsx +579 -0
  146. package/src/widgets/DirectReward/previewStyles/thankYouPreviewDesktop.module.css +276 -0
  147. package/src/widgets/DirectReward/previewStyles/thankYouPreviewMobile.module.css +303 -0
  148. package/src/widgets/DirectReward/previewStyles/userDetailsPreviewDesktop.module.css +511 -0
  149. package/src/widgets/DirectReward/previewStyles/userDetailsPreviewMobile.module.css +462 -0
  150. package/src/widgets/DirectReward/style.module.css +836 -0
  151. package/src/widgets/ExitIntentHook.tsx +28 -0
  152. package/src/widgets/STW/api.ts +70 -0
  153. package/src/widgets/STW/components/svgFactory.tsx +44 -0
  154. package/src/widgets/STW/config.ts +193 -0
  155. package/src/widgets/STW/context.ts +7 -0
  156. package/src/widgets/STW/couponDetails.tsx +121 -0
  157. package/src/widgets/STW/index.tsx +733 -0
  158. package/src/widgets/STW/previewStyles/thankyouPreviewDesktop.module.css +215 -0
  159. package/src/widgets/STW/previewStyles/thankyouPreviewMobile.module.css +205 -0
  160. package/src/widgets/STW/previewStyles/userInputsPreviewDesktop.module.css +732 -0
  161. package/src/widgets/STW/previewStyles/userInputsPreviewMobile.module.css +661 -0
  162. package/src/widgets/STW/previewStyles/wheelPreviewDesktop.module.css +498 -0
  163. package/src/widgets/STW/previewStyles/wheelPreviewMobile.module.css +497 -0
  164. package/src/widgets/STW/stw1.tsx +119 -0
  165. package/src/widgets/STW/stw2Components/wheelDesign.tsx +183 -0
  166. package/src/widgets/STW/stw2Pages/couponDetails.tsx +72 -0
  167. package/src/widgets/STW/stw2Pages/stw2.tsx +212 -0
  168. package/src/widgets/STW/stw2Pages/style.module.css +1226 -0
  169. package/src/widgets/STW/stw2Pages/userDetails.tsx +86 -0
  170. package/src/widgets/STW/stw2Pages/wheel.tsx +117 -0
  171. package/src/widgets/STW/stw2PreviewStyles/thankyouPreviewDesktop.module.css +835 -0
  172. package/src/widgets/STW/stw2PreviewStyles/thankyouPreviewMobile.module.css +787 -0
  173. package/src/widgets/STW/stw2PreviewStyles/userInputsPreviewDesktop.module.css +867 -0
  174. package/src/widgets/STW/stw2PreviewStyles/userInputsPreviewMobile.module.css +798 -0
  175. package/src/widgets/STW/stw2PreviewStyles/wheelPreviewDesktop.module.css +572 -0
  176. package/src/widgets/STW/stw2PreviewStyles/wheelPreviewMobile.module.css +559 -0
  177. package/src/widgets/STW/style.module.css +901 -0
  178. package/src/widgets/STW/userDetails.tsx +150 -0
  179. package/src/widgets/STW/utility.ts +664 -0
  180. package/src/widgets/STW/wheel.tsx +304 -0
  181. package/src/widgets/ScratchCard/ScratchOff/scratchOff.tsx +157 -0
  182. package/src/widgets/ScratchCard/config.ts +152 -0
  183. package/src/widgets/ScratchCard/globalStyle.module.css +931 -0
  184. package/src/widgets/ScratchCard/index.tsx +546 -0
  185. package/src/widgets/ScratchCard/modal.tsx +225 -0
  186. package/src/widgets/ScratchCard/preview.module.css +250 -0
  187. package/src/widgets/ScratchCard/previewMobile.module.css +247 -0
  188. package/src/widgets/ScratchCard/previewStyles/userDetailsPreviewDesktop.module.css +537 -0
  189. package/src/widgets/ScratchCard/previewStyles/userDetailsPreviewMobile.module.css +463 -0
  190. package/src/widgets/ScratchCard/style.module.css +220 -0
  191. package/src/widgets/ShopifyForm/config.ts +168 -0
  192. package/src/widgets/ShopifyForm/index.tsx +214 -0
  193. package/src/widgets/ShopifyForm/previewDesktop.module.css +117 -0
  194. package/src/widgets/ShopifyForm/previewMobile.module.css +131 -0
  195. package/src/widgets/ShopifyForm/shopifyForm.tsx +445 -0
  196. package/src/widgets/ShopifyForm/style.module.css +161 -0
  197. package/src/widgets/SingleButtonRedirection/config.ts +47 -0
  198. package/src/widgets/SingleButtonRedirection/index.tsx +121 -0
  199. package/src/widgets/WebStories/config.ts +105 -0
  200. package/src/widgets/WebStories/index.css +3 -0
  201. package/src/widgets/WebStories/index.tsx +282 -0
  202. package/src/widgets/WebStories/style.module.css +26 -0
  203. package/src/widgets/index.tsx +3 -0
  204. package/src/widgets/utility.ts +31 -0
  205. package/tsconfig.json +12 -0
  206. package/webpack.config.js +239 -0
@@ -0,0 +1,630 @@
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import { ChevronIcon } from '../../assets/svg/ChevronIcon';
3
+ import { DropdownCheckIcon } from '../../assets/svg/DropdownCheckIcon';
4
+ import { RadioIcon } from '../../assets/svg/RadioIcon';
5
+ import { UncheckedCheckboxIcon } from '../../assets/svg/UncheckedCheckboxIcon';
6
+ import { UncheckedRadioIcon } from '../../assets/svg/UncheckedRadioIcon';
7
+ import ErrorIcon from 'assets/svg/ErrorIcon';
8
+
9
+ export interface DropdownOption {
10
+ value: string;
11
+ label: string;
12
+ disabled?: boolean;
13
+ }
14
+
15
+ export interface DropdownProps {
16
+ id?: string;
17
+ value: string[];
18
+ label?: string;
19
+ onChange: (value: string[]) => void;
20
+ options: DropdownOption[];
21
+ placeholder?: string;
22
+ disabled?: boolean;
23
+ isRequired?: boolean;
24
+ className?: string;
25
+ error?: string;
26
+ onValidate?: (value: string[]) => string | null;
27
+ onBlur?: () => void;
28
+ searchable?: boolean;
29
+ maxHeight?: number;
30
+ closeOnSelect?: boolean;
31
+ showSelectAll?: boolean;
32
+ maxSelectedDisplay?: number;
33
+ isMultiSelect?: boolean;
34
+ customTrigger?: (
35
+ selectedItems: DropdownOption[],
36
+ isOpen: boolean,
37
+ ) => React.ReactNode;
38
+ isPreview?: boolean;
39
+ fontColour?: string;
40
+ }
41
+
42
+ interface DropdownState {
43
+ isOpen: boolean;
44
+ isFocused: boolean;
45
+ searchTerm: string;
46
+ dropdownPosition: 'bottom' | 'top';
47
+ }
48
+
49
+ interface DropdownComputedValues {
50
+ hasValue: boolean;
51
+ shouldFloatLabel: boolean;
52
+ shouldCloseOnSelect: boolean;
53
+ shouldShowSelectAll: boolean;
54
+ selectedOptions: DropdownOption[];
55
+ filteredOptions: DropdownOption[];
56
+ isAllSelected: boolean;
57
+ }
58
+
59
+ const useDropdownState = (initialState?: Partial<DropdownState>) => {
60
+ const [state, setState] = useState<DropdownState>({
61
+ isOpen: false,
62
+ isFocused: false,
63
+ searchTerm: '',
64
+ dropdownPosition: 'bottom',
65
+ ...initialState,
66
+ });
67
+
68
+ const updateState = (updates: Partial<DropdownState>) => {
69
+ setState(prev => ({ ...prev, ...updates }));
70
+ };
71
+
72
+ const resetState = () => {
73
+ setState({
74
+ isOpen: false,
75
+ isFocused: false,
76
+ searchTerm: '',
77
+ dropdownPosition: 'bottom',
78
+ });
79
+ };
80
+
81
+ return { state, updateState, resetState };
82
+ };
83
+
84
+ const useDropdownComputedValues = (
85
+ options: DropdownOption[],
86
+ value: string[],
87
+ isMultiSelect: boolean,
88
+ closeOnSelect: boolean | undefined,
89
+ showSelectAll: boolean,
90
+ searchTerm: string,
91
+ searchable: boolean,
92
+ isOpen: boolean,
93
+ isFocused: boolean,
94
+ ): DropdownComputedValues => {
95
+ const selectedOptions = options.filter(option =>
96
+ value.includes(option.value),
97
+ );
98
+
99
+ const filteredOptions =
100
+ !searchable || !searchTerm
101
+ ? options
102
+ : options.filter(option =>
103
+ option.label.toLowerCase().includes(searchTerm.toLowerCase()),
104
+ );
105
+
106
+ const shouldCloseOnSelect =
107
+ closeOnSelect !== undefined ? closeOnSelect : !isMultiSelect;
108
+
109
+ const shouldShowSelectAll = showSelectAll && isMultiSelect;
110
+
111
+ const isAllSelected =
112
+ filteredOptions.length > 0 &&
113
+ filteredOptions
114
+ .filter(opt => !opt.disabled)
115
+ .every(option => value.includes(option.value));
116
+
117
+ const hasValue = value.length > 0;
118
+ const shouldFloatLabel = isFocused || hasValue || isOpen;
119
+
120
+ return {
121
+ hasValue,
122
+ shouldFloatLabel,
123
+ shouldCloseOnSelect,
124
+ shouldShowSelectAll,
125
+ selectedOptions,
126
+ filteredOptions,
127
+ isAllSelected,
128
+ };
129
+ };
130
+
131
+ export const Dropdown: React.FC<DropdownProps> = ({
132
+ value = [],
133
+ onChange,
134
+ options = [],
135
+ placeholder = 'Select options',
136
+ disabled = false,
137
+ isRequired = false,
138
+ className = '',
139
+ label,
140
+ error,
141
+ id,
142
+ onBlur,
143
+ searchable = false,
144
+ maxHeight = 200,
145
+ closeOnSelect,
146
+ showSelectAll = true,
147
+ maxSelectedDisplay = 2,
148
+ isMultiSelect = true,
149
+ customTrigger,
150
+ isPreview = false,
151
+ fontColour,
152
+ }) => {
153
+ const { state, updateState, resetState } = useDropdownState();
154
+ const { isOpen, isFocused, searchTerm, dropdownPosition } = state;
155
+
156
+ const [currentStyle, setCurrentStyle] = useState<Record<
157
+ string,
158
+ string
159
+ > | null>(null);
160
+ const dropdownRef = useRef<HTMLDivElement>(null);
161
+ const inputRef = useRef<HTMLInputElement>(null);
162
+
163
+ const computedValues = useDropdownComputedValues(
164
+ options,
165
+ value,
166
+ isMultiSelect,
167
+ closeOnSelect,
168
+ showSelectAll,
169
+ searchTerm,
170
+ searchable,
171
+ isOpen,
172
+ isFocused,
173
+ );
174
+
175
+ const {
176
+ selectedOptions,
177
+ filteredOptions,
178
+ shouldCloseOnSelect,
179
+ shouldShowSelectAll,
180
+ isAllSelected,
181
+ hasValue,
182
+ shouldFloatLabel,
183
+ } = computedValues;
184
+
185
+ useEffect(() => {
186
+ import('./Dropdown.module.css').then(stylesModule => {
187
+ setCurrentStyle(stylesModule.default);
188
+ });
189
+ }, []);
190
+
191
+ useEffect(() => {
192
+ const handleClickOutside = (event: MouseEvent) => {
193
+ if (
194
+ dropdownRef.current &&
195
+ !dropdownRef.current.contains(event.target as Node)
196
+ ) {
197
+ resetState();
198
+ onBlur?.();
199
+ }
200
+ };
201
+
202
+ const handleResize = () => {
203
+ if (isOpen && dropdownRef.current) {
204
+ // Recalculate position on resize
205
+ const rect = dropdownRef.current.getBoundingClientRect();
206
+ const viewportHeight = window.innerHeight;
207
+ const spaceBelow = viewportHeight - rect.bottom;
208
+ const spaceAbove = rect.top;
209
+ const estimatedDropdownHeight = Math.min(maxHeight + 80, 320);
210
+ const minSpaceBuffer = 20;
211
+
212
+ const newDropdownPosition =
213
+ spaceBelow < estimatedDropdownHeight + minSpaceBuffer &&
214
+ spaceAbove > estimatedDropdownHeight + minSpaceBuffer
215
+ ? 'top'
216
+ : 'bottom';
217
+
218
+ updateState({ dropdownPosition: newDropdownPosition });
219
+ }
220
+ };
221
+
222
+ document.addEventListener('mousedown', handleClickOutside);
223
+ window.addEventListener('resize', handleResize);
224
+
225
+ return () => {
226
+ document.removeEventListener('mousedown', handleClickOutside);
227
+ window.removeEventListener('resize', handleResize);
228
+ };
229
+ }, [onBlur, resetState, isOpen, maxHeight, updateState]);
230
+
231
+ const handleToggle = () => {
232
+ if (disabled) return;
233
+
234
+ const newIsOpen = !isOpen;
235
+
236
+ // Calculate dropdown position when opening
237
+ let newDropdownPosition: 'bottom' | 'top' = 'bottom';
238
+
239
+ if (newIsOpen && dropdownRef.current) {
240
+ const rect = dropdownRef.current.getBoundingClientRect();
241
+ const viewportHeight = window.innerHeight;
242
+ const spaceBelow = viewportHeight - rect.bottom;
243
+ const spaceAbove = rect.top;
244
+ const estimatedDropdownHeight = Math.min(maxHeight + 80, 320); // Adding buffer for search/select all/padding
245
+ const minSpaceBuffer = 20; // Minimum space to keep from viewport edge
246
+
247
+ // If there's not enough space below but enough space above, open upward
248
+ if (
249
+ spaceBelow < estimatedDropdownHeight + minSpaceBuffer &&
250
+ spaceAbove > estimatedDropdownHeight + minSpaceBuffer
251
+ ) {
252
+ newDropdownPosition = 'top';
253
+ }
254
+ }
255
+
256
+ updateState({
257
+ isOpen: newIsOpen,
258
+ isFocused: newIsOpen,
259
+ dropdownPosition: newDropdownPosition,
260
+ });
261
+
262
+ if (newIsOpen && searchable) {
263
+ setTimeout(() => inputRef.current?.focus(), 0);
264
+ }
265
+ };
266
+
267
+ const handleOptionClick = (optionValue: string) => {
268
+ const option = options.find(opt => opt.value === optionValue);
269
+ if (option?.disabled) return;
270
+
271
+ const newValue = isMultiSelect
272
+ ? value.includes(optionValue)
273
+ ? value.filter(v => v !== optionValue)
274
+ : [...value, optionValue]
275
+ : value.includes(optionValue)
276
+ ? []
277
+ : [optionValue];
278
+
279
+ onChange(newValue);
280
+
281
+ if (
282
+ !isMultiSelect ||
283
+ (shouldCloseOnSelect && !value.includes(optionValue))
284
+ ) {
285
+ resetState();
286
+ onBlur?.();
287
+ }
288
+ };
289
+
290
+ const handleSelectAll = () => {
291
+ const availableOptions = filteredOptions.filter(option => !option.disabled);
292
+
293
+ if (isAllSelected) {
294
+ const newValue = value.filter(
295
+ v => !availableOptions.some(option => option.value === v),
296
+ );
297
+ onChange(newValue);
298
+ } else {
299
+ const newValues = availableOptions.map(option => option.value);
300
+ const combinedValue = Array.from(new Set([...value, ...newValues]));
301
+ onChange(combinedValue);
302
+ }
303
+ };
304
+
305
+ const handleClearAll = () => {
306
+ onChange([]);
307
+ };
308
+
309
+ const handleRemoveItem = (optionValue: string, event: React.MouseEvent) => {
310
+ event.stopPropagation();
311
+ const newValue = value.filter(v => v !== optionValue);
312
+ onChange(newValue);
313
+ };
314
+
315
+ const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
316
+ updateState({ searchTerm: e.target.value });
317
+ };
318
+
319
+ const handleKeyDown = (e: React.KeyboardEvent) => {
320
+ if (e.key === 'Escape') {
321
+ resetState();
322
+ } else if (e.key === 'Backspace' && searchTerm === '' && value.length > 0) {
323
+ const newValue = value.slice(0, -1);
324
+ onChange(newValue);
325
+ }
326
+ };
327
+
328
+ if (!currentStyle) {
329
+ return <div style={{ height: '80px' }} />;
330
+ }
331
+
332
+ const wrapperClasses = [
333
+ currentStyle['bik-dropdown__inputWrapper'],
334
+ (isFocused || isOpen) &&
335
+ currentStyle['bik-dropdown__inputWrapper--focused'],
336
+ error && currentStyle['bik-dropdown__inputWrapper--error'],
337
+ disabled && currentStyle['bik-dropdown__inputWrapper--disabled'],
338
+ isOpen && currentStyle['bik-dropdown__inputWrapper--open'],
339
+ hasValue && currentStyle['bik-dropdown__inputWrapper--hasSelectedItems'],
340
+ ]
341
+ .filter(Boolean)
342
+ .join(' ');
343
+
344
+ const labelClasses = [
345
+ currentStyle['bik-dropdown__label'],
346
+ shouldFloatLabel && currentStyle['bik-dropdown__label--floated'],
347
+ error && currentStyle['bik-dropdown__label--error'],
348
+ disabled && currentStyle['bik-dropdown__label--disabled'],
349
+ hasValue && currentStyle['bik-dropdown__label--hasValue'],
350
+ hasValue && currentStyle['bik-dropdown__label--hasSelectedItems'],
351
+ ]
352
+ .filter(Boolean)
353
+ .join(' ');
354
+
355
+ const wrapperStyle = {
356
+ borderColor: error ? '#b92321' : '#e0e0e0',
357
+ ...(fontColour ? { color: fontColour } : {}),
358
+ } as React.CSSProperties;
359
+
360
+ return (
361
+ <div
362
+ className={`${currentStyle['bik-dropdown__container']} ${className}`}
363
+ ref={dropdownRef}
364
+ data-mode={isPreview ? 'preview' : 'default'}
365
+ >
366
+ <div className={wrapperClasses} style={wrapperStyle}>
367
+ <div
368
+ className={currentStyle['bik-dropdown__input']}
369
+ onClick={handleToggle}
370
+ style={fontColour ? { color: fontColour } : undefined}
371
+ >
372
+ {customTrigger ? (
373
+ customTrigger(selectedOptions, isOpen)
374
+ ) : searchable && isOpen ? (
375
+ <div className={currentStyle?.['bik-dropdown__searchContainer']}>
376
+ {value.length === 0 ? (
377
+ <span
378
+ className={currentStyle?.['bik-dropdown__placeholder']}
379
+ style={fontColour ? { color: fontColour } : undefined}
380
+ >
381
+ {placeholder}
382
+ </span>
383
+ ) : !isMultiSelect && value.length === 1 ? (
384
+ <div className={currentStyle?.['bik-dropdown__selectedItems']}>
385
+ <span
386
+ className={
387
+ currentStyle?.['bik-dropdown__singleSelectedItem']
388
+ }
389
+ style={fontColour ? { color: fontColour } : undefined}
390
+ >
391
+ {selectedOptions[0]?.label}
392
+ </span>
393
+ </div>
394
+ ) : value.length <= maxSelectedDisplay ? (
395
+ <div className={currentStyle?.['bik-dropdown__selectedItems']}>
396
+ {selectedOptions.map(option => (
397
+ <span
398
+ key={option.value}
399
+ className={currentStyle?.['bik-dropdown__selectedItem']}
400
+ style={fontColour ? { color: fontColour } : undefined}
401
+ >
402
+ {option.label}
403
+ <button
404
+ type="button"
405
+ className={currentStyle?.['bik-dropdown__removeButton']}
406
+ onClick={e => handleRemoveItem(option.value, e)}
407
+ disabled={disabled}
408
+ >
409
+ ×
410
+ </button>
411
+ </span>
412
+ ))}
413
+ </div>
414
+ ) : (
415
+ <div className={currentStyle?.['bik-dropdown__selectedItems']}>
416
+ <span
417
+ className={currentStyle?.['bik-dropdown__selectedCount']}
418
+ >
419
+ {value.length} items selected
420
+ </span>
421
+ {value.length > 0 && (
422
+ <button
423
+ type="button"
424
+ className={currentStyle?.['bik-dropdown__clearButton']}
425
+ onClick={e => {
426
+ e.stopPropagation();
427
+ handleClearAll();
428
+ }}
429
+ disabled={disabled}
430
+ >
431
+ Clear all
432
+ </button>
433
+ )}
434
+ </div>
435
+ )}
436
+ <input
437
+ id={id}
438
+ ref={inputRef}
439
+ type="text"
440
+ value={searchTerm}
441
+ onChange={handleSearchChange}
442
+ onKeyDown={handleKeyDown}
443
+ placeholder="Search..."
444
+ className={currentStyle?.['bik-dropdown__search']}
445
+ style={fontColour ? { color: fontColour } : undefined}
446
+ />
447
+ </div>
448
+ ) : value.length === 0 ? (
449
+ <span
450
+ className={currentStyle?.['bik-dropdown__placeholder']}
451
+ style={fontColour ? { color: fontColour } : undefined}
452
+ >
453
+ {placeholder}
454
+ </span>
455
+ ) : !isMultiSelect && value.length === 1 ? (
456
+ <div className={currentStyle?.['bik-dropdown__selectedItems']}>
457
+ <span
458
+ className={currentStyle?.['bik-dropdown__singleSelectedItem']}
459
+ style={fontColour ? { color: fontColour } : undefined}
460
+ >
461
+ {selectedOptions[0]?.label}
462
+ </span>
463
+ </div>
464
+ ) : value.length <= maxSelectedDisplay ? (
465
+ <div className={currentStyle?.['bik-dropdown__selectedItems']}>
466
+ {selectedOptions.map(option => (
467
+ <span
468
+ key={option.value}
469
+ className={currentStyle?.['bik-dropdown__selectedItem']}
470
+ style={fontColour ? { color: fontColour } : undefined}
471
+ >
472
+ {option.label}
473
+ <button
474
+ type="button"
475
+ className={currentStyle?.['bik-dropdown__removeButton']}
476
+ onClick={e => handleRemoveItem(option.value, e)}
477
+ disabled={disabled}
478
+ >
479
+ ×
480
+ </button>
481
+ </span>
482
+ ))}
483
+ </div>
484
+ ) : (
485
+ <div className={currentStyle?.['bik-dropdown__selectedItems']}>
486
+ <span className={currentStyle?.['bik-dropdown__selectedCount']}>
487
+ {value.length} items selected
488
+ </span>
489
+ {value.length > 0 && (
490
+ <button
491
+ type="button"
492
+ className={currentStyle?.['bik-dropdown__clearButton']}
493
+ onClick={e => {
494
+ e.stopPropagation();
495
+ handleClearAll();
496
+ }}
497
+ disabled={disabled}
498
+ >
499
+ Clear all
500
+ </button>
501
+ )}
502
+ </div>
503
+ )}
504
+ </div>
505
+ {label && (
506
+ <div className={labelClasses}>
507
+ {label}
508
+ {isRequired && (
509
+ <span className={currentStyle['bik-dropdown__required']}>*</span>
510
+ )}
511
+ </div>
512
+ )}
513
+ <div className={currentStyle['bik-dropdown__icon']}>
514
+ <ChevronIcon
515
+ className={`${currentStyle['bik-dropdown__chevron']} ${
516
+ isOpen ? currentStyle['bik-dropdown__chevron--open'] : ''
517
+ }`}
518
+ />
519
+ </div>
520
+ </div>
521
+
522
+ {isOpen && (
523
+ <div
524
+ className={`${currentStyle['bik-dropdown__options']} ${
525
+ dropdownPosition === 'top'
526
+ ? currentStyle['bik-dropdown__options--top']
527
+ : ''
528
+ }`}
529
+ style={{ maxHeight }}
530
+ >
531
+ {searchable && (
532
+ <div className={currentStyle?.['bik-dropdown__searchWrapper']}>
533
+ <input
534
+ type="text"
535
+ value={searchTerm}
536
+ onChange={handleSearchChange}
537
+ onKeyDown={handleKeyDown}
538
+ placeholder="Search options..."
539
+ className={currentStyle?.['bik-dropdown__searchInput']}
540
+ />
541
+ </div>
542
+ )}
543
+ {shouldShowSelectAll &&
544
+ filteredOptions.filter(opt => !opt.disabled).length > 1 && (
545
+ <div className={currentStyle?.['bik-dropdown__selectAllWrapper']}>
546
+ <div
547
+ className={`${currentStyle?.['bik-dropdown__option']} ${currentStyle?.['bik-dropdown__selectAllOption']}`}
548
+ onClick={handleSelectAll}
549
+ style={fontColour ? { color: fontColour } : undefined}
550
+ >
551
+ <div className={currentStyle?.['bik-dropdown__checkbox']}>
552
+ {isAllSelected ? (
553
+ <DropdownCheckIcon />
554
+ ) : (
555
+ <UncheckedCheckboxIcon />
556
+ )}
557
+ </div>
558
+ <span style={fontColour ? { color: fontColour } : undefined}>
559
+ {isAllSelected ? 'Deselect All' : 'Select All'}
560
+ </span>
561
+ </div>
562
+ </div>
563
+ )}
564
+ <div className={currentStyle['bik-dropdown__optionsList']}>
565
+ {filteredOptions.length === 0 ? (
566
+ <div className={currentStyle?.['bik-dropdown__noOptions']}>
567
+ No options found
568
+ </div>
569
+ ) : (
570
+ filteredOptions.map(option => (
571
+ <div
572
+ key={option.value}
573
+ className={`${currentStyle?.['bik-dropdown__option']} ${
574
+ value.includes(option.value)
575
+ ? currentStyle?.['bik-dropdown__option--selected']
576
+ : ''
577
+ } ${
578
+ option.disabled
579
+ ? currentStyle?.['bik-dropdown__option--disabled']
580
+ : ''
581
+ }`}
582
+ onClick={() => handleOptionClick(option.value)}
583
+ style={fontColour ? { color: fontColour } : undefined}
584
+ >
585
+ <div
586
+ className={
587
+ isMultiSelect
588
+ ? currentStyle?.['bik-dropdown__checkbox']
589
+ : currentStyle?.['bik-dropdown__radio']
590
+ }
591
+ >
592
+ {isMultiSelect ? (
593
+ value.includes(option.value) ? (
594
+ <DropdownCheckIcon />
595
+ ) : (
596
+ <UncheckedCheckboxIcon />
597
+ )
598
+ ) : value.includes(option.value) ? (
599
+ <RadioIcon />
600
+ ) : (
601
+ <UncheckedRadioIcon />
602
+ )}
603
+ </div>
604
+ <span
605
+ className={currentStyle?.['bik-dropdown__optionLabel']}
606
+ style={fontColour ? { color: fontColour } : undefined}
607
+ >
608
+ {option.label}
609
+ </span>
610
+ </div>
611
+ ))
612
+ )}
613
+ </div>
614
+ </div>
615
+ )}
616
+
617
+ {error && (
618
+ <div
619
+ className={currentStyle['bik-dropdown__error']}
620
+ style={{ display: 'flex', alignItems: 'center' }}
621
+ >
622
+ <div className={currentStyle['bik-dropdown__errorIcon']}>
623
+ <ErrorIcon size={12} />
624
+ </div>
625
+ {error}
626
+ </div>
627
+ )}
628
+ </div>
629
+ );
630
+ };