bitboss-ui 2.1.113 → 2.1.115
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/dist/ai/BaseButton.md +448 -0
- package/dist/ai/BaseCheckbox.md +494 -0
- package/dist/ai/BaseCheckboxGroup.md +597 -0
- package/dist/ai/BaseColorInput.md +461 -0
- package/dist/ai/BaseDatePicker.md +739 -0
- package/dist/ai/BaseDatePickerInput.md +1517 -0
- package/dist/ai/BaseDialog.md +610 -0
- package/dist/ai/BaseInputContainer.md +570 -0
- package/dist/ai/BaseNumberInput.md +509 -0
- package/dist/ai/BaseRadio.md +405 -0
- package/dist/ai/BaseRadioGroup.md +535 -0
- package/dist/ai/BaseRating.md +489 -0
- package/dist/ai/BaseSelect.md +1720 -0
- package/dist/ai/BaseSlider.md +871 -0
- package/dist/ai/BaseSwitch.md +322 -0
- package/dist/ai/BaseSwitchGroup.md +298 -0
- package/dist/ai/BaseTag.md +624 -0
- package/dist/ai/BaseTextInput.md +392 -0
- package/dist/ai/BaseTextarea.md +398 -0
- package/dist/ai/BbAccordion.md +135 -0
- package/dist/ai/BbAlert.md +226 -0
- package/dist/ai/BbAvatar.md +200 -0
- package/dist/ai/BbBadge.md +185 -0
- package/dist/ai/BbBreadcrumbs.md +536 -0
- package/dist/ai/BbButton.md +687 -0
- package/dist/ai/BbCheckbox.md +280 -0
- package/dist/ai/BbCheckboxGroup.md +387 -0
- package/dist/ai/BbChip.md +148 -0
- package/dist/ai/BbCollapsible.md +119 -0
- package/dist/ai/BbColorInput.md +345 -0
- package/dist/ai/BbColorPalette.md +360 -0
- package/dist/ai/BbConfirm.md +160 -0
- package/dist/ai/BbDatePickerInput.md +414 -0
- package/dist/ai/BbDialog.md +135 -0
- package/dist/ai/BbDropdown.md +765 -0
- package/dist/ai/BbDropdownButton.md +629 -0
- package/dist/ai/BbDropzone.md +504 -0
- package/dist/ai/BbIcon.md +238 -0
- package/dist/ai/BbIntersection.md +121 -0
- package/dist/ai/BbNumberInput.md +372 -0
- package/dist/ai/BbOffCanvas.md +549 -0
- package/dist/ai/BbPagination.md +562 -0
- package/dist/ai/BbPopover.md +580 -0
- package/dist/ai/BbProgress.md +97 -0
- package/dist/ai/BbRadio.md +256 -0
- package/dist/ai/BbRadioGroup.md +373 -0
- package/dist/ai/BbRating.md +245 -0
- package/dist/ai/BbRatio.md +62 -0
- package/dist/ai/BbRows.md +307 -0
- package/dist/ai/BbSelect.md +562 -0
- package/dist/ai/BbSelectPopover.md +2010 -0
- package/dist/ai/BbSlider.md +274 -0
- package/dist/ai/BbSmoothHeight.md +167 -0
- package/dist/ai/BbSpinner.md +154 -0
- package/dist/ai/BbSwitch.md +151 -0
- package/dist/ai/BbSwitchGroup.md +237 -0
- package/dist/ai/BbTab.md +954 -0
- package/dist/ai/BbTable.md +1624 -0
- package/dist/ai/BbTag.md +315 -0
- package/dist/ai/BbTextInput.md +357 -0
- package/dist/ai/BbTextarea.md +277 -0
- package/dist/ai/BbToast.md +219 -0
- package/dist/ai/BbTooltip.md +353 -0
- package/dist/ai/BbTree.md +271 -0
- package/dist/ai/ChipsBox.md +211 -0
- package/dist/ai/ClearableButton.md +67 -0
- package/dist/ai/CommaBox.md +212 -0
- package/dist/ai/CommonInputInnerContainer.md +419 -0
- package/dist/ai/CommonInputOuterContainer.md +56 -0
- package/dist/ai/CommonPopover.md +446 -0
- package/dist/ai/ErrorIcon.md +61 -0
- package/dist/ai/FlatListBox.md +382 -0
- package/dist/ai/GroupedListBox.md +538 -0
- package/dist/ai/ListBox.md +234 -0
- package/dist/ai/OptionsContainer.md +257 -0
- package/dist/ai/index.md +124 -0
- package/dist/components/BaseButton/BaseButton.vue.d.ts +2 -163
- package/dist/components/BaseButton/types.d.ts +158 -0
- package/dist/components/BaseCheckbox/BaseCheckbox.vue.d.ts +4 -4
- package/dist/components/BaseCheckboxGroup/BaseCheckboxGroup.vue.d.ts +2 -2
- package/dist/components/BaseCheckboxGroup/types.d.ts +16 -9
- package/dist/components/BaseColorInput/BaseColorInput.vue.d.ts +12 -52
- package/dist/components/BaseDatePicker/BaseDatePicker.vue.d.ts +4 -76
- package/dist/components/BaseDatePicker/types.d.ts +100 -0
- package/dist/components/BaseDatePickerInput/BaseDatePickerInput.vue.d.ts +18 -315
- package/dist/components/BaseDatePickerInput/types.d.ts +206 -0
- package/dist/components/BaseDialog/BaseDialog.vue.d.ts +6 -156
- package/dist/components/BaseDialog/types.d.ts +180 -0
- package/dist/components/BaseInputContainer/BaseInputContainer.vue.d.ts +1 -107
- package/dist/components/BaseInputContainer/types.d.ts +126 -0
- package/dist/components/BaseNumberInput/BaseNumberInput.vue.d.ts +7 -170
- package/dist/components/BaseNumberInput/types.d.ts +191 -0
- package/dist/components/BaseRadio/BaseRadio.vue.d.ts +6 -119
- package/dist/components/BaseRadio/types.d.ts +173 -0
- package/dist/components/BaseRadioGroup/BaseRadioGroup.vue.d.ts +4 -274
- package/dist/components/BaseRadioGroup/types.d.ts +240 -0
- package/dist/components/BaseRating/BaseRating.vue.d.ts +5 -106
- package/dist/components/BaseRating/types.d.ts +144 -0
- package/dist/components/BaseSelect/BaseSelect.vue.d.ts +2 -363
- package/dist/components/BaseSelect/types.d.ts +457 -0
- package/dist/components/BaseSlider/BaseSlider.vue.d.ts +6 -178
- package/dist/components/BaseSlider/types.d.ts +201 -0
- package/dist/components/BaseSwitch/BaseSwitch.vue.d.ts +7 -35
- package/dist/components/BaseSwitch/types.d.ts +25 -0
- package/dist/components/BaseSwitchGroup/BaseSwitchGroup.vue.d.ts +5 -11
- package/dist/components/BaseSwitchGroup/types.d.ts +8 -0
- package/dist/components/BaseTag/BaseTag.vue.d.ts +27 -222
- package/dist/components/BaseTag/types.d.ts +136 -0
- package/dist/components/BaseTextInput/BaseTextInput.vue.d.ts +5 -141
- package/dist/components/BaseTextInput/types.d.ts +166 -0
- package/dist/components/BaseTextarea/BaseTextarea.vue.d.ts +5 -131
- package/dist/components/BaseTextarea/types.d.ts +151 -0
- package/dist/components/BbAccordion/BbAccordion.vue.d.ts +3 -51
- package/dist/components/BbAccordion/types.d.ts +32 -0
- package/dist/components/BbAlert/BbAlert.vue.d.ts +3 -50
- package/dist/components/BbAlert/types.d.ts +42 -0
- package/dist/components/BbAvatar/BbAvatar.vue.d.ts +3 -23
- package/dist/components/BbAvatar/types.d.ts +34 -0
- package/dist/components/BbBadge/BbBadge.vue.d.ts +3 -40
- package/dist/components/BbBadge/types.d.ts +30 -0
- package/dist/components/BbBreadcrumbs/BbBreadcrumbs.vue.d.ts +14 -178
- package/dist/components/BbBreadcrumbs/types.d.ts +109 -0
- package/dist/components/BbButton/BbButton.vue.d.ts +4 -163
- package/dist/components/BbButton/types.d.ts +159 -0
- package/dist/components/BbCheckbox/BbCheckbox.vue.d.ts +7 -165
- package/dist/components/BbCheckbox/types.d.ts +130 -0
- package/dist/components/BbCheckboxGroup/BbCheckboxGroup.vue.d.ts +7 -324
- package/dist/components/BbCheckboxGroup/types.d.ts +189 -0
- package/dist/components/BbChip/BbChip.vue.d.ts +6 -28
- package/dist/components/BbChip/types.d.ts +23 -0
- package/dist/components/BbCollapsible/BbCollapsible.vue.d.ts +3 -24
- package/dist/components/BbCollapsible/types.d.ts +20 -0
- package/dist/components/BbColorInput/BbColorInput.vue.d.ts +10 -151
- package/dist/components/BbColorInput/types.d.ts +131 -0
- package/dist/components/BbColorPalette/BbColorPalette.vue.d.ts +2 -112
- package/dist/components/BbColorPalette/types.d.ts +127 -0
- package/dist/components/BbDatePickerInput/BbDatePickerInput.vue.d.ts +6 -212
- package/dist/components/BbDatePickerInput/types.d.ts +180 -0
- package/dist/components/BbDialog/BbDialog.vue.d.ts +2 -2
- package/dist/components/BbDialog/types.d.ts +1 -0
- package/dist/components/BbDropdown/BbDropdown.vue.d.ts +21 -247
- package/dist/components/BbDropdown/types.d.ts +147 -0
- package/dist/components/BbDropdownButton/BbDropdownButton.vue.d.ts +16 -209
- package/dist/components/BbDropdownButton/types.d.ts +114 -0
- package/dist/components/BbDropzone/BbDropzone.vue.d.ts +7 -86
- package/dist/components/BbDropzone/types.d.ts +67 -0
- package/dist/components/BbIcon/BbIcon.vue.d.ts +2 -26
- package/dist/components/BbIcon/types.d.ts +28 -0
- package/dist/components/BbIntersection/BbIntersection.vue.d.ts +3 -41
- package/dist/components/BbIntersection/types.d.ts +36 -0
- package/dist/components/BbNumberInput/BbNumberInput.vue.d.ts +44 -175
- package/dist/components/BbNumberInput/types.d.ts +130 -0
- package/dist/components/BbOffCanvas/BbOffCanvas.vue.d.ts +5 -93
- package/dist/components/BbOffCanvas/types.d.ts +97 -0
- package/dist/components/BbPagination/BbPagination.vue.d.ts +4 -87
- package/dist/components/BbPagination/types.d.ts +80 -0
- package/dist/components/BbPopover/BbPopover.vue.d.ts +9 -135
- package/dist/components/BbPopover/types.d.ts +99 -0
- package/dist/components/BbProgress/BbProgress.vue.d.ts +2 -14
- package/dist/components/BbProgress/types.d.ts +20 -0
- package/dist/components/BbRadio/BbRadio.vue.d.ts +7 -150
- package/dist/components/BbRadio/types.d.ts +117 -0
- package/dist/components/BbRadioGroup/BbRadioGroup.vue.d.ts +7 -322
- package/dist/components/BbRadioGroup/types.d.ts +182 -0
- package/dist/components/BbRating/BbRating.vue.d.ts +10 -113
- package/dist/components/BbRating/types.d.ts +105 -0
- package/dist/components/BbRatio/BbRatio.vue.d.ts +3 -18
- package/dist/components/BbRatio/types.d.ts +15 -0
- package/dist/components/BbSelect/BbSelect.vue.d.ts +7 -375
- package/dist/components/BbSelect/types.d.ts +351 -0
- package/dist/components/BbSelectPopover/BbSelectPopover.vue.d.ts +1 -1
- package/dist/components/BbSelectPopover/types.d.ts +351 -0
- package/dist/components/BbSlider/BbSlider.vue.d.ts +10 -129
- package/dist/components/BbSlider/types.d.ts +123 -0
- package/dist/components/BbSmoothHeight/BbSmoothHeight.vue.d.ts +2 -23
- package/dist/components/BbSmoothHeight/types.d.ts +24 -0
- package/dist/components/BbSpinner/BbSpinner.vue.d.ts +3 -5
- package/dist/components/BbSpinner/types.d.ts +8 -0
- package/dist/components/BbSwitch/BbSwitch.vue.d.ts +9 -65
- package/dist/components/BbSwitch/types.d.ts +29 -0
- package/dist/components/BbSwitchGroup/BbSwitchGroup.vue.d.ts +7 -190
- package/dist/components/BbSwitchGroup/types.d.ts +81 -0
- package/dist/components/BbTab/BbTab.vue.d.ts +9 -247
- package/dist/components/BbTab/types.d.ts +186 -0
- package/dist/components/BbTag/BbTag.vue.d.ts +6 -156
- package/dist/components/BbTag/types.d.ts +158 -0
- package/dist/components/BbTextInput/BbTextInput.vue.d.ts +10 -152
- package/dist/components/BbTextInput/types.d.ts +137 -0
- package/dist/components/BbTextarea/BbTextarea.vue.d.ts +10 -142
- package/dist/components/BbTextarea/types.d.ts +123 -0
- package/dist/components/BbToast/BbToast.vue.d.ts +2 -6
- package/dist/components/BbToast/types.d.ts +8 -0
- package/dist/components/BbTooltip/BbTooltip.vue.d.ts +8 -65
- package/dist/components/BbTooltip/types.d.ts +55 -0
- package/dist/components/BbTree/BbTree.vue.d.ts +2 -65
- package/dist/components/BbTree/types.d.ts +69 -0
- package/dist/components/{ChipsBox.vue.d.ts → ChipsBox/ChipsBox.vue.d.ts} +5 -6
- package/dist/components/ChipsBox/types.d.ts +14 -0
- package/dist/components/{ClearableButton.vue.d.ts → ClearableButton/ClearableButton.vue.d.ts} +2 -0
- package/dist/components/ClearableButton/types.d.ts +3 -0
- package/dist/components/{CommaBox.vue.d.ts → CommaBox/CommaBox.vue.d.ts} +5 -6
- package/dist/components/CommaBox/types.d.ts +14 -0
- package/dist/components/CommonInputInnerContainer/CommonInputInnerContainer.vue.d.ts +25 -0
- package/dist/components/CommonInputInnerContainer/types.d.ts +47 -0
- package/dist/components/CommonInputOuterContainer/CommonInputOuterContainer.vue.d.ts +17 -0
- package/dist/components/CommonInputOuterContainer/types.d.ts +16 -0
- package/dist/components/{CommonPopover.vue.d.ts → CommonPopover/CommonPopover.vue.d.ts} +5 -30
- package/dist/components/CommonPopover/types.d.ts +43 -0
- package/dist/components/{ErrorIcon.vue.d.ts → ErrorIcon/ErrorIcon.vue.d.ts} +2 -0
- package/dist/components/ErrorIcon/types.d.ts +3 -0
- package/dist/components/FlatListBox/types.d.ts +97 -0
- package/dist/components/GroupedListBox/types.d.ts +118 -0
- package/dist/components/ListBox/ListBox.vue.d.ts +30 -0
- package/dist/components/ListBox/types.d.ts +133 -0
- package/dist/components/OptionsContainer/OptionsContainer.vue.d.ts +13 -0
- package/dist/components/OptionsContainer/types.d.ts +96 -0
- package/dist/composables/useBbConfig.d.ts +1 -1
- package/dist/composables/useConfirm.d.ts +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.ts +18 -18
- package/dist/index109.js +9 -9
- package/dist/index110.js +50 -49
- package/dist/index114.js +1 -1
- package/dist/index118.js +1 -1
- package/dist/index122.js +1 -0
- package/dist/index124.js +4 -4
- package/dist/index126.js +13 -13
- package/dist/index132.js +22 -19
- package/dist/index134.js +1 -1
- package/dist/index136.js +5 -5
- package/dist/index138.js +1 -1
- package/dist/index14.js +1 -1
- package/dist/index140.js +18 -17
- package/dist/index144.js +1 -1
- package/dist/index146.js +2 -2
- package/dist/index149.js +2 -2
- package/dist/index16.js +3 -3
- package/dist/index18.js +3 -3
- package/dist/index20.js +70 -59
- package/dist/index22.js +14 -14
- package/dist/index221.js +138 -2
- package/dist/index222.js +2 -138
- package/dist/index224.js +5 -34
- package/dist/index225.js +7 -32
- package/dist/index226.js +32 -26
- package/dist/index227.js +7 -0
- package/dist/index228.js +5 -5
- package/dist/index229.js +5 -8
- package/dist/index230.js +5 -7
- package/dist/index231.js +3 -2
- package/dist/index232.js +2 -9
- package/dist/index233.js +6 -13
- package/dist/index234.js +8 -3
- package/dist/index235.js +268 -2
- package/dist/index236.js +52 -11
- package/dist/index237.js +50 -6
- package/dist/index238.js +32 -3
- package/dist/index239.js +60 -3
- package/dist/index24.js +10 -10
- package/dist/index240.js +13 -2
- package/dist/index241.js +187 -17
- package/dist/index242.js +3 -12
- package/dist/index243.js +2 -51
- package/dist/index244.js +2 -18
- package/dist/index245.js +2 -12
- package/dist/index246.js +12 -16
- package/dist/index247.js +11 -28
- package/dist/index248.js +48 -15
- package/dist/index249.js +17 -4
- package/dist/index250.js +2 -2
- package/dist/index252.js +2 -2
- package/dist/index254.js +3 -135
- package/dist/index255.js +4 -0
- package/dist/index256.js +4 -107
- package/dist/index257.js +19 -12
- package/dist/index258.js +6 -2
- package/dist/index259.js +16 -7
- package/dist/index26.js +3 -3
- package/dist/index260.js +86 -7
- package/dist/index262.js +32 -0
- package/dist/index263.js +18 -5
- package/dist/index264.js +12 -5
- package/dist/index265.js +18 -5
- package/dist/index266.js +2 -5
- package/dist/index267.js +7 -5
- package/dist/index268.js +7 -5
- package/dist/index269.js +3 -67
- package/dist/index270.js +4 -33
- package/dist/index271.js +5 -2
- package/dist/index272.js +5 -2
- package/dist/index273.js +5 -3
- package/dist/index274.js +135 -4
- package/dist/index276.js +9 -6
- package/dist/index277.js +7 -11
- package/dist/index278.js +23 -5
- package/dist/index279.js +3 -5
- package/dist/index28.js +57 -55
- package/dist/index280.js +21 -266
- package/dist/index281.js +364 -43
- package/dist/index283.js +32 -31
- package/dist/index284.js +3 -60
- package/dist/index285.js +25 -4
- package/dist/index286.js +3 -20
- package/dist/index287.js +18 -5
- package/dist/index288.js +12 -373
- package/dist/index289.js +109 -0
- package/dist/index290.js +11 -6
- package/dist/index291.js +66 -15
- package/dist/index292.js +32 -10
- package/dist/index294.js +5 -8
- package/dist/index295.js +9 -20
- package/dist/index296.js +2 -8
- package/dist/index297.js +9 -23
- package/dist/index298.js +52 -24
- package/dist/index299.js +5 -188
- package/dist/index30.js +3 -3
- package/dist/index300.js +21 -3
- package/dist/index301.js +28 -3
- package/dist/index303.js +9 -0
- package/dist/index304.js +2 -7
- package/dist/index305.js +280 -3
- package/dist/index306.js +2 -2
- package/dist/index307.js +16 -5
- package/dist/index308.js +2 -7
- package/dist/index309.js +16 -3
- package/dist/index310.js +2 -3
- package/dist/index311.js +27 -3
- package/dist/index312.js +2 -2
- package/dist/index313.js +2 -28
- package/dist/index314.js +2 -17
- package/dist/index315.js +2 -4
- package/dist/index316.js +1 -1
- package/dist/index317.js +28 -3
- package/dist/index318.js +2 -280
- package/dist/index319.js +7 -2
- package/dist/index32.js +2 -2
- package/dist/index320.js +719 -125
- package/dist/index321.js +366 -2
- package/dist/index322.js +56 -14
- package/dist/index323.js +4 -2
- package/dist/index324.js +3 -16
- package/dist/index325.js +17 -2
- package/dist/index326.js +3 -16
- package/dist/index327.js +3 -2
- package/dist/index328.js +3 -19
- package/dist/index329.js +3 -2
- package/dist/index330.js +120 -22
- package/dist/index331.js +2 -2
- package/dist/index332.js +15 -2
- package/dist/index333.js +2 -2
- package/dist/index334.js +19 -2
- package/dist/index335.js +2 -2
- package/dist/index336.js +5 -2
- package/dist/index337.js +5 -3
- package/dist/index338.js +2 -4
- package/dist/index339.js +4 -719
- package/dist/index34.js +8 -8
- package/dist/index340.js +2 -366
- package/dist/index341.js +3 -57
- package/dist/index342.js +3 -6
- package/dist/index343.js +6 -5
- package/dist/index344.js +6 -34
- package/dist/index345.js +17 -127
- package/dist/index346.js +7 -396
- package/dist/index347.js +14 -199
- package/dist/index348.js +5 -259
- package/dist/index349.js +6 -227
- package/dist/index352.js +35 -2
- package/dist/index353.js +129 -2
- package/dist/index354.js +378 -114
- package/dist/index355.js +92 -6
- package/dist/index356.js +226 -17
- package/dist/index357.js +22 -9
- package/dist/index359.js +7 -5
- package/dist/index36.js +4 -4
- package/dist/index360.js +200 -7
- package/dist/index361.js +255 -18
- package/dist/index362.js +136 -0
- package/dist/index363.js +2 -93
- package/dist/index364.js +2 -441
- package/dist/index365.js +427 -114
- package/dist/index366.js +127 -46
- package/dist/index367.js +44 -67
- package/dist/index368.js +66 -516
- package/dist/index369.js +515 -45
- package/dist/index370.js +52 -0
- package/dist/index38.js +133 -131
- package/dist/index40.js +8 -8
- package/dist/index42.js +2 -2
- package/dist/index44.js +16 -15
- package/dist/index46.js +4 -4
- package/dist/index50.js +28 -25
- package/dist/index54.js +1 -1
- package/dist/index56.js +1 -1
- package/dist/index58.js +2 -2
- package/dist/index60.js +2 -2
- package/dist/index62.js +5 -5
- package/dist/index66.js +3 -1
- package/dist/index68.js +1 -1
- package/dist/index74.js +4 -4
- package/dist/index82.js +6 -6
- package/dist/index84.js +1 -1
- package/dist/index86.js +2 -2
- package/dist/index88.js +3 -3
- package/dist/index90.js +1 -1
- package/dist/index93.js +3 -3
- package/dist/index95.js +2 -2
- package/dist/index97.js +5 -5
- package/dist/index99.js +1 -1
- package/dist/utilities/functions/parseSize.d.ts +1 -1
- package/package.json +5 -3
- package/dist/components/CommonInputInnerContainer.vue.d.ts +0 -81
- package/dist/components/CommonInputOuterContainer.vue.d.ts +0 -41
- package/dist/components/FlatListBox.vue.d.ts +0 -119
- package/dist/components/GroupedListBox.vue.d.ts +0 -153
- package/dist/components/ListBox.vue.d.ts +0 -170
- package/dist/components/OptionsContainer.vue.d.ts +0 -172
- package/dist/index261.js +0 -88
- package/dist/index275.js +0 -25
- package/dist/index282.js +0 -54
- package/dist/index293.js +0 -5
- package/dist/index302.js +0 -55
- package/dist/index358.js +0 -17
|
@@ -0,0 +1,2010 @@
|
|
|
1
|
+
# BbSelectPopover
|
|
2
|
+
|
|
3
|
+
## Template & Script
|
|
4
|
+
|
|
5
|
+
```vue
|
|
6
|
+
<template>
|
|
7
|
+
<slot
|
|
8
|
+
v-if="!hasExternalActivator"
|
|
9
|
+
name="activator"
|
|
10
|
+
v-bind="{
|
|
11
|
+
props: {
|
|
12
|
+
ref: setActivatorRef,
|
|
13
|
+
},
|
|
14
|
+
shown,
|
|
15
|
+
disabled,
|
|
16
|
+
readonly,
|
|
17
|
+
loading: computedLoading,
|
|
18
|
+
query,
|
|
19
|
+
selectedOptions,
|
|
20
|
+
text: triggerInputValue,
|
|
21
|
+
clear: onClear,
|
|
22
|
+
open,
|
|
23
|
+
close,
|
|
24
|
+
toggle,
|
|
25
|
+
}"
|
|
26
|
+
/>
|
|
27
|
+
<CommonPopover
|
|
28
|
+
v-model="shown"
|
|
29
|
+
:anchor="activatorEl"
|
|
30
|
+
:arrow-padding="arrowPadding"
|
|
31
|
+
:boundary="boundary"
|
|
32
|
+
class="bb-select-popover"
|
|
33
|
+
:class="{
|
|
34
|
+
'bb-select-popover--errors': hasErrors,
|
|
35
|
+
'bb-select-popover--loading': computedLoading,
|
|
36
|
+
}"
|
|
37
|
+
:flip="flip"
|
|
38
|
+
:placement="placement"
|
|
39
|
+
:offset="offset"
|
|
40
|
+
:padding="padding"
|
|
41
|
+
:transition-duration="transitionDuration"
|
|
42
|
+
:hide-arrow="hideArrow"
|
|
43
|
+
:style="panelStyles"
|
|
44
|
+
>
|
|
45
|
+
<div
|
|
46
|
+
v-if="computedAllowWriting"
|
|
47
|
+
class="bb-select-popover__search-container"
|
|
48
|
+
>
|
|
49
|
+
<input
|
|
50
|
+
:id="`${id}_search`"
|
|
51
|
+
ref="searchInput"
|
|
52
|
+
v-model="query"
|
|
53
|
+
:aria-activedescendant="activeDescendantId"
|
|
54
|
+
:aria-controls="renderListBox ? `${id}_listbox` : undefined"
|
|
55
|
+
:aria-describedby="computedAriaDescribedby"
|
|
56
|
+
:aria-invalid="computedAriaInvalid"
|
|
57
|
+
:aria-label="searchInputAriaLabel"
|
|
58
|
+
:autocomplete="autocomplete"
|
|
59
|
+
class="bb-select-popover__search-input"
|
|
60
|
+
:inputmode="inputmode"
|
|
61
|
+
:tabindex="shown ? 0 : -1"
|
|
62
|
+
role="searchbox"
|
|
63
|
+
:type="'text'"
|
|
64
|
+
@blur.stop="onSearchInputBlur"
|
|
65
|
+
@change.stop="onSearchInputChange"
|
|
66
|
+
@focus.stop="onSearchInputFocus"
|
|
67
|
+
@input.stop="onSearchInputInput"
|
|
68
|
+
@keydown.stop.prevent.down="onArrowDown"
|
|
69
|
+
@keydown.stop.prevent.enter="onEnter"
|
|
70
|
+
@keydown.stop.prevent.esc="onEscape"
|
|
71
|
+
@keydown.stop.prevent.up="onArrowUp"
|
|
72
|
+
/>
|
|
73
|
+
<ErrorIcon
|
|
74
|
+
aria-hidden="true"
|
|
75
|
+
class="bb-select-popover__search-error-icon"
|
|
76
|
+
focusable="false"
|
|
77
|
+
/>
|
|
78
|
+
<BbSpinner
|
|
79
|
+
aria-hidden="true"
|
|
80
|
+
class="bb-select-popover__search-spinner"
|
|
81
|
+
focusable="false"
|
|
82
|
+
/>
|
|
83
|
+
<svg
|
|
84
|
+
class="bb-select-popover__search-icon"
|
|
85
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
86
|
+
viewBox="0 0 24 24"
|
|
87
|
+
aria-hidden="true"
|
|
88
|
+
>
|
|
89
|
+
<path
|
|
90
|
+
fill="none"
|
|
91
|
+
stroke="currentColor"
|
|
92
|
+
stroke-linecap="round"
|
|
93
|
+
stroke-linejoin="round"
|
|
94
|
+
stroke-width="2"
|
|
95
|
+
d="m21 21-4.343-4.343m0 0A8 8 0 1 0 5.343 5.343a8 8 0 0 0 11.314 11.314"
|
|
96
|
+
/>
|
|
97
|
+
</svg>
|
|
98
|
+
</div>
|
|
99
|
+
<button
|
|
100
|
+
v-else
|
|
101
|
+
ref="popoverFocusProxy"
|
|
102
|
+
class="bb-select-popover__focus-proxy sr-only"
|
|
103
|
+
type="button"
|
|
104
|
+
:aria-controls="`${id}_listbox`"
|
|
105
|
+
:aria-describedby="computedAriaDescribedby"
|
|
106
|
+
:aria-invalid="computedAriaInvalid"
|
|
107
|
+
:aria-label="searchInputAriaLabel"
|
|
108
|
+
tabindex="-1"
|
|
109
|
+
@focus="onPopoverFocusProxyFocus"
|
|
110
|
+
@keydown.stop.prevent.down="onArrowDown"
|
|
111
|
+
@keydown.stop.prevent.up="onArrowUp"
|
|
112
|
+
@keydown.stop.prevent.enter="onEnter"
|
|
113
|
+
@keydown.stop.prevent.esc="onEscape"
|
|
114
|
+
>
|
|
115
|
+
Focus options
|
|
116
|
+
</button>
|
|
117
|
+
<div
|
|
118
|
+
class="bb-select-popover__options-outer"
|
|
119
|
+
:aria-hidden="shown ? undefined : 'true'"
|
|
120
|
+
:inert="!shown"
|
|
121
|
+
>
|
|
122
|
+
<slot name="options:prepend:outer" :focus="focusPopoverContent" />
|
|
123
|
+
</div>
|
|
124
|
+
<ListBox
|
|
125
|
+
:id="`${id}_listbox`"
|
|
126
|
+
ref="optionsContainer"
|
|
127
|
+
:compact="compact"
|
|
128
|
+
:loading="computedLoading"
|
|
129
|
+
:loading-text="loadingText"
|
|
130
|
+
:multiple="multiple"
|
|
131
|
+
:no-data-text="noDataText"
|
|
132
|
+
:open="shown"
|
|
133
|
+
:options="groupBy ? undefined : filteredOptions"
|
|
134
|
+
:groups="groupBy ? groupedOptions : undefined"
|
|
135
|
+
:header-height="headerHeight"
|
|
136
|
+
:option-height="itemHeight"
|
|
137
|
+
@focused:change="onListboxFocusedChange"
|
|
138
|
+
@option:selected="onOptionSelected"
|
|
139
|
+
@option:unselected="onOptionUnselected"
|
|
140
|
+
>
|
|
141
|
+
<template #options:prepend
|
|
142
|
+
><slot name="options:prepend" :focus="focusPopoverContent"
|
|
143
|
+
/></template>
|
|
144
|
+
<template #loading><slot name="loading" :query="query" /></template>
|
|
145
|
+
<template #no-data
|
|
146
|
+
><slot name="no-data" :query="query" :focus="focusPopoverContent"
|
|
147
|
+
/></template>
|
|
148
|
+
<template #options:append
|
|
149
|
+
><slot name="options:append" :focus="focusPopoverContent"
|
|
150
|
+
/></template>
|
|
151
|
+
<template #option="data"
|
|
152
|
+
><slot :loading="computedLoading" name="option" v-bind="data"
|
|
153
|
+
/></template>
|
|
154
|
+
<template #group="data"><slot name="group" v-bind="data" /></template>
|
|
155
|
+
</ListBox>
|
|
156
|
+
<div
|
|
157
|
+
class="bb-select-popover__options-outer"
|
|
158
|
+
:aria-hidden="shown ? undefined : 'true'"
|
|
159
|
+
:inert="!shown"
|
|
160
|
+
>
|
|
161
|
+
<slot name="options:append:outer" :focus="focusPopoverContent" />
|
|
162
|
+
</div>
|
|
163
|
+
</CommonPopover>
|
|
164
|
+
</template>
|
|
165
|
+
|
|
166
|
+
<script setup lang="ts" generic="Item = any">
|
|
167
|
+
import type { HTMLAttributes, InputHTMLAttributes } from 'vue';
|
|
168
|
+
import type { CommonProps } from '@/types/CommonProps';
|
|
169
|
+
import type { Option as BaseOption, GroupedOptions } from '@/types/Option';
|
|
170
|
+
import { computed, ref, nextTick, watch, onBeforeUnmount } from 'vue';
|
|
171
|
+
import { extractDomContainer } from '@/utilities/functions/extractDomContainer';
|
|
172
|
+
import { get } from '@/utilities/functions/get';
|
|
173
|
+
import { hash } from '@/utilities/functions/hash';
|
|
174
|
+
import { isNil } from '@/utilities/functions/isNil';
|
|
175
|
+
import { matchAnyKey } from '@/utilities/functions/matchAnyKey';
|
|
176
|
+
import { parseWidthString } from '@/utilities/functions/parseWidthString';
|
|
177
|
+
import { toRef } from 'vue';
|
|
178
|
+
import { useArray } from '@/composables/useArray';
|
|
179
|
+
import { useBaseOptions } from '@/composables/useBaseOptions';
|
|
180
|
+
import { useCoherence } from '@/composables/useCoherence';
|
|
181
|
+
import { useElementSize, useIntersectionObserver } from '@vueuse/core';
|
|
182
|
+
import { useHashedWatcher } from '@/composables/useHashedWatcher';
|
|
183
|
+
import { useId } from '@/composables/useId';
|
|
184
|
+
import { useIndexById } from '@/composables/useIndexById';
|
|
185
|
+
import { useItemsGetter } from '@/composables/useItemsGetter';
|
|
186
|
+
import { useMobile } from '@/composables/useMobile';
|
|
187
|
+
import { usePrefill } from '@/composables/usePrefill';
|
|
188
|
+
import { useUntil } from '@/composables/useUntil';
|
|
189
|
+
import { wait } from '@/utilities/functions/wait';
|
|
190
|
+
import { BbSpinner } from '@/index';
|
|
191
|
+
import CommonPopover from '../CommonPopover/CommonPopover.vue';
|
|
192
|
+
import ErrorIcon from '../ErrorIcon/ErrorIcon.vue';
|
|
193
|
+
import ListBox from '../ListBox/ListBox.vue';
|
|
194
|
+
import { waitFor } from '@/utilities/functions/waitFor';
|
|
195
|
+
|
|
196
|
+
export type BbSelectPopoverProps<Item> = {
|
|
197
|
+
/**
|
|
198
|
+
* External activator element or component ref.
|
|
199
|
+
* When provided, the activator slot is not rendered and event listeners
|
|
200
|
+
* are attached programmatically to the referenced element.
|
|
201
|
+
*/
|
|
202
|
+
activator?: HTMLElement | Record<string, any> | null;
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Controls whether the search input is shown inside the popover panel.
|
|
206
|
+
* Use `'not-mobile'` to hide the search input on mobile.
|
|
207
|
+
* @default false
|
|
208
|
+
*/
|
|
209
|
+
allowWriting?: boolean | 'not-mobile';
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Browser autocomplete hint for the popover search input.
|
|
213
|
+
* @default 'off'
|
|
214
|
+
*/
|
|
215
|
+
autocomplete?: InputHTMLAttributes['autocomplete'];
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Apply the compact density variant.
|
|
219
|
+
*/
|
|
220
|
+
compact?: boolean;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Space the arrow keeps from popover edges.
|
|
224
|
+
* Useful with rounded corners.
|
|
225
|
+
* @default 10
|
|
226
|
+
*/
|
|
227
|
+
arrowPadding?: CommonProps['arrowPadding'];
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Constrains the popover to a specific boundary element.
|
|
231
|
+
*/
|
|
232
|
+
boundary?: HTMLElement | Record<string, any> | string | null;
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Additional dependencies that trigger item reloading when changed.
|
|
236
|
+
* @default []
|
|
237
|
+
*/
|
|
238
|
+
dependencies?: any[];
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Debounce delay (ms) for dependency-triggered reloads.
|
|
242
|
+
* @default 0
|
|
243
|
+
*/
|
|
244
|
+
depsDebounceTime?: number;
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Disables the component.
|
|
248
|
+
*/
|
|
249
|
+
disabled?: boolean;
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Marks the component as being in error state.
|
|
253
|
+
*/
|
|
254
|
+
hasErrors?: boolean;
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Explicit invalid state for accessibility semantics.
|
|
258
|
+
* Falls back to `hasErrors` when not provided.
|
|
259
|
+
*/
|
|
260
|
+
ariaInvalid?: boolean;
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Space-separated IDREF(s) that describe this field for assistive technologies.
|
|
264
|
+
* Accepts either a string or array of ids.
|
|
265
|
+
*/
|
|
266
|
+
ariaDescribedby?: string | string[];
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Enable or disable floating flip behavior.
|
|
270
|
+
* @default true
|
|
271
|
+
*/
|
|
272
|
+
flip?: boolean;
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Whether the floating arrow is hidden.
|
|
276
|
+
* @default true
|
|
277
|
+
*/
|
|
278
|
+
hideArrow?: boolean;
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Reset modelValue to `null` or empty array if it no longer matches available items.
|
|
282
|
+
*/
|
|
283
|
+
enforceCoherence?: boolean;
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Controls how options are filtered during search.
|
|
287
|
+
* Options are always filtered by display text, but can also match additional properties
|
|
288
|
+
* when set to a string path, array of paths, or custom function.
|
|
289
|
+
* Set to `false` to disable filtering, or `'not_stashed'` to exclude stashed items.
|
|
290
|
+
* @default () => []
|
|
291
|
+
*/
|
|
292
|
+
filterBy?:
|
|
293
|
+
| string
|
|
294
|
+
| string[]
|
|
295
|
+
| false
|
|
296
|
+
| 'not_stashed'
|
|
297
|
+
| ((value: any, item: any, query: string | null) => boolean)
|
|
298
|
+
| null;
|
|
299
|
+
|
|
300
|
+
groupBy?: string | ((item: Item) => string | number | symbol);
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Height of group headers in the listbox (px). Defaults to 24px (compact) or 32px.
|
|
304
|
+
* Only applies when `groupBy` is set.
|
|
305
|
+
*/
|
|
306
|
+
headerHeight?: number;
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Explicit id for the component. Used to generate ids for listbox and options.
|
|
310
|
+
*/
|
|
311
|
+
id?: HTMLAttributes['id'];
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Array of items or function to load them asynchronously.
|
|
315
|
+
* Functions receive `(query, prefill, modelValue)` and can return a promise.
|
|
316
|
+
* @default []
|
|
317
|
+
*/
|
|
318
|
+
items:
|
|
319
|
+
| Item[]
|
|
320
|
+
| ((query: string, prefill: boolean, modelValue: any[]) => Promise<Item[]>)
|
|
321
|
+
| ((query: string, prefill: boolean, modelValue: any[]) => Item[]);
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Height of the options in the listbox (px).
|
|
325
|
+
* @default 40
|
|
326
|
+
*/
|
|
327
|
+
itemHeight?: number;
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Path to item property for display text or function to extract it.
|
|
331
|
+
*/
|
|
332
|
+
itemText?: string | ((item: Item) => string) | undefined;
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Path to item property for value or function to extract it.
|
|
336
|
+
*/
|
|
337
|
+
itemValue?: string | ((item: Item) => string) | undefined;
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Display the loading state styles.
|
|
341
|
+
*/
|
|
342
|
+
loading?: boolean;
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Message shown while items are being loaded.
|
|
346
|
+
*/
|
|
347
|
+
loadingText?: string;
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Maximum number of selectable items (limits selection when `multiple` is true).
|
|
351
|
+
* @default Infinity
|
|
352
|
+
*/
|
|
353
|
+
max?: number;
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* v-model value. Single value for single select, array for multiple select.
|
|
357
|
+
*/
|
|
358
|
+
modelValue: any;
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Debounce delay (ms) for modelValue change handling.
|
|
362
|
+
* @default 0
|
|
363
|
+
*/
|
|
364
|
+
modelValueDebounceTime?: number;
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Enable multiple item selection. When true, modelValue must be an array.
|
|
368
|
+
*/
|
|
369
|
+
multiple?: boolean;
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Message displayed when no items are available.
|
|
373
|
+
*/
|
|
374
|
+
noDataText?: string;
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Distance (px) between the activator and the popover.
|
|
378
|
+
* @default 4
|
|
379
|
+
*/
|
|
380
|
+
offset?: number;
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Minimum page padding used by floating shift middleware.
|
|
384
|
+
* @default 0
|
|
385
|
+
*/
|
|
386
|
+
padding?: CommonProps['padding'];
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Floating placement of the popover.
|
|
390
|
+
* @default 'bottom'
|
|
391
|
+
*/
|
|
392
|
+
placement?: CommonProps['placement'];
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Controls when items are pre-loaded.
|
|
396
|
+
* `'interaction'` loads on first user interaction (first popover open),
|
|
397
|
+
* `true` loads immediately, `false` loads only on explicit search/open flows.
|
|
398
|
+
* @default 'interaction'
|
|
399
|
+
*/
|
|
400
|
+
prefill?: boolean | 'interaction';
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Debounce delay (ms) before triggering search queries after user input stops.
|
|
404
|
+
* @default 500
|
|
405
|
+
*/
|
|
406
|
+
queryDebounceTime?: number;
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Prevents opening the popover while keeping the activator accessible.
|
|
410
|
+
*/
|
|
411
|
+
readonly?: boolean;
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Accessible label used by the search input rendered inside the popover.
|
|
415
|
+
* @default 'Search options'
|
|
416
|
+
*/
|
|
417
|
+
searchInputAriaLabel?: string;
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Accumulate selected items across searches instead of replacing them.
|
|
421
|
+
*/
|
|
422
|
+
stash?: boolean;
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Transition duration (ms) for popover show/hide animations.
|
|
426
|
+
* @default 300
|
|
427
|
+
*/
|
|
428
|
+
transitionDuration?: number;
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Width of the popover in pixels, percentage, or `'auto'`.
|
|
432
|
+
*/
|
|
433
|
+
width?: number | string;
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
export type BbSelectPopoverEvents = {
|
|
437
|
+
(e: 'blur', event: FocusEvent): void;
|
|
438
|
+
(e: 'change', event: Event): void;
|
|
439
|
+
(e: 'click', event: MouseEvent): void;
|
|
440
|
+
(e: 'focus', event: FocusEvent): void;
|
|
441
|
+
(e: 'inactive'): void;
|
|
442
|
+
(e: 'input', event: Event): void;
|
|
443
|
+
(e: 'update:modelValue', value: any): void;
|
|
444
|
+
(e: 'option:add', text: string): void;
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
export type BbSelectPopoverSlots<Item> = {
|
|
448
|
+
/**
|
|
449
|
+
* Replaces the default trigger element that opens the select popover.
|
|
450
|
+
* @param props - Bind these onto the activator element with `v-bind="props"` to wire up the popover reference.
|
|
451
|
+
* @param shown - Whether the popover is currently open.
|
|
452
|
+
* @param disabled - Whether the select is disabled.
|
|
453
|
+
* @param readonly - Whether the select is read-only.
|
|
454
|
+
* @param loading - Whether options are currently being fetched.
|
|
455
|
+
* @param query - The current search query string.
|
|
456
|
+
* @param clear - Clears the current selection and query.
|
|
457
|
+
* @param open - Opens the popover.
|
|
458
|
+
* @param close - Closes the popover.
|
|
459
|
+
* @param toggle - Toggles the popover open/closed.
|
|
460
|
+
*/
|
|
461
|
+
activator?: (props: {
|
|
462
|
+
props: {
|
|
463
|
+
ref: (reference: unknown) => void;
|
|
464
|
+
};
|
|
465
|
+
shown: boolean;
|
|
466
|
+
disabled: boolean | undefined;
|
|
467
|
+
readonly: boolean | undefined;
|
|
468
|
+
loading: boolean;
|
|
469
|
+
query: string;
|
|
470
|
+
clear: () => void;
|
|
471
|
+
open: () => Promise<void>;
|
|
472
|
+
close: () => Promise<void>;
|
|
473
|
+
toggle: () => Promise<void>;
|
|
474
|
+
}) => any;
|
|
475
|
+
/**
|
|
476
|
+
* Content shown inside the options dropdown while items are loading.
|
|
477
|
+
* @param query - The current search query string.
|
|
478
|
+
*/
|
|
479
|
+
loading?: (props: { query: string }) => any;
|
|
480
|
+
/**
|
|
481
|
+
* Content shown when no options match the query or the list is empty.
|
|
482
|
+
* @param query - The current search query string.
|
|
483
|
+
* @param focus - Focuses the internal search input.
|
|
484
|
+
*/
|
|
485
|
+
'no-data'?: (props: { query: string; focus: () => void }) => any;
|
|
486
|
+
/**
|
|
487
|
+
* Replaces the default rendering of each option row in the dropdown.
|
|
488
|
+
* @param disabled - Whether this option is disabled.
|
|
489
|
+
* @param focused - Whether this option currently has keyboard focus.
|
|
490
|
+
* @param index - Zero-based index of this option in the list.
|
|
491
|
+
* @param item - The raw item from the `items` prop.
|
|
492
|
+
* @param loading - Whether items are still being fetched.
|
|
493
|
+
* @param selected - Whether this option is currently selected.
|
|
494
|
+
* @param text - The resolved display text for this option.
|
|
495
|
+
* @param value - The resolved value for this option.
|
|
496
|
+
*/
|
|
497
|
+
option?: (props: {
|
|
498
|
+
disabled: boolean;
|
|
499
|
+
focused: boolean;
|
|
500
|
+
index: number;
|
|
501
|
+
item: Item;
|
|
502
|
+
loading: boolean;
|
|
503
|
+
selected: boolean;
|
|
504
|
+
text: string;
|
|
505
|
+
value: any;
|
|
506
|
+
}) => any;
|
|
507
|
+
/**
|
|
508
|
+
* Replaces the default group header rendered above grouped options.
|
|
509
|
+
* @param text - The display text for this group.
|
|
510
|
+
* @param item - The raw group item.
|
|
511
|
+
* @param index - Zero-based index of the first option in this group.
|
|
512
|
+
* @param length - Number of options in this group.
|
|
513
|
+
* @param disabled - Whether all options in this group are disabled.
|
|
514
|
+
*/
|
|
515
|
+
group?: (props: {
|
|
516
|
+
text: string;
|
|
517
|
+
item: Item;
|
|
518
|
+
index: number;
|
|
519
|
+
length: number;
|
|
520
|
+
disabled: boolean;
|
|
521
|
+
}) => any;
|
|
522
|
+
/**
|
|
523
|
+
* Content appended inside the options list, after the last option.
|
|
524
|
+
* @param focus - Focuses the internal search input.
|
|
525
|
+
*/
|
|
526
|
+
'options:append'?: (props: { focus: () => void }) => any;
|
|
527
|
+
/**
|
|
528
|
+
* Content appended outside the options list container.
|
|
529
|
+
* @param focus - Focuses the internal search input.
|
|
530
|
+
*/
|
|
531
|
+
'options:append:outer'?: (props: { focus: () => void }) => any;
|
|
532
|
+
/**
|
|
533
|
+
* Content prepended inside the options list, before the first option.
|
|
534
|
+
* @param focus - Focuses the internal search input.
|
|
535
|
+
*/
|
|
536
|
+
'options:prepend'?: (props: { focus: () => void }) => any;
|
|
537
|
+
/**
|
|
538
|
+
* Content prepended outside the options list container.
|
|
539
|
+
* @param focus - Focuses the internal search input.
|
|
540
|
+
*/
|
|
541
|
+
'options:prepend:outer'?: (props: { focus: () => void }) => any;
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
const props = withDefaults(defineProps<BbSelectPopoverProps<Item>>(), {
|
|
545
|
+
allowWriting: true,
|
|
546
|
+
arrowPadding: 10,
|
|
547
|
+
autocomplete: 'off',
|
|
548
|
+
depsDebounceTime: 0,
|
|
549
|
+
dependencies: () => [],
|
|
550
|
+
filterBy: () => [],
|
|
551
|
+
flip: true,
|
|
552
|
+
hideArrow: true,
|
|
553
|
+
max: Infinity,
|
|
554
|
+
modelValueDebounceTime: 0,
|
|
555
|
+
offset: 4,
|
|
556
|
+
padding: 10,
|
|
557
|
+
placement: 'bottom',
|
|
558
|
+
prefill: 'interaction',
|
|
559
|
+
queryDebounceTime: 500,
|
|
560
|
+
transitionDuration: 300,
|
|
561
|
+
width: '200',
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
const emit = defineEmits<BbSelectPopoverEvents>();
|
|
565
|
+
|
|
566
|
+
defineSlots<BbSelectPopoverSlots<Item>>();
|
|
567
|
+
|
|
568
|
+
if (props.multiple && !Array.isArray(props.modelValue)) {
|
|
569
|
+
throw new Error('Multiple is set to "true" but modelValue is not an array.');
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* START ELEMENT REFERENCES
|
|
574
|
+
*/
|
|
575
|
+
const activatorEl = ref<HTMLElement>();
|
|
576
|
+
const optionsContainer = ref<any | null>(null);
|
|
577
|
+
const searchInput = ref<HTMLInputElement | null>(null);
|
|
578
|
+
const popoverFocusProxy = ref<HTMLButtonElement | null>(null);
|
|
579
|
+
const activeDescendantId = ref<string>();
|
|
580
|
+
const { width: activatorWidth } = useElementSize(
|
|
581
|
+
activatorEl,
|
|
582
|
+
{ width: 0, height: 0 },
|
|
583
|
+
{
|
|
584
|
+
box: 'border-box',
|
|
585
|
+
}
|
|
586
|
+
);
|
|
587
|
+
/**
|
|
588
|
+
* END ELEMENT REFERENCES
|
|
589
|
+
*/
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* START ACTIVATOR MANAGEMENT
|
|
593
|
+
*/
|
|
594
|
+
const hasExternalActivator = computed(() => props.activator != null);
|
|
595
|
+
|
|
596
|
+
const setActivatorRef = (reference: unknown) => {
|
|
597
|
+
const el = extractDomContainer(reference);
|
|
598
|
+
if (!el) return;
|
|
599
|
+
activatorEl.value = el;
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
watch(
|
|
603
|
+
() => props.activator,
|
|
604
|
+
(val) => {
|
|
605
|
+
if (val) setActivatorRef(val);
|
|
606
|
+
},
|
|
607
|
+
{ immediate: true }
|
|
608
|
+
);
|
|
609
|
+
/**
|
|
610
|
+
* END ACTIVATOR MANAGEMENT
|
|
611
|
+
*/
|
|
612
|
+
|
|
613
|
+
const { isMobile } = useMobile();
|
|
614
|
+
const computedAllowWriting = computed(() => {
|
|
615
|
+
if (props.allowWriting === 'not-mobile') return !isMobile.value;
|
|
616
|
+
return !!props.allowWriting;
|
|
617
|
+
});
|
|
618
|
+
const inputmode = computed(() =>
|
|
619
|
+
props.allowWriting === 'not-mobile' && isMobile.value ? 'none' : undefined
|
|
620
|
+
);
|
|
621
|
+
const searchInputAriaLabel = computed(
|
|
622
|
+
() => props.searchInputAriaLabel ?? 'Search options'
|
|
623
|
+
);
|
|
624
|
+
|
|
625
|
+
const id = props.id || useId().id.value;
|
|
626
|
+
const computedAriaInvalid = computed(() => {
|
|
627
|
+
const isInvalid = props.ariaInvalid ?? !!props.hasErrors;
|
|
628
|
+
return isInvalid ? 'true' : undefined;
|
|
629
|
+
});
|
|
630
|
+
const computedAriaDescribedby = computed(() => {
|
|
631
|
+
const describedBy = props.ariaDescribedby;
|
|
632
|
+
if (Array.isArray(describedBy)) {
|
|
633
|
+
const normalized = describedBy
|
|
634
|
+
.map((value) => value.trim())
|
|
635
|
+
.filter(Boolean)
|
|
636
|
+
.join(' ');
|
|
637
|
+
return normalized || undefined;
|
|
638
|
+
}
|
|
639
|
+
if (typeof describedBy === 'string') {
|
|
640
|
+
const normalized = describedBy.trim();
|
|
641
|
+
return normalized || undefined;
|
|
642
|
+
}
|
|
643
|
+
return undefined;
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
const { resume, pause } = useIntersectionObserver(
|
|
647
|
+
activatorEl,
|
|
648
|
+
async ([{ intersectionRatio }]) => {
|
|
649
|
+
if (intersectionRatio !== 1 && shown.value) {
|
|
650
|
+
await close();
|
|
651
|
+
}
|
|
652
|
+
},
|
|
653
|
+
{ threshold: [0, 1], immediate: false }
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
const normalizedWidth = computed(() => {
|
|
657
|
+
if (props.width === 'auto') return 'auto';
|
|
658
|
+
if (props.width == null || props.width === '') {
|
|
659
|
+
return activatorWidth.value ? `${activatorWidth.value}px` : '320px';
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
try {
|
|
663
|
+
const { value, unit } = parseWidthString(props.width);
|
|
664
|
+
if (unit === '%') {
|
|
665
|
+
if (!activatorWidth.value) return '320px';
|
|
666
|
+
return `${activatorWidth.value * (value / 100)}px`;
|
|
667
|
+
}
|
|
668
|
+
return `${value}${unit}`;
|
|
669
|
+
} catch {
|
|
670
|
+
if (typeof props.width === 'string') return props.width.trim();
|
|
671
|
+
return activatorWidth.value ? `${activatorWidth.value}px` : '320px';
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
const panelStyles = computed(() => ({
|
|
676
|
+
'--bb-select-popover-width': normalizedWidth.value,
|
|
677
|
+
}));
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* START FOCUS HELPERS
|
|
681
|
+
*/
|
|
682
|
+
const focusActivator = () => {
|
|
683
|
+
activatorEl.value?.focus();
|
|
684
|
+
};
|
|
685
|
+
const focusSearchInput = () => {
|
|
686
|
+
if (searchInput.value instanceof HTMLInputElement) {
|
|
687
|
+
searchInput.value.focus();
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
const focusPopoverFocusProxy = () => {
|
|
691
|
+
if (popoverFocusProxy.value instanceof HTMLButtonElement) {
|
|
692
|
+
popoverFocusProxy.value.focus();
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
const focusPopoverContent = () => {
|
|
696
|
+
if (computedAllowWriting.value) {
|
|
697
|
+
focusSearchInput();
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
focusPopoverFocusProxy();
|
|
701
|
+
};
|
|
702
|
+
/**
|
|
703
|
+
* END FOCUS HELPERS
|
|
704
|
+
*/
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Search input focus keeps the whole control in active state.
|
|
708
|
+
*/
|
|
709
|
+
const onSearchInputFocus = (event: FocusEvent) => {
|
|
710
|
+
emit('focus', event);
|
|
711
|
+
setActive();
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
const onSearchInputBlur = (event: FocusEvent) => {
|
|
715
|
+
emit('blur', event);
|
|
716
|
+
syncActiveStateOnBlur();
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
const onSearchInputChange = (event: Event) => {
|
|
720
|
+
emit('change', event);
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
const onPopoverFocusProxyFocus = (_event: FocusEvent) => {
|
|
724
|
+
setActive();
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Search on user typing inside the popover search field.
|
|
729
|
+
*/
|
|
730
|
+
const onSearchInputInput = (event: Event) => {
|
|
731
|
+
emit('input', event);
|
|
732
|
+
debouncedGetter(query.value, false, props.modelValue);
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
const onListboxFocusedChange = (id: string | undefined) => {
|
|
736
|
+
activeDescendantId.value = id;
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* On click outside of the activator and popover, close the panel and remove active state.
|
|
741
|
+
*/
|
|
742
|
+
const onOutsideInteraction = async (event: Event) => {
|
|
743
|
+
const target =
|
|
744
|
+
event.target instanceof Element
|
|
745
|
+
? event.target
|
|
746
|
+
: event.target instanceof Text
|
|
747
|
+
? event.target.parentElement
|
|
748
|
+
: null;
|
|
749
|
+
if (!target) return;
|
|
750
|
+
const insideActivator = activatorEl.value?.contains(target);
|
|
751
|
+
const insidePopover = !!target.closest('.bb-common-popover');
|
|
752
|
+
if (insideActivator || insidePopover) return;
|
|
753
|
+
await close();
|
|
754
|
+
setInactive();
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
const canProcessKeyboardBindings = computed(() => {
|
|
758
|
+
if (props.readonly) return false;
|
|
759
|
+
if (props.disabled) return false;
|
|
760
|
+
if (computedLoading.value) return false;
|
|
761
|
+
return true;
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* On first open the listbox is lazily mounted.
|
|
766
|
+
* Wait a few micro-cycles so imperative focus calls don't no-op.
|
|
767
|
+
*/
|
|
768
|
+
const waitForOptionsContainer = async () =>
|
|
769
|
+
waitFor(() => !!optionsContainer.value, 100);
|
|
770
|
+
|
|
771
|
+
const focusFirstSelectedOption = () => {
|
|
772
|
+
const firstSelected = selectedOptions.value[0];
|
|
773
|
+
if (firstSelected?.valueHash) {
|
|
774
|
+
optionsContainer.value?.focusByHash(firstSelected.valueHash);
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
optionsContainer.value?.focusFirstSelected();
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
const focusLastSelectedOption = () => {
|
|
781
|
+
const lastSelected = selectedOptions.value[selectedOptions.value.length - 1];
|
|
782
|
+
if (lastSelected?.valueHash) {
|
|
783
|
+
optionsContainer.value?.focusByHash(lastSelected.valueHash);
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
optionsContainer.value?.focusLastSelected();
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* On arrow up:
|
|
791
|
+
* When the panel is open — focus on the previous option
|
|
792
|
+
* When the panel is closed — open and focus on the last selected option
|
|
793
|
+
*/
|
|
794
|
+
const onArrowUp = async () => {
|
|
795
|
+
if (!canProcessKeyboardBindings.value) return;
|
|
796
|
+
if (shown.value) {
|
|
797
|
+
optionsContainer.value?.focusPrevious();
|
|
798
|
+
} else {
|
|
799
|
+
await open({ focusPopoverContent: false });
|
|
800
|
+
await waitForOptionsContainer();
|
|
801
|
+
await wait(props.transitionDuration);
|
|
802
|
+
focusLastSelectedOption();
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
const onArrowDown = async () => {
|
|
807
|
+
if (!canProcessKeyboardBindings.value) return;
|
|
808
|
+
if (shown.value) {
|
|
809
|
+
optionsContainer.value?.focusNext();
|
|
810
|
+
} else {
|
|
811
|
+
await open({ focusPopoverContent: false });
|
|
812
|
+
await waitForOptionsContainer();
|
|
813
|
+
await wait(props.transitionDuration);
|
|
814
|
+
focusFirstSelectedOption();
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
const onEnter = async () => {
|
|
819
|
+
if (!canProcessKeyboardBindings.value) {
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
if (!shown.value) {
|
|
823
|
+
await open({ focusPopoverContent: false });
|
|
824
|
+
await waitForOptionsContainer();
|
|
825
|
+
await wait(props.transitionDuration);
|
|
826
|
+
if (selectedOptions.value.length) {
|
|
827
|
+
focusFirstSelectedOption();
|
|
828
|
+
} else if (computedAllowWriting.value) {
|
|
829
|
+
focusSearchInput();
|
|
830
|
+
} else {
|
|
831
|
+
focusPopoverFocusProxy();
|
|
832
|
+
}
|
|
833
|
+
return;
|
|
834
|
+
} else if (optionsContainer.value?.getHighlighted()) {
|
|
835
|
+
optionsContainer.value.confirmOption();
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
if (!computedAllowWriting.value) return;
|
|
839
|
+
const text = query.value.trim();
|
|
840
|
+
if (isItemSelected(text)) return;
|
|
841
|
+
emit('option:add', text);
|
|
842
|
+
query.value = '';
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
const onEscape = async () => {
|
|
846
|
+
if (shown.value) {
|
|
847
|
+
await close();
|
|
848
|
+
focusActivator();
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
const query = ref<string>('');
|
|
853
|
+
const hasInteracted = ref(false);
|
|
854
|
+
|
|
855
|
+
const {
|
|
856
|
+
getter,
|
|
857
|
+
debouncedGetter,
|
|
858
|
+
items: internalItems,
|
|
859
|
+
loading,
|
|
860
|
+
} = useItemsGetter({
|
|
861
|
+
items: toRef(props, 'items'),
|
|
862
|
+
debounce: props.queryDebounceTime,
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
const {
|
|
866
|
+
array: innerStash,
|
|
867
|
+
add: addToStash,
|
|
868
|
+
remove: removeFromStash,
|
|
869
|
+
set: setStash,
|
|
870
|
+
} = useArray<BaseOption>();
|
|
871
|
+
|
|
872
|
+
const {
|
|
873
|
+
hasPrefilled,
|
|
874
|
+
isPrefilling,
|
|
875
|
+
canLoad,
|
|
876
|
+
prefill: prefillItems,
|
|
877
|
+
} = usePrefill({
|
|
878
|
+
trigger: computed(
|
|
879
|
+
() =>
|
|
880
|
+
props.prefill === true ||
|
|
881
|
+
(props.prefill === 'interaction' && hasInteracted.value)
|
|
882
|
+
),
|
|
883
|
+
currentValue: undefined,
|
|
884
|
+
multiple: false,
|
|
885
|
+
fn: async (isPrefill) => {
|
|
886
|
+
await getter(query.value, isPrefill, props.modelValue);
|
|
887
|
+
if (props.stash) {
|
|
888
|
+
addToStash(...options.value.map((o) => o.item));
|
|
889
|
+
}
|
|
890
|
+
},
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
const { data: modelValueIndexedByHash, get: isItemSelected } = useIndexById({
|
|
894
|
+
items: computed(() => [].concat(props.modelValue)),
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
const { options } = useBaseOptions({
|
|
898
|
+
disabled: toRef(props, 'disabled'),
|
|
899
|
+
items: computed(() => [...internalItems.value, ...innerStash.value]),
|
|
900
|
+
itemText: props.itemText,
|
|
901
|
+
itemValue: props.itemValue,
|
|
902
|
+
max: props.max,
|
|
903
|
+
selectable: true,
|
|
904
|
+
selectedIndexedByHash: modelValueIndexedByHash,
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
const { data: optionsIndexedByHash } = useIndexById({
|
|
908
|
+
items: options,
|
|
909
|
+
key: 'valueHash',
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
const { coherent, coherentValue } = useCoherence({
|
|
913
|
+
modelValue: toRef(props, 'modelValue'),
|
|
914
|
+
multiple: props.multiple,
|
|
915
|
+
iteratee: (item) => !!optionsIndexedByHash.value[hash(item)],
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
const selectedOptions = computed<BaseOption[]>(() =>
|
|
919
|
+
([] as any[])
|
|
920
|
+
.concat(props.modelValue)
|
|
921
|
+
.map((v) => optionsIndexedByHash.value[hash(v)])
|
|
922
|
+
.filter(Boolean)
|
|
923
|
+
);
|
|
924
|
+
|
|
925
|
+
const computedLoading = computed(() => !!(loading.value || props.loading));
|
|
926
|
+
const hasErrors = computed(() => !!props.hasErrors);
|
|
927
|
+
const triggerInputValue = computed(() => {
|
|
928
|
+
if (props.multiple) return '';
|
|
929
|
+
return selectedOptions.value[0]?.text ?? '';
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
useHashedWatcher(
|
|
933
|
+
() => props.modelValue,
|
|
934
|
+
async () => {
|
|
935
|
+
if (!canLoad.value) return;
|
|
936
|
+
if (!coherent.value) {
|
|
937
|
+
await getter(query.value, true, props.modelValue);
|
|
938
|
+
if (props.enforceCoherence) {
|
|
939
|
+
emit('update:modelValue', coherentValue.value);
|
|
940
|
+
optionsContainer.value?.blur();
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
},
|
|
944
|
+
{ debounce: props.modelValueDebounceTime }
|
|
945
|
+
);
|
|
946
|
+
useHashedWatcher(
|
|
947
|
+
() => [props.dependencies, props.items],
|
|
948
|
+
async () => {
|
|
949
|
+
if (!canLoad.value) return;
|
|
950
|
+
await getter(query.value, true, props.modelValue);
|
|
951
|
+
if (props.enforceCoherence && !coherent.value) {
|
|
952
|
+
emit('update:modelValue', coherentValue.value);
|
|
953
|
+
optionsContainer.value?.blur();
|
|
954
|
+
}
|
|
955
|
+
},
|
|
956
|
+
{ debounce: props.depsDebounceTime }
|
|
957
|
+
);
|
|
958
|
+
|
|
959
|
+
const onOptionSelected = async (option: BaseOption) => {
|
|
960
|
+
if (props.multiple) {
|
|
961
|
+
emit('update:modelValue', props.modelValue.concat(option.value));
|
|
962
|
+
if (props.stash) {
|
|
963
|
+
addToStash(option.item);
|
|
964
|
+
}
|
|
965
|
+
} else {
|
|
966
|
+
if (props.stash) {
|
|
967
|
+
setStash(option.item);
|
|
968
|
+
}
|
|
969
|
+
emit('update:modelValue', option.value);
|
|
970
|
+
await nextTick();
|
|
971
|
+
}
|
|
972
|
+
if (props.multiple && computedAllowWriting.value && shown.value) {
|
|
973
|
+
focusSearchInput();
|
|
974
|
+
} else {
|
|
975
|
+
focusActivator();
|
|
976
|
+
}
|
|
977
|
+
if (!props.multiple) {
|
|
978
|
+
close();
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
|
|
982
|
+
const onOptionUnselected = async (option: BaseOption) => {
|
|
983
|
+
removeFromStash((o) => o.valueHash !== option.valueHash);
|
|
984
|
+
if (props.multiple) {
|
|
985
|
+
const copy = { ...modelValueIndexedByHash.value };
|
|
986
|
+
delete copy[option.valueHash];
|
|
987
|
+
emit('update:modelValue', Object.values(copy));
|
|
988
|
+
} else {
|
|
989
|
+
emit('update:modelValue', null);
|
|
990
|
+
}
|
|
991
|
+
if (props.multiple && computedAllowWriting.value && shown.value) {
|
|
992
|
+
focusSearchInput();
|
|
993
|
+
} else {
|
|
994
|
+
focusActivator();
|
|
995
|
+
}
|
|
996
|
+
};
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* Filter options based on the current query.
|
|
1000
|
+
*/
|
|
1001
|
+
const filteredOptions = computed<BaseOption<Item>[]>(() => {
|
|
1002
|
+
const queryMatchesASelectedOption = selectedOptions.value.some(
|
|
1003
|
+
(option: BaseOption) => option.text === query.value
|
|
1004
|
+
);
|
|
1005
|
+
const shouldFilter =
|
|
1006
|
+
(props.multiple && query.value) ||
|
|
1007
|
+
(props.filterBy === 'not_stashed' && query.value) ||
|
|
1008
|
+
(query.value && !queryMatchesASelectedOption && !props.multiple);
|
|
1009
|
+
|
|
1010
|
+
if (!shouldFilter || !props.filterBy) return options.value;
|
|
1011
|
+
|
|
1012
|
+
const stashedHashes =
|
|
1013
|
+
props.filterBy === 'not_stashed'
|
|
1014
|
+
? new Set(innerStash.value.map((o) => o.valueHash))
|
|
1015
|
+
: null;
|
|
1016
|
+
|
|
1017
|
+
return options.value.filter((item: BaseOption) => {
|
|
1018
|
+
let matchedAnything = false;
|
|
1019
|
+
if (typeof props.filterBy === 'function') {
|
|
1020
|
+
matchedAnything = props.filterBy(item.value, item.item, query.value);
|
|
1021
|
+
} else if (props.filterBy === 'not_stashed' && query.value) {
|
|
1022
|
+
matchedAnything = !stashedHashes!.has(item.valueHash);
|
|
1023
|
+
} else if (Array.isArray(props.filterBy) && props.filterBy.length) {
|
|
1024
|
+
matchedAnything = matchAnyKey(
|
|
1025
|
+
item.item,
|
|
1026
|
+
props.filterBy,
|
|
1027
|
+
query.value.trim()
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
1030
|
+
return matchedAnything || matchAnyKey(item, ['text'], query.value.trim());
|
|
1031
|
+
});
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
const defaultGroupSymbol = Symbol('default');
|
|
1035
|
+
|
|
1036
|
+
const getGroupData = (option: BaseOption) => {
|
|
1037
|
+
if (isNil(props.groupBy)) return defaultGroupSymbol;
|
|
1038
|
+
if (typeof props.groupBy === 'function') return props.groupBy(option.item);
|
|
1039
|
+
return (get as any)(option.item, props.groupBy);
|
|
1040
|
+
};
|
|
1041
|
+
|
|
1042
|
+
const groupedOptions = computed<GroupedOptions[]>(() => {
|
|
1043
|
+
const groupIndex: Record<GroupedOptions['id'], GroupedOptions> = {};
|
|
1044
|
+
for (const option of filteredOptions.value) {
|
|
1045
|
+
const groupData = getGroupData(option);
|
|
1046
|
+
const groupId = hash(groupData);
|
|
1047
|
+
if (!groupIndex[groupId]) {
|
|
1048
|
+
groupIndex[groupId] = {
|
|
1049
|
+
id: groupId,
|
|
1050
|
+
data: groupData ?? null,
|
|
1051
|
+
options: [],
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
groupIndex[groupId].options.push(option);
|
|
1055
|
+
}
|
|
1056
|
+
return Object.values(groupIndex).filter((g) => g.options.length);
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
/**
|
|
1060
|
+
* START POPOVER STATE
|
|
1061
|
+
*/
|
|
1062
|
+
const shown = ref(false);
|
|
1063
|
+
const renderListBox = useUntil(shown);
|
|
1064
|
+
|
|
1065
|
+
const open = async ({
|
|
1066
|
+
focusPopoverContent = true,
|
|
1067
|
+
}: { focusPopoverContent?: boolean } = {}) => {
|
|
1068
|
+
if (props.disabled || props.readonly) return;
|
|
1069
|
+
if (shown.value) return;
|
|
1070
|
+
hasInteracted.value = true;
|
|
1071
|
+
resume();
|
|
1072
|
+
shown.value = true;
|
|
1073
|
+
|
|
1074
|
+
if (!hasPrefilled.value && !isPrefilling.value) {
|
|
1075
|
+
await prefillItems();
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
await nextTick();
|
|
1079
|
+
document.addEventListener('click', onOutsideInteraction);
|
|
1080
|
+
document.addEventListener('focusin', onOutsideInteraction);
|
|
1081
|
+
|
|
1082
|
+
if (!focusPopoverContent) return;
|
|
1083
|
+
|
|
1084
|
+
if (computedAllowWriting.value) {
|
|
1085
|
+
await wait(0);
|
|
1086
|
+
focusSearchInput();
|
|
1087
|
+
} else {
|
|
1088
|
+
await wait(0);
|
|
1089
|
+
focusPopoverFocusProxy();
|
|
1090
|
+
}
|
|
1091
|
+
};
|
|
1092
|
+
|
|
1093
|
+
const close = async () => {
|
|
1094
|
+
if (!shown.value) return;
|
|
1095
|
+
shown.value = false;
|
|
1096
|
+
optionsContainer.value?.blur();
|
|
1097
|
+
activeDescendantId.value = undefined;
|
|
1098
|
+
pause();
|
|
1099
|
+
document.removeEventListener('click', onOutsideInteraction);
|
|
1100
|
+
document.removeEventListener('focusin', onOutsideInteraction);
|
|
1101
|
+
query.value = '';
|
|
1102
|
+
};
|
|
1103
|
+
|
|
1104
|
+
const toggle = () => {
|
|
1105
|
+
if (shown.value) return close();
|
|
1106
|
+
return open();
|
|
1107
|
+
};
|
|
1108
|
+
/**
|
|
1109
|
+
* END POPOVER STATE
|
|
1110
|
+
*/
|
|
1111
|
+
|
|
1112
|
+
/**
|
|
1113
|
+
* START ACTIVE STATE
|
|
1114
|
+
* Tracks whether the user is inside this whole component.
|
|
1115
|
+
* We cannot use :focus-within as it doesn't work for teleported elements.
|
|
1116
|
+
*/
|
|
1117
|
+
const active = ref(false);
|
|
1118
|
+
|
|
1119
|
+
const setActive = () => {
|
|
1120
|
+
active.value = true;
|
|
1121
|
+
};
|
|
1122
|
+
const setInactive = () => {
|
|
1123
|
+
if (!active.value) return;
|
|
1124
|
+
active.value = false;
|
|
1125
|
+
emit('inactive');
|
|
1126
|
+
};
|
|
1127
|
+
|
|
1128
|
+
const isFocusInsideComponent = (target: EventTarget | null) => {
|
|
1129
|
+
if (!(target instanceof Node)) return false;
|
|
1130
|
+
const insideActivator = !!activatorEl.value?.contains(target);
|
|
1131
|
+
const insidePopover = !!optionsContainer.value?.$el
|
|
1132
|
+
?.closest('.bb-common-popover')
|
|
1133
|
+
?.contains(target);
|
|
1134
|
+
return insideActivator || insidePopover;
|
|
1135
|
+
};
|
|
1136
|
+
|
|
1137
|
+
const syncActiveStateOnBlur = () => {
|
|
1138
|
+
setTimeout(() => {
|
|
1139
|
+
if (isFocusInsideComponent(document.activeElement)) {
|
|
1140
|
+
setActive();
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
setInactive();
|
|
1144
|
+
}, 0);
|
|
1145
|
+
};
|
|
1146
|
+
/**
|
|
1147
|
+
* END ACTIVE STATE
|
|
1148
|
+
*/
|
|
1149
|
+
|
|
1150
|
+
const onClear = () => {
|
|
1151
|
+
emit('update:modelValue', props.multiple ? [] : null);
|
|
1152
|
+
};
|
|
1153
|
+
|
|
1154
|
+
/**
|
|
1155
|
+
* START ACTIVATOR LISTENERS
|
|
1156
|
+
* Programmatically attach accessibility attributes and event listeners
|
|
1157
|
+
* to the activator element, following the BbDropdown pattern.
|
|
1158
|
+
*/
|
|
1159
|
+
let cleanupActivator: (() => void) | null = null;
|
|
1160
|
+
|
|
1161
|
+
watch(
|
|
1162
|
+
activatorEl,
|
|
1163
|
+
(el) => {
|
|
1164
|
+
if (cleanupActivator) {
|
|
1165
|
+
cleanupActivator();
|
|
1166
|
+
cleanupActivator = null;
|
|
1167
|
+
}
|
|
1168
|
+
if (!el) return;
|
|
1169
|
+
|
|
1170
|
+
el.setAttribute('role', 'combobox');
|
|
1171
|
+
el.setAttribute('aria-haspopup', 'listbox');
|
|
1172
|
+
|
|
1173
|
+
const handleClick = (event: MouseEvent) => {
|
|
1174
|
+
emit('click', event);
|
|
1175
|
+
if (props.disabled || props.readonly) return;
|
|
1176
|
+
event.preventDefault();
|
|
1177
|
+
toggle();
|
|
1178
|
+
};
|
|
1179
|
+
|
|
1180
|
+
const handleKeydown = (event: KeyboardEvent) => {
|
|
1181
|
+
switch (event.key) {
|
|
1182
|
+
case 'ArrowDown':
|
|
1183
|
+
event.preventDefault();
|
|
1184
|
+
onArrowDown();
|
|
1185
|
+
break;
|
|
1186
|
+
case 'ArrowUp':
|
|
1187
|
+
event.preventDefault();
|
|
1188
|
+
onArrowUp();
|
|
1189
|
+
break;
|
|
1190
|
+
case 'Enter':
|
|
1191
|
+
event.preventDefault();
|
|
1192
|
+
onEnter();
|
|
1193
|
+
break;
|
|
1194
|
+
case 'Escape':
|
|
1195
|
+
if (shown.value) {
|
|
1196
|
+
event.preventDefault();
|
|
1197
|
+
onEscape();
|
|
1198
|
+
}
|
|
1199
|
+
break;
|
|
1200
|
+
}
|
|
1201
|
+
};
|
|
1202
|
+
|
|
1203
|
+
const handleFocus = (event: FocusEvent) => {
|
|
1204
|
+
emit('focus', event);
|
|
1205
|
+
setActive();
|
|
1206
|
+
};
|
|
1207
|
+
|
|
1208
|
+
const handleBlur = (event: FocusEvent) => {
|
|
1209
|
+
emit('blur', event);
|
|
1210
|
+
syncActiveStateOnBlur();
|
|
1211
|
+
};
|
|
1212
|
+
|
|
1213
|
+
el.addEventListener('click', handleClick);
|
|
1214
|
+
el.addEventListener('keydown', handleKeydown);
|
|
1215
|
+
el.addEventListener('focus', handleFocus);
|
|
1216
|
+
el.addEventListener('blur', handleBlur);
|
|
1217
|
+
|
|
1218
|
+
cleanupActivator = () => {
|
|
1219
|
+
el.removeEventListener('click', handleClick);
|
|
1220
|
+
el.removeEventListener('keydown', handleKeydown);
|
|
1221
|
+
el.removeEventListener('focus', handleFocus);
|
|
1222
|
+
el.removeEventListener('blur', handleBlur);
|
|
1223
|
+
el.removeAttribute('role');
|
|
1224
|
+
el.removeAttribute('aria-haspopup');
|
|
1225
|
+
el.removeAttribute('aria-expanded');
|
|
1226
|
+
el.removeAttribute('aria-controls');
|
|
1227
|
+
el.removeAttribute('aria-invalid');
|
|
1228
|
+
el.removeAttribute('aria-describedby');
|
|
1229
|
+
el.removeAttribute('aria-activedescendant');
|
|
1230
|
+
};
|
|
1231
|
+
},
|
|
1232
|
+
{ immediate: true }
|
|
1233
|
+
);
|
|
1234
|
+
|
|
1235
|
+
watch(
|
|
1236
|
+
[
|
|
1237
|
+
activatorEl,
|
|
1238
|
+
shown,
|
|
1239
|
+
renderListBox,
|
|
1240
|
+
computedAriaInvalid,
|
|
1241
|
+
computedAriaDescribedby,
|
|
1242
|
+
activeDescendantId,
|
|
1243
|
+
] as const,
|
|
1244
|
+
([el, isShown, hasListBox, ariaInvalid, ariaDescribedby, descendantId]) => {
|
|
1245
|
+
if (!el) return;
|
|
1246
|
+
el.setAttribute('aria-expanded', String(isShown));
|
|
1247
|
+
if (ariaInvalid != null) {
|
|
1248
|
+
el.setAttribute('aria-invalid', ariaInvalid);
|
|
1249
|
+
} else {
|
|
1250
|
+
el.removeAttribute('aria-invalid');
|
|
1251
|
+
}
|
|
1252
|
+
if (hasListBox) {
|
|
1253
|
+
el.setAttribute('aria-controls', `${id}_listbox`);
|
|
1254
|
+
} else {
|
|
1255
|
+
el.removeAttribute('aria-controls');
|
|
1256
|
+
}
|
|
1257
|
+
if (ariaDescribedby != null) {
|
|
1258
|
+
el.setAttribute('aria-describedby', ariaDescribedby);
|
|
1259
|
+
} else {
|
|
1260
|
+
el.removeAttribute('aria-describedby');
|
|
1261
|
+
}
|
|
1262
|
+
if (isShown && descendantId) {
|
|
1263
|
+
el.setAttribute('aria-activedescendant', descendantId);
|
|
1264
|
+
} else {
|
|
1265
|
+
el.removeAttribute('aria-activedescendant');
|
|
1266
|
+
}
|
|
1267
|
+
},
|
|
1268
|
+
{ immediate: true }
|
|
1269
|
+
);
|
|
1270
|
+
|
|
1271
|
+
onBeforeUnmount(() => {
|
|
1272
|
+
cleanupActivator?.();
|
|
1273
|
+
document.removeEventListener('click', onOutsideInteraction);
|
|
1274
|
+
document.removeEventListener('focusin', onOutsideInteraction);
|
|
1275
|
+
});
|
|
1276
|
+
/**
|
|
1277
|
+
* END ACTIVATOR LISTENERS
|
|
1278
|
+
*/
|
|
1279
|
+
</script>
|
|
1280
|
+
|
|
1281
|
+
<style lang="postcss">
|
|
1282
|
+
.bb-select-popover {
|
|
1283
|
+
width: var(--bb-select-popover-width, 320px);
|
|
1284
|
+
|
|
1285
|
+
.bb-select-popover__search-container {
|
|
1286
|
+
align-items: center;
|
|
1287
|
+
border-bottom: 1px solid var(--bb-border);
|
|
1288
|
+
display: flex;
|
|
1289
|
+
padding: 0.375rem 0.75rem;
|
|
1290
|
+
position: relative;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
.bb-select-popover__search-icon,
|
|
1294
|
+
.bb-select-popover__search-spinner,
|
|
1295
|
+
.bb-select-popover__search-error-icon {
|
|
1296
|
+
color: var(--bb-icon-color);
|
|
1297
|
+
flex-shrink: 0;
|
|
1298
|
+
width: var(--bb-input-icon);
|
|
1299
|
+
position: absolute;
|
|
1300
|
+
right: 0.75rem;
|
|
1301
|
+
top: 50%;
|
|
1302
|
+
transform: translateY(-50%);
|
|
1303
|
+
opacity: 0;
|
|
1304
|
+
transition:
|
|
1305
|
+
color 0.3s,
|
|
1306
|
+
opacity 0.3s;
|
|
1307
|
+
pointer-events: none;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
.bb-select-popover__search-input {
|
|
1311
|
+
background-color: transparent;
|
|
1312
|
+
border: 0;
|
|
1313
|
+
color: var(--bb-input-color);
|
|
1314
|
+
flex: 1 1 auto;
|
|
1315
|
+
font-size: var(--bb-input-font-size);
|
|
1316
|
+
line-height: var(--bb-leading);
|
|
1317
|
+
outline: none;
|
|
1318
|
+
padding: 0;
|
|
1319
|
+
padding-right: calc(var(--bb-input-icon) + 0.5rem);
|
|
1320
|
+
width: 100%;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
.bb-select-popover__search-icon {
|
|
1324
|
+
opacity: 1;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
&.bb-select-popover--errors {
|
|
1328
|
+
.bb-select-popover__search-error-icon {
|
|
1329
|
+
color: var(--bb-danger);
|
|
1330
|
+
opacity: 1;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
.bb-select-popover__search-icon,
|
|
1334
|
+
.bb-select-popover__search-spinner {
|
|
1335
|
+
opacity: 0;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
&.bb-select-popover--loading {
|
|
1340
|
+
.bb-select-popover__search-spinner {
|
|
1341
|
+
opacity: 1;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
.bb-select-popover__search-icon,
|
|
1345
|
+
.bb-select-popover__search-error-icon {
|
|
1346
|
+
opacity: 0;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
.bb-listbox {
|
|
1351
|
+
--option-h: var(--option-height);
|
|
1352
|
+
display: block;
|
|
1353
|
+
|
|
1354
|
+
&.bb-listbox--open {
|
|
1355
|
+
transition: min-height 0s 0s;
|
|
1356
|
+
|
|
1357
|
+
.bb-listbox__outer-container {
|
|
1358
|
+
grid-template-rows: 1fr;
|
|
1359
|
+
opacity: 1;
|
|
1360
|
+
transition-delay: 0s, 00ms;
|
|
1361
|
+
|
|
1362
|
+
> * {
|
|
1363
|
+
opacity: 1;
|
|
1364
|
+
transition-delay: calc(var(--transition-duration) * 0.8);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
&.bb-listbox--no-data {
|
|
1369
|
+
.bb-listbox__outer-container {
|
|
1370
|
+
.bb-listbox__inner-container {
|
|
1371
|
+
.bb-listbox__no-data {
|
|
1372
|
+
display: block;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
&.bb-listbox--loading {
|
|
1378
|
+
.bb-listbox__outer-container {
|
|
1379
|
+
.bb-listbox__inner-container {
|
|
1380
|
+
.bb-listbox__loading {
|
|
1381
|
+
display: block;
|
|
1382
|
+
text-align: left;
|
|
1383
|
+
}
|
|
1384
|
+
.bb-listbox__loading {
|
|
1385
|
+
display: hidden;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
&.bb-listbox--multiple {
|
|
1392
|
+
.autocomplete-option__checkbox {
|
|
1393
|
+
display: inline-block !important;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
.bb-listbox__outer-container {
|
|
1398
|
+
background-color: var(--bb-panel);
|
|
1399
|
+
border-color: var(--bb-primary);
|
|
1400
|
+
border-radius: var(--bb-radius);
|
|
1401
|
+
border-width: 0px;
|
|
1402
|
+
display: grid;
|
|
1403
|
+
grid-template-rows: 0fr;
|
|
1404
|
+
opacity: 0;
|
|
1405
|
+
transition-delay: calc(var(--transition-duration) * 0.25), 0s, 0s;
|
|
1406
|
+
transition-duration:
|
|
1407
|
+
var(--transition-duration), calc(var(--transition-duration) * 1.4),
|
|
1408
|
+
var(--transition-duration);
|
|
1409
|
+
transition-property: grid-template-rows, opacity, border;
|
|
1410
|
+
|
|
1411
|
+
> * {
|
|
1412
|
+
opacity: 0;
|
|
1413
|
+
transition-delay: 0s;
|
|
1414
|
+
transition-duration: calc(var(--transition-duration) * 2);
|
|
1415
|
+
transition-property: opacity;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
.bb-listbox__inner-container {
|
|
1419
|
+
overflow: auto;
|
|
1420
|
+
overflow-x: clip;
|
|
1421
|
+
text-align: left;
|
|
1422
|
+
/* Always show options up to the bottom of the screen, but not more than 6 options */
|
|
1423
|
+
max-height: min(
|
|
1424
|
+
calc(var(--option-h) * 6),
|
|
1425
|
+
calc(round(down, calc(100dvh - 130px), var(--option-h)) - 1px)
|
|
1426
|
+
);
|
|
1427
|
+
|
|
1428
|
+
.bb-listbox__loading,
|
|
1429
|
+
.bb-listbox__no-data {
|
|
1430
|
+
color: var(--bb-text);
|
|
1431
|
+
display: none;
|
|
1432
|
+
padding-bottom: var(--bb-select-option-py);
|
|
1433
|
+
padding-left: var(--bb-select-option-px);
|
|
1434
|
+
padding-right: var(--bb-select-option-px);
|
|
1435
|
+
padding-top: var(--bb-select-option-py);
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
span[role='listbox'] {
|
|
1439
|
+
position: relative;
|
|
1440
|
+
width: 100%;
|
|
1441
|
+
display: block;
|
|
1442
|
+
|
|
1443
|
+
.bb-listbox__group {
|
|
1444
|
+
left: 0;
|
|
1445
|
+
position: absolute;
|
|
1446
|
+
top: 0;
|
|
1447
|
+
width: 100%;
|
|
1448
|
+
display: block;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
.bb-listbox__group-header {
|
|
1452
|
+
display: flex;
|
|
1453
|
+
align-items: center;
|
|
1454
|
+
height: var(--group-header-height, 32px);
|
|
1455
|
+
padding-left: var(--bb-select-option-px);
|
|
1456
|
+
padding-right: var(--bb-select-option-px);
|
|
1457
|
+
|
|
1458
|
+
user-select: none;
|
|
1459
|
+
|
|
1460
|
+
.bb-listbox__group-header-label {
|
|
1461
|
+
display: flex;
|
|
1462
|
+
align-items: center;
|
|
1463
|
+
gap: 16px;
|
|
1464
|
+
flex: auto;
|
|
1465
|
+
text-align: left;
|
|
1466
|
+
overflow: hidden;
|
|
1467
|
+
text-overflow: ellipsis;
|
|
1468
|
+
white-space: nowrap;
|
|
1469
|
+
color: color-mix(in srgb, var(--bb-text) 50%, transparent);
|
|
1470
|
+
font-size: 0.75rem;
|
|
1471
|
+
font-weight: 600;
|
|
1472
|
+
letter-spacing: 0.025em;
|
|
1473
|
+
text-transform: uppercase;
|
|
1474
|
+
|
|
1475
|
+
&::after {
|
|
1476
|
+
content: '';
|
|
1477
|
+
display: block;
|
|
1478
|
+
width: 100%;
|
|
1479
|
+
height: 1px;
|
|
1480
|
+
background-color: var(--bb-border);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
.bb-listbox__option {
|
|
1486
|
+
left: 0;
|
|
1487
|
+
position: absolute;
|
|
1488
|
+
display: block;
|
|
1489
|
+
top: 0;
|
|
1490
|
+
width: 100%;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
.bb-listbox__group > .bb-listbox__option {
|
|
1494
|
+
position: static;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
.autocomplete-option {
|
|
1498
|
+
--bg-opacity: 0;
|
|
1499
|
+
--bg-base-color: var(--bb-text);
|
|
1500
|
+
align-items: center;
|
|
1501
|
+
background-color: color-mix(
|
|
1502
|
+
in srgb,
|
|
1503
|
+
var(--bg-base-color) calc(100% * var(--bg-opacity)),
|
|
1504
|
+
transparent
|
|
1505
|
+
);
|
|
1506
|
+
color: var(--bb-text);
|
|
1507
|
+
cursor: pointer;
|
|
1508
|
+
display: flex;
|
|
1509
|
+
flex-grow: 1;
|
|
1510
|
+
gap: 8px;
|
|
1511
|
+
height: var(--option-h);
|
|
1512
|
+
padding-left: var(--bb-select-option-px);
|
|
1513
|
+
padding-right: var(--bb-select-option-px);
|
|
1514
|
+
transition-duration: 250ms;
|
|
1515
|
+
transition-property: color, background-color;
|
|
1516
|
+
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
1517
|
+
width: 100%;
|
|
1518
|
+
|
|
1519
|
+
&:hover:not(.autocomplete-option--disabled) {
|
|
1520
|
+
--bg-opacity: 0.05;
|
|
1521
|
+
--bg-base-color: var(--bb-text);
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
&--focused {
|
|
1525
|
+
--bg-opacity: 0.15;
|
|
1526
|
+
--bg-base-color: var(--bb-text);
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
&--focused:hover:not(.autocomplete-option--disabled) {
|
|
1530
|
+
--bg-opacity: 0.2;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
&--selected {
|
|
1534
|
+
--bg-opacity: 0.1;
|
|
1535
|
+
--bg-base-color: var(--bb-primary) !important;
|
|
1536
|
+
color: var(--bb-primary);
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
&--selected:hover:not(.autocomplete-option--disabled) {
|
|
1540
|
+
--bg-opacity: 0.15;
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
&--selected.autocomplete-option--focused {
|
|
1544
|
+
--bg-opacity: 0.2;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
&--selected.autocomplete-option--focused:hover:not(
|
|
1548
|
+
.autocomplete-option--disabled
|
|
1549
|
+
) {
|
|
1550
|
+
--bg-opacity: 0.2;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
&--disabled {
|
|
1554
|
+
cursor: not-allowed;
|
|
1555
|
+
opacity: 0.5;
|
|
1556
|
+
|
|
1557
|
+
.autocomplete-option__checkbox.autocomplete-option__checkbox {
|
|
1558
|
+
background-color: var(--bb-panel-disabled);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
.autocomplete-option__checkbox {
|
|
1563
|
+
pointer-events: none;
|
|
1564
|
+
display: none;
|
|
1565
|
+
width: 16px;
|
|
1566
|
+
height: 16px;
|
|
1567
|
+
background-color: var(--bb-panel);
|
|
1568
|
+
border-color: var(--bb-border);
|
|
1569
|
+
border-radius: min(6px, var(--bb-radius));
|
|
1570
|
+
border-style: solid;
|
|
1571
|
+
border-width: 1px;
|
|
1572
|
+
margin-left: -6px;
|
|
1573
|
+
transition-duration: 300ms;
|
|
1574
|
+
transition-property:
|
|
1575
|
+
color, background-color, border-color, text-decoration-color,
|
|
1576
|
+
box-shadow;
|
|
1577
|
+
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
1578
|
+
|
|
1579
|
+
path {
|
|
1580
|
+
stroke: var(--bb-contrasting);
|
|
1581
|
+
stroke-dasharray: 105;
|
|
1582
|
+
stroke-dashoffset: 105;
|
|
1583
|
+
transition: stroke-dashoffset 0.3s;
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
.autocomplete-option__label {
|
|
1588
|
+
display: block;
|
|
1589
|
+
flex: auto;
|
|
1590
|
+
text-align: left;
|
|
1591
|
+
overflow: hidden;
|
|
1592
|
+
text-overflow: ellipsis;
|
|
1593
|
+
white-space: nowrap;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
&.autocomplete-option--selected {
|
|
1597
|
+
.autocomplete-option__checkbox {
|
|
1598
|
+
background-color: var(--bb-primary);
|
|
1599
|
+
border-color: var(--bb-primary);
|
|
1600
|
+
|
|
1601
|
+
path {
|
|
1602
|
+
stroke-dashoffset: 0;
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
</style>
|
|
1613
|
+
```
|
|
1614
|
+
|
|
1615
|
+
## Types
|
|
1616
|
+
|
|
1617
|
+
```ts
|
|
1618
|
+
import type { HTMLAttributes, InputHTMLAttributes } from 'vue';
|
|
1619
|
+
import type { CommonProps } from '@/types/CommonProps';
|
|
1620
|
+
|
|
1621
|
+
export type BbSelectPopoverProps<Item> = {
|
|
1622
|
+
/**
|
|
1623
|
+
* External activator element or component ref.
|
|
1624
|
+
* When provided, the activator slot is not rendered and event listeners
|
|
1625
|
+
* are attached programmatically to the referenced element.
|
|
1626
|
+
*/
|
|
1627
|
+
activator?: HTMLElement | Record<string, any> | null;
|
|
1628
|
+
|
|
1629
|
+
/**
|
|
1630
|
+
* Controls whether the search input is shown inside the popover panel.
|
|
1631
|
+
* Use `'not-mobile'` to hide the search input on mobile.
|
|
1632
|
+
*
|
|
1633
|
+
* @defaultValue `false`
|
|
1634
|
+
*/
|
|
1635
|
+
allowWriting?: boolean | 'not-mobile';
|
|
1636
|
+
|
|
1637
|
+
/**
|
|
1638
|
+
* Browser autocomplete hint for the popover search input.
|
|
1639
|
+
*
|
|
1640
|
+
* @defaultValue `'off'`
|
|
1641
|
+
*/
|
|
1642
|
+
autocomplete?: InputHTMLAttributes['autocomplete'];
|
|
1643
|
+
|
|
1644
|
+
/**
|
|
1645
|
+
* Apply the compact density variant.
|
|
1646
|
+
*/
|
|
1647
|
+
compact?: boolean;
|
|
1648
|
+
|
|
1649
|
+
/**
|
|
1650
|
+
* Space the arrow keeps from popover edges. Useful with rounded corners.
|
|
1651
|
+
*
|
|
1652
|
+
* @defaultValue `10`
|
|
1653
|
+
*/
|
|
1654
|
+
arrowPadding?: CommonProps['arrowPadding'];
|
|
1655
|
+
|
|
1656
|
+
/**
|
|
1657
|
+
* Constrains the popover to a specific boundary element.
|
|
1658
|
+
*/
|
|
1659
|
+
boundary?: HTMLElement | Record<string, any> | string | null;
|
|
1660
|
+
|
|
1661
|
+
/**
|
|
1662
|
+
* Additional dependencies that trigger item reloading when changed.
|
|
1663
|
+
*
|
|
1664
|
+
* @defaultValue `[]`
|
|
1665
|
+
*/
|
|
1666
|
+
dependencies?: any[];
|
|
1667
|
+
|
|
1668
|
+
/**
|
|
1669
|
+
* Debounce delay (ms) for dependency-triggered reloads.
|
|
1670
|
+
*
|
|
1671
|
+
* @defaultValue `0`
|
|
1672
|
+
*/
|
|
1673
|
+
depsDebounceTime?: number;
|
|
1674
|
+
|
|
1675
|
+
/**
|
|
1676
|
+
* Disables the component.
|
|
1677
|
+
*/
|
|
1678
|
+
disabled?: boolean;
|
|
1679
|
+
|
|
1680
|
+
/**
|
|
1681
|
+
* Marks the component as being in error state.
|
|
1682
|
+
*/
|
|
1683
|
+
hasErrors?: boolean;
|
|
1684
|
+
|
|
1685
|
+
/**
|
|
1686
|
+
* Explicit invalid state for accessibility semantics.
|
|
1687
|
+
* Falls back to `hasErrors` when not provided.
|
|
1688
|
+
*/
|
|
1689
|
+
ariaInvalid?: boolean;
|
|
1690
|
+
|
|
1691
|
+
/**
|
|
1692
|
+
* Space-separated IDREF(s) that describe this field for assistive technologies.
|
|
1693
|
+
* Accepts either a string or array of ids.
|
|
1694
|
+
*/
|
|
1695
|
+
ariaDescribedby?: string | string[];
|
|
1696
|
+
|
|
1697
|
+
/**
|
|
1698
|
+
* Enable or disable floating flip behavior.
|
|
1699
|
+
*
|
|
1700
|
+
* @defaultValue `true`
|
|
1701
|
+
*/
|
|
1702
|
+
flip?: boolean;
|
|
1703
|
+
|
|
1704
|
+
/**
|
|
1705
|
+
* Whether the floating arrow is hidden.
|
|
1706
|
+
*
|
|
1707
|
+
* @defaultValue `true`
|
|
1708
|
+
*/
|
|
1709
|
+
hideArrow?: boolean;
|
|
1710
|
+
|
|
1711
|
+
/**
|
|
1712
|
+
* Reset modelValue to `null` or empty array if it no longer matches available items.
|
|
1713
|
+
*/
|
|
1714
|
+
enforceCoherence?: boolean;
|
|
1715
|
+
|
|
1716
|
+
/**
|
|
1717
|
+
* Controls how options are filtered during search.
|
|
1718
|
+
*
|
|
1719
|
+
* @defaultValue `() => []`
|
|
1720
|
+
*/
|
|
1721
|
+
filterBy?:
|
|
1722
|
+
| string
|
|
1723
|
+
| string[]
|
|
1724
|
+
| false
|
|
1725
|
+
| 'not_stashed'
|
|
1726
|
+
| ((value: any, item: any, query: string | null) => boolean)
|
|
1727
|
+
| null;
|
|
1728
|
+
|
|
1729
|
+
/** Path to item property for grouping options. */
|
|
1730
|
+
groupBy?: string | ((item: Item) => string | number | symbol);
|
|
1731
|
+
|
|
1732
|
+
/**
|
|
1733
|
+
* Height of group headers in the listbox (px). Defaults to 24px (compact) or 32px.
|
|
1734
|
+
* Only applies when `groupBy` is set.
|
|
1735
|
+
*/
|
|
1736
|
+
headerHeight?: number;
|
|
1737
|
+
|
|
1738
|
+
/**
|
|
1739
|
+
* Explicit id for the component. Used to generate ids for listbox and options.
|
|
1740
|
+
*/
|
|
1741
|
+
id?: HTMLAttributes['id'];
|
|
1742
|
+
|
|
1743
|
+
/**
|
|
1744
|
+
* Array of items or function to load them asynchronously.
|
|
1745
|
+
* Functions receive `(query, prefill, modelValue)` and can return a promise.
|
|
1746
|
+
*
|
|
1747
|
+
* @defaultValue `[]`
|
|
1748
|
+
*/
|
|
1749
|
+
items:
|
|
1750
|
+
| Item[]
|
|
1751
|
+
| ((query: string, prefill: boolean, modelValue: any[]) => Promise<Item[]>)
|
|
1752
|
+
| ((query: string, prefill: boolean, modelValue: any[]) => Item[]);
|
|
1753
|
+
|
|
1754
|
+
/**
|
|
1755
|
+
* Height of the options in the listbox (px).
|
|
1756
|
+
*
|
|
1757
|
+
* @defaultValue `40`
|
|
1758
|
+
*/
|
|
1759
|
+
itemHeight?: number;
|
|
1760
|
+
|
|
1761
|
+
/**
|
|
1762
|
+
* Path to item property for display text or function to extract it.
|
|
1763
|
+
*/
|
|
1764
|
+
itemText?: string | ((item: Item) => string) | undefined;
|
|
1765
|
+
|
|
1766
|
+
/**
|
|
1767
|
+
* Path to item property for value or function to extract it.
|
|
1768
|
+
*/
|
|
1769
|
+
itemValue?: string | ((item: Item) => string) | undefined;
|
|
1770
|
+
|
|
1771
|
+
/**
|
|
1772
|
+
* Display the loading state styles.
|
|
1773
|
+
*/
|
|
1774
|
+
loading?: boolean;
|
|
1775
|
+
|
|
1776
|
+
/**
|
|
1777
|
+
* Message shown while items are being loaded.
|
|
1778
|
+
*/
|
|
1779
|
+
loadingText?: string;
|
|
1780
|
+
|
|
1781
|
+
/**
|
|
1782
|
+
* Maximum number of selectable items (limits selection when `multiple` is true).
|
|
1783
|
+
*
|
|
1784
|
+
* @defaultValue `Infinity`
|
|
1785
|
+
*/
|
|
1786
|
+
max?: number;
|
|
1787
|
+
|
|
1788
|
+
/**
|
|
1789
|
+
* v-model value. Single value for single select, array for multiple select.
|
|
1790
|
+
*/
|
|
1791
|
+
modelValue: any;
|
|
1792
|
+
|
|
1793
|
+
/**
|
|
1794
|
+
* Debounce delay (ms) for modelValue change handling.
|
|
1795
|
+
*
|
|
1796
|
+
* @defaultValue `0`
|
|
1797
|
+
*/
|
|
1798
|
+
modelValueDebounceTime?: number;
|
|
1799
|
+
|
|
1800
|
+
/**
|
|
1801
|
+
* Enable multiple item selection. When true, modelValue must be an array.
|
|
1802
|
+
*/
|
|
1803
|
+
multiple?: boolean;
|
|
1804
|
+
|
|
1805
|
+
/**
|
|
1806
|
+
* Message displayed when no items are available.
|
|
1807
|
+
*/
|
|
1808
|
+
noDataText?: string;
|
|
1809
|
+
|
|
1810
|
+
/**
|
|
1811
|
+
* Distance (px) between the activator and the popover.
|
|
1812
|
+
*
|
|
1813
|
+
* @defaultValue `4`
|
|
1814
|
+
*/
|
|
1815
|
+
offset?: number;
|
|
1816
|
+
|
|
1817
|
+
/**
|
|
1818
|
+
* Minimum page padding used by floating shift middleware.
|
|
1819
|
+
*
|
|
1820
|
+
* @defaultValue `0`
|
|
1821
|
+
*/
|
|
1822
|
+
padding?: CommonProps['padding'];
|
|
1823
|
+
|
|
1824
|
+
/**
|
|
1825
|
+
* Floating placement of the popover.
|
|
1826
|
+
*
|
|
1827
|
+
* @defaultValue `'bottom'`
|
|
1828
|
+
*/
|
|
1829
|
+
placement?: CommonProps['placement'];
|
|
1830
|
+
|
|
1831
|
+
/**
|
|
1832
|
+
* Controls when items are pre-loaded.
|
|
1833
|
+
* `'interaction'` loads on first user interaction, `true` loads immediately,
|
|
1834
|
+
* `false` loads only on explicit search/open flows.
|
|
1835
|
+
*
|
|
1836
|
+
* @defaultValue `'interaction'`
|
|
1837
|
+
*/
|
|
1838
|
+
prefill?: boolean | 'interaction';
|
|
1839
|
+
|
|
1840
|
+
/**
|
|
1841
|
+
* Debounce delay (ms) before triggering search queries after user input stops.
|
|
1842
|
+
*
|
|
1843
|
+
* @defaultValue `500`
|
|
1844
|
+
*/
|
|
1845
|
+
queryDebounceTime?: number;
|
|
1846
|
+
|
|
1847
|
+
/**
|
|
1848
|
+
* Prevents opening the popover while keeping the activator accessible.
|
|
1849
|
+
*/
|
|
1850
|
+
readonly?: boolean;
|
|
1851
|
+
|
|
1852
|
+
/**
|
|
1853
|
+
* Accessible label used by the search input rendered inside the popover.
|
|
1854
|
+
*
|
|
1855
|
+
* @defaultValue `'Search options'`
|
|
1856
|
+
*/
|
|
1857
|
+
searchInputAriaLabel?: string;
|
|
1858
|
+
|
|
1859
|
+
/**
|
|
1860
|
+
* Accumulate selected items across searches instead of replacing them.
|
|
1861
|
+
*/
|
|
1862
|
+
stash?: boolean;
|
|
1863
|
+
|
|
1864
|
+
/**
|
|
1865
|
+
* Transition duration (ms) for popover show/hide animations.
|
|
1866
|
+
*
|
|
1867
|
+
* @defaultValue `300`
|
|
1868
|
+
*/
|
|
1869
|
+
transitionDuration?: number;
|
|
1870
|
+
|
|
1871
|
+
/**
|
|
1872
|
+
* Width of the popover in pixels, percentage, or `'auto'`.
|
|
1873
|
+
*/
|
|
1874
|
+
width?: number | string;
|
|
1875
|
+
};
|
|
1876
|
+
|
|
1877
|
+
export type BbSelectPopoverEvents = {
|
|
1878
|
+
/**
|
|
1879
|
+
* Emitted when the activator loses focus.
|
|
1880
|
+
*/
|
|
1881
|
+
(e: 'blur', event: FocusEvent): void;
|
|
1882
|
+
/**
|
|
1883
|
+
* Emitted when the value changes via native change event.
|
|
1884
|
+
*/
|
|
1885
|
+
(e: 'change', event: Event): void;
|
|
1886
|
+
/**
|
|
1887
|
+
* Emitted when the activator is clicked.
|
|
1888
|
+
*/
|
|
1889
|
+
(e: 'click', event: MouseEvent): void;
|
|
1890
|
+
/**
|
|
1891
|
+
* Emitted when the activator receives focus.
|
|
1892
|
+
*/
|
|
1893
|
+
(e: 'focus', event: FocusEvent): void;
|
|
1894
|
+
/**
|
|
1895
|
+
* Emitted when focus moves outside the entire component after it was active.
|
|
1896
|
+
*/
|
|
1897
|
+
(e: 'inactive'): void;
|
|
1898
|
+
/**
|
|
1899
|
+
* Emitted on native input events from the search input.
|
|
1900
|
+
*/
|
|
1901
|
+
(e: 'input', event: Event): void;
|
|
1902
|
+
/**
|
|
1903
|
+
* Emitted with the next selection value whenever selection changes.
|
|
1904
|
+
*/
|
|
1905
|
+
(e: 'update:modelValue', value: any): void;
|
|
1906
|
+
/**
|
|
1907
|
+
* Emitted when the user types a new value and confirms it (Enter key) when `allowWriting` is enabled.
|
|
1908
|
+
*/
|
|
1909
|
+
(e: 'option:add', text: string): void;
|
|
1910
|
+
};
|
|
1911
|
+
|
|
1912
|
+
/** Props exposed by the `activator` slot. */
|
|
1913
|
+
export type BbSelectPopoverActivatorSlotProps = {
|
|
1914
|
+
/** Bind onto the activator element with `v-bind="props"` to wire up the popover reference. */
|
|
1915
|
+
props: {
|
|
1916
|
+
ref: (reference: unknown) => void;
|
|
1917
|
+
};
|
|
1918
|
+
/** Whether the popover is currently open. */
|
|
1919
|
+
shown: boolean;
|
|
1920
|
+
/** Whether the select is disabled. */
|
|
1921
|
+
disabled: boolean | undefined;
|
|
1922
|
+
/** Whether the select is read-only. */
|
|
1923
|
+
readonly: boolean | undefined;
|
|
1924
|
+
/** Whether options are currently being fetched. */
|
|
1925
|
+
loading: boolean;
|
|
1926
|
+
/** The current search query string. */
|
|
1927
|
+
query: string;
|
|
1928
|
+
/** Clears the current selection and query. */
|
|
1929
|
+
clear: () => void;
|
|
1930
|
+
/** Opens the popover. */
|
|
1931
|
+
open: () => Promise<void>;
|
|
1932
|
+
/** Closes the popover. */
|
|
1933
|
+
close: () => Promise<void>;
|
|
1934
|
+
/** Toggles the popover open/closed. */
|
|
1935
|
+
toggle: () => Promise<void>;
|
|
1936
|
+
};
|
|
1937
|
+
|
|
1938
|
+
/** Props exposed by the `option` slot. */
|
|
1939
|
+
export type BbSelectPopoverOptionSlotProps<Item> = {
|
|
1940
|
+
/** Whether this option is disabled. */
|
|
1941
|
+
disabled: boolean;
|
|
1942
|
+
/** Whether this option currently has keyboard focus. */
|
|
1943
|
+
focused: boolean;
|
|
1944
|
+
/** Zero-based index of this option in the list. */
|
|
1945
|
+
index: number;
|
|
1946
|
+
/** The raw item from the `items` prop. */
|
|
1947
|
+
item: Item;
|
|
1948
|
+
/** Whether items are still being fetched. */
|
|
1949
|
+
loading: boolean;
|
|
1950
|
+
/** Whether this option is currently selected. */
|
|
1951
|
+
selected: boolean;
|
|
1952
|
+
/** The resolved display text for this option. */
|
|
1953
|
+
text: string;
|
|
1954
|
+
/** The resolved value for this option. */
|
|
1955
|
+
value: any;
|
|
1956
|
+
};
|
|
1957
|
+
|
|
1958
|
+
/** Props exposed by the `group` slot. */
|
|
1959
|
+
export type BbSelectPopoverGroupSlotProps<Item> = {
|
|
1960
|
+
/** The display text for this group. */
|
|
1961
|
+
text: string;
|
|
1962
|
+
/** The raw group item. */
|
|
1963
|
+
item: Item;
|
|
1964
|
+
/** Zero-based index of the first option in this group. */
|
|
1965
|
+
index: number;
|
|
1966
|
+
/** Number of options in this group. */
|
|
1967
|
+
length: number;
|
|
1968
|
+
/** Whether all options in this group are disabled. */
|
|
1969
|
+
disabled: boolean;
|
|
1970
|
+
};
|
|
1971
|
+
|
|
1972
|
+
export type BbSelectPopoverSlots<Item> = {
|
|
1973
|
+
/**
|
|
1974
|
+
* Replaces the default trigger element that opens the select popover.
|
|
1975
|
+
*/
|
|
1976
|
+
activator?: (props: BbSelectPopoverActivatorSlotProps) => any;
|
|
1977
|
+
/**
|
|
1978
|
+
* Content shown inside the options dropdown while items are loading.
|
|
1979
|
+
*/
|
|
1980
|
+
loading?: (props: { query: string }) => any;
|
|
1981
|
+
/**
|
|
1982
|
+
* Content shown when no options match the query or the list is empty.
|
|
1983
|
+
*/
|
|
1984
|
+
'no-data'?: (props: { query: string; focus: () => void }) => any;
|
|
1985
|
+
/**
|
|
1986
|
+
* Replaces the default rendering of each option row in the dropdown.
|
|
1987
|
+
*/
|
|
1988
|
+
option?: (props: BbSelectPopoverOptionSlotProps<Item>) => any;
|
|
1989
|
+
/**
|
|
1990
|
+
* Replaces the default group header rendered above grouped options.
|
|
1991
|
+
*/
|
|
1992
|
+
group?: (props: BbSelectPopoverGroupSlotProps<Item>) => any;
|
|
1993
|
+
/**
|
|
1994
|
+
* Content appended inside the options list, after the last option.
|
|
1995
|
+
*/
|
|
1996
|
+
'options:append'?: (props: { focus: () => void }) => any;
|
|
1997
|
+
/**
|
|
1998
|
+
* Content appended outside the options list container.
|
|
1999
|
+
*/
|
|
2000
|
+
'options:append:outer'?: (props: { focus: () => void }) => any;
|
|
2001
|
+
/**
|
|
2002
|
+
* Content prepended inside the options list, before the first option.
|
|
2003
|
+
*/
|
|
2004
|
+
'options:prepend'?: (props: { focus: () => void }) => any;
|
|
2005
|
+
/**
|
|
2006
|
+
* Content prepended outside the options list container.
|
|
2007
|
+
*/
|
|
2008
|
+
'options:prepend:outer'?: (props: { focus: () => void }) => any;
|
|
2009
|
+
};
|
|
2010
|
+
```
|