vectis-ui 0.10.1 → 0.10.2
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/components/VAccordion/VAccordion.js.map +1 -1
- package/dist/components/VAccordion/VAccordion.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VAccordion/VAccordionItem.d.ts +6 -21
- package/dist/components/VAccordion/VAccordionItem.js.map +1 -1
- package/dist/components/VAccordion/VAccordionItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VAccordion/context.js.map +1 -1
- package/dist/components/VAvatar/VAvatar.d.ts +2 -12
- package/dist/components/VAvatar/VAvatar.js.map +1 -1
- package/dist/components/VAvatar/VAvatar.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VAvatar/VAvatarGroup.d.ts +2 -2
- package/dist/components/VAvatar/VAvatarGroup.js.map +1 -1
- package/dist/components/VAvatar/VAvatarGroup.vue_vue_type_script_setup_true_lang.js +6 -6
- package/dist/components/VAvatar/VAvatarGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VAvatar/context.d.ts +1 -5
- package/dist/components/VAvatar/context.js.map +1 -1
- package/dist/components/VBadge/VBadge.d.ts +13 -27
- package/dist/components/VBadge/VBadge.js.map +1 -1
- package/dist/components/VBadge/VBadge.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VBreadcrumb/VBreadcrumb.d.ts +3 -9
- package/dist/components/VBreadcrumb/VBreadcrumb.js.map +1 -1
- package/dist/components/VBreadcrumb/VBreadcrumb.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VButton/VButton.d.ts +10 -17
- package/dist/components/VButton/VButton.js.map +1 -1
- package/dist/components/VButton/VButton.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VButton/VButtonGroup.d.ts +6 -15
- package/dist/components/VButton/VButtonGroup.js.map +1 -1
- package/dist/components/VButton/VButtonGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VButton/context.d.ts +1 -9
- package/dist/components/VButton/context.js.map +1 -1
- package/dist/components/VCalendar/VCalendar.d.ts +9 -24
- package/dist/components/VCalendar/VCalendar.js.map +1 -1
- package/dist/components/VCalendar/VCalendar.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCalendar/VCalendarEvent.d.ts +4 -13
- package/dist/components/VCalendar/VCalendarEvent.js.map +1 -1
- package/dist/components/VCalendar/VCalendarEvent.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCalendar/VCalendarMonth.js.map +1 -1
- package/dist/components/VCalendar/VCalendarMonth.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCalendar/VCalendarTimeGrid.d.ts +1 -5
- package/dist/components/VCalendar/VCalendarTimeGrid.js.map +1 -1
- package/dist/components/VCalendar/VCalendarTimeGrid.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCalendar/VCalendarYear.js.map +1 -1
- package/dist/components/VCalendar/VCalendarYear.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCalendar/color.d.ts +1 -7
- package/dist/components/VCalendar/color.js.map +1 -1
- package/dist/components/VCalendar/edgeStep.d.ts +5 -15
- package/dist/components/VCalendar/edgeStep.js.map +1 -1
- package/dist/components/VCalendar/gesture.d.ts +12 -36
- package/dist/components/VCalendar/gesture.js.map +1 -1
- package/dist/components/VCalendar/keyboard.d.ts +5 -42
- package/dist/components/VCalendar/keyboard.js.map +1 -1
- package/dist/components/VCalendar/layout.d.ts +38 -199
- package/dist/components/VCalendar/layout.js.map +1 -1
- package/dist/components/VCalendar/timeGrid.d.ts +3 -25
- package/dist/components/VCalendar/timeGrid.js.map +1 -1
- package/dist/components/VCalendar/types.d.ts +6 -23
- package/dist/components/VCarousel/VCarousel.d.ts +25 -96
- package/dist/components/VCarousel/VCarousel.js.map +1 -1
- package/dist/components/VCarousel/VCarousel.vue_vue_type_script_setup_true_lang.js +6 -6
- package/dist/components/VCarousel/VCarousel.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCarousel/VCarouselItem.d.ts +1 -6
- package/dist/components/VCarousel/VCarouselItem.js.map +1 -1
- package/dist/components/VCarousel/VCarouselItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCarousel/context.js.map +1 -1
- package/dist/components/VCheckbox/VCheckbox.d.ts +1 -5
- package/dist/components/VCheckbox/VCheckbox.js.map +1 -1
- package/dist/components/VCheckbox/VCheckbox.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VChip/VChip.d.ts +2 -8
- package/dist/components/VChip/VChip.js.map +1 -1
- package/dist/components/VChip/VChip.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCombobox/VCombobox.d.ts +21 -59
- package/dist/components/VCombobox/VCombobox.js.map +1 -1
- package/dist/components/VCombobox/VCombobox.vue_vue_type_script_setup_true_lang.js +117 -119
- package/dist/components/VCombobox/VCombobox.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCombobox/VComboboxGroup.js.map +1 -1
- package/dist/components/VCombobox/VComboboxGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCombobox/VComboboxOption.js.map +1 -1
- package/dist/components/VCombobox/VComboboxOption.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCombobox/VComboboxSeparator.js.map +1 -1
- package/dist/components/VCombobox/VComboboxSeparator.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VCombobox/infiniteScroll.d.ts +4 -10
- package/dist/components/VCombobox/infiniteScroll.js.map +1 -1
- package/dist/components/VDataTable/VDataTable.d.ts +11 -36
- package/dist/components/VDataTable/VDataTable.js.map +1 -1
- package/dist/components/VDataTable/VDataTable.vue_vue_type_script_setup_true_lang.js +23 -20
- package/dist/components/VDataTable/VDataTable.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VDateInput/VDateInput.d.ts +12 -21
- package/dist/components/VDateInput/VDateInput.js.map +1 -1
- package/dist/components/VDateInput/VDateInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VDateInput/mask.d.ts +14 -59
- package/dist/components/VDateInput/mask.js.map +1 -1
- package/dist/components/VDatePicker/VDatePicker.d.ts +6 -12
- package/dist/components/VDatePicker/VDatePicker.js.map +1 -1
- package/dist/components/VDatePicker/VDatePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VDatePicker/keyboard.d.ts +10 -42
- package/dist/components/VDatePicker/keyboard.js.map +1 -1
- package/dist/components/VDatePicker/names.d.ts +8 -22
- package/dist/components/VDatePicker/names.js.map +1 -1
- package/dist/components/VDialog/VDialog.d.ts +5 -10
- package/dist/components/VDialog/VDialog.js.map +1 -1
- package/dist/components/VDialog/VDialog.vue_vue_type_script_setup_true_lang.js +51 -53
- package/dist/components/VDialog/VDialog.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VDialog/VDialogAlert.d.ts +2 -3
- package/dist/components/VDialog/VDialogAlert.js.map +1 -1
- package/dist/components/VDialog/VDialogAlert.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VFileInput/VFileInput.d.ts +11 -37
- package/dist/components/VFileInput/VFileInput.js.map +1 -1
- package/dist/components/VFileInput/VFileInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VFileInput/truncate.d.ts +3 -23
- package/dist/components/VFileInput/truncate.js.map +1 -1
- package/dist/components/VFilePicker/VFilePicker.d.ts +15 -65
- package/dist/components/VFilePicker/VFilePicker.js.map +1 -1
- package/dist/components/VFilePicker/VFilePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VFilePicker/fileKind.js.map +1 -1
- package/dist/components/VHotkeys/VHotkeys.d.ts +6 -15
- package/dist/components/VHotkeys/VHotkeys.js.map +1 -1
- package/dist/components/VHotkeys/VHotkeys.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VHotkeys/platform.d.ts +10 -49
- package/dist/components/VHotkeys/platform.js.map +1 -1
- package/dist/components/VIcon/VIcon.d.ts +7 -19
- package/dist/components/VIcon/VIcon.js.map +1 -1
- package/dist/components/VIcon/VIcon.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VIcon/iconProps.d.ts +5 -19
- package/dist/components/VIcon/iconProps.js.map +1 -1
- package/dist/components/VIcon/resolver.d.ts +15 -43
- package/dist/components/VIcon/resolver.js.map +1 -1
- package/dist/components/VIcon/types.d.ts +7 -34
- package/dist/components/VIconButton/VIconButton.d.ts +2 -3
- package/dist/components/VIconButton/VIconButton.js.map +1 -1
- package/dist/components/VIconButton/VIconButton.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VInput/VInput.d.ts +14 -29
- package/dist/components/VInput/VInput.js.map +1 -1
- package/dist/components/VInput/VInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VInput/VInputGroup.js.map +1 -1
- package/dist/components/VInput/VInputGroup.vue_vue_type_script_setup_true_lang.js +34 -50
- package/dist/components/VInput/VInputGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VInput/context.js.map +1 -1
- package/dist/components/VInputOTP/VInputOTP.d.ts +8 -16
- package/dist/components/VInputOTP/VInputOTP.js.map +1 -1
- package/dist/components/VInputOTP/VInputOTP.vue_vue_type_script_setup_true_lang.js +55 -63
- package/dist/components/VInputOTP/VInputOTP.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VMenu/VMenu.js.map +1 -1
- package/dist/components/VMenu/VMenu.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VMenu/VMenuGroup.d.ts +1 -6
- package/dist/components/VMenu/VMenuGroup.js.map +1 -1
- package/dist/components/VMenu/VMenuGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VMenu/VMenuItem.d.ts +0 -5
- package/dist/components/VMenu/VMenuItem.js.map +1 -1
- package/dist/components/VMenu/VMenuItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VMenu/VMenuPanel.d.ts +1 -6
- package/dist/components/VMenu/VMenuPanel.js.map +1 -1
- package/dist/components/VMenu/VMenuPanel.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/components/VMenu/VMenuPanel.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VMenu/VMenuSeparator.js.map +1 -1
- package/dist/components/VMenu/VMenuSeparator.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VMenu/context.d.ts +3 -11
- package/dist/components/VMenu/context.js.map +1 -1
- package/dist/components/VPagination/VPagination.d.ts +9 -20
- package/dist/components/VPagination/VPagination.js.map +1 -1
- package/dist/components/VPagination/VPagination.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VPopover/VPopover.d.ts +7 -18
- package/dist/components/VPopover/VPopover.js.map +1 -1
- package/dist/components/VPopover/VPopover.vue_vue_type_script_setup_true_lang.js +23 -27
- package/dist/components/VPopover/VPopover.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VProgressCircular/VProgressCircular.d.ts +2 -6
- package/dist/components/VProgressCircular/VProgressCircular.js.map +1 -1
- package/dist/components/VProgressCircular/VProgressCircular.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VProgressLinear/VProgressLinear.d.ts +4 -15
- package/dist/components/VProgressLinear/VProgressLinear.js.map +1 -1
- package/dist/components/VProgressLinear/VProgressLinear.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VRadio/VRadio.d.ts +1 -5
- package/dist/components/VRadio/VRadio.js.map +1 -1
- package/dist/components/VRadio/VRadio.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSeparator/VSeparator.d.ts +2 -15
- package/dist/components/VSeparator/VSeparator.js.map +1 -1
- package/dist/components/VSeparator/VSeparator.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSideNavigation/VSideNavigation.d.ts +1 -1
- package/dist/components/VSideNavigation/VSideNavigation.js.map +1 -1
- package/dist/components/VSideNavigation/VSideNavigation.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSideNavigation/VSideNavigationGroup.d.ts +1 -5
- package/dist/components/VSideNavigation/VSideNavigationGroup.js.map +1 -1
- package/dist/components/VSideNavigation/VSideNavigationGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSideNavigation/VSideNavigationItem.d.ts +1 -5
- package/dist/components/VSideNavigation/VSideNavigationItem.js.map +1 -1
- package/dist/components/VSideNavigation/VSideNavigationItem.vue_vue_type_script_setup_true_lang.js +80 -75
- package/dist/components/VSideNavigation/VSideNavigationItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSideNavigation/VSideNavigationSeparator.js.map +1 -1
- package/dist/components/VSideNavigation/context.js.map +1 -1
- package/dist/components/VSkeletonLoader/VSkeletonLoader.d.ts +6 -19
- package/dist/components/VSkeletonLoader/VSkeletonLoader.js.map +1 -1
- package/dist/components/VSkeletonLoader/VSkeletonLoader.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSlider/VSlider.d.ts +5 -13
- package/dist/components/VSlider/VSlider.js.map +1 -1
- package/dist/components/VSlider/VSlider.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSnackbar/VSnackbar.js.map +1 -1
- package/dist/components/VSnackbar/VSnackbar.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSnackbar/state.d.ts +4 -17
- package/dist/components/VSnackbar/state.js.map +1 -1
- package/dist/components/VSpinner/VSpinner.d.ts +3 -5
- package/dist/components/VSpinner/VSpinner.js.map +1 -1
- package/dist/components/VSpinner/VSpinner.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSwitch/VSwitch.js.map +1 -1
- package/dist/components/VSwitch/VSwitch.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTabs/VTab.d.ts +1 -5
- package/dist/components/VTabs/VTab.js.map +1 -1
- package/dist/components/VTabs/VTab.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTabs/VTabPanel.js.map +1 -1
- package/dist/components/VTabs/VTabPanel.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTabs/VTabs.d.ts +5 -17
- package/dist/components/VTabs/VTabs.js.map +1 -1
- package/dist/components/VTabs/VTabs.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTabs/context.js.map +1 -1
- package/dist/components/VTextarea/VTextarea.d.ts +13 -20
- package/dist/components/VTextarea/VTextarea.js.map +1 -1
- package/dist/components/VTextarea/VTextarea.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTimeInput/VTimeInput.d.ts +8 -21
- package/dist/components/VTimeInput/VTimeInput.js.map +1 -1
- package/dist/components/VTimeInput/VTimeInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTimeInput/search.d.ts +3 -34
- package/dist/components/VTimeInput/search.js.map +1 -1
- package/dist/components/VTimePicker/VTimePicker.d.ts +9 -31
- package/dist/components/VTimePicker/VTimePicker.js.map +1 -1
- package/dist/components/VTimePicker/VTimePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTimePicker/limits.d.ts +18 -45
- package/dist/components/VTimePicker/limits.js.map +1 -1
- package/dist/components/VToast/VToast.js.map +1 -1
- package/dist/components/VToast/VToast.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VToast/VToaster.js.map +1 -1
- package/dist/components/VToast/VToaster.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VToast/state.d.ts +2 -7
- package/dist/components/VToast/state.js.map +1 -1
- package/dist/components/VToggle/VToggle.d.ts +5 -9
- package/dist/components/VToggle/VToggle.js.map +1 -1
- package/dist/components/VToggle/VToggle.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VToggle/VToggleItem.d.ts +1 -9
- package/dist/components/VToggle/VToggleItem.js.map +1 -1
- package/dist/components/VToggle/VToggleItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VToggle/context.js.map +1 -1
- package/dist/components/VTooltip/VTooltip.d.ts +3 -16
- package/dist/components/VTooltip/VTooltip.js.map +1 -1
- package/dist/components/VTooltip/VTooltip.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/components/VTooltip/VTooltip.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VTypography/VTypography.d.ts +4 -8
- package/dist/components/VTypography/VTypography.js.map +1 -1
- package/dist/components/VTypography/VTypography.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/composables/useAriaLabel.js.map +1 -1
- package/dist/composables/useClearable.d.ts +2 -8
- package/dist/composables/useClearable.js.map +1 -1
- package/dist/composables/useControlShape.js.map +1 -1
- package/dist/composables/useDetailsOpen.d.ts +2 -10
- package/dist/composables/useDetailsOpen.js.map +1 -1
- package/dist/composables/useFieldIds.js.map +1 -1
- package/dist/composables/useFieldPanel.d.ts +4 -17
- package/dist/composables/useFieldPanel.js.map +1 -1
- package/dist/composables/useFileDrop.js.map +1 -1
- package/dist/composables/useFileField.d.ts +1 -5
- package/dist/composables/useFileField.js.map +1 -1
- package/dist/composables/useFocusoutDismiss.js.map +1 -1
- package/dist/composables/useIconClickHandlers.d.ts +4 -19
- package/dist/composables/useIconClickHandlers.js.map +1 -1
- package/dist/composables/useInertLink.d.ts +1 -1
- package/dist/composables/useInertLink.js.map +1 -1
- package/dist/composables/useLiveAnnouncer.js.map +1 -1
- package/dist/composables/useMaskedField.d.ts +12 -27
- package/dist/composables/useMaskedField.js.map +1 -1
- package/dist/composables/usePopover.d.ts +3 -11
- package/dist/composables/usePopover.js.map +1 -1
- package/dist/composables/useProgressValue.js.map +1 -1
- package/dist/composables/useRootAttrs.d.ts +0 -9
- package/dist/composables/useRootAttrs.js +11 -4
- package/dist/composables/useRootAttrs.js.map +1 -1
- package/dist/composables/useSlotNodes.d.ts +3 -14
- package/dist/composables/useSlotNodes.js.map +1 -1
- package/dist/composables/useTextLimit.d.ts +4 -16
- package/dist/composables/useTextLimit.js.map +1 -1
- package/dist/composables/useTimer.d.ts +2 -15
- package/dist/composables/useTimer.js.map +1 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/fr.js.map +1 -1
- package/dist/i18n/state.d.ts +9 -38
- package/dist/i18n/state.js.map +1 -1
- package/dist/i18n/types.d.ts +19 -81
- package/dist/icons.d.ts +2 -12
- package/dist/tokens/css.d.ts +3 -16
- package/dist/tokens/css.js.map +1 -1
- package/dist/tokens/index.js.map +1 -1
- package/dist/tokens/primitives.js.map +1 -1
- package/dist/tokens/semantic.d.ts +1 -23
- package/dist/tokens/semantic.js.map +1 -1
- package/dist/tokens/themes/dark.js.map +1 -1
- package/dist/tokens/types.d.ts +2 -12
- package/dist/tokens/types.js.map +1 -1
- package/dist/types.d.ts +3 -21
- package/dist/utils/array.d.ts +2 -4
- package/dist/utils/array.js.map +1 -1
- package/dist/utils/arrowNav.d.ts +2 -26
- package/dist/utils/arrowNav.js.map +1 -1
- package/dist/utils/chip.d.ts +1 -8
- package/dist/utils/chip.js.map +1 -1
- package/dist/utils/clock.d.ts +16 -39
- package/dist/utils/clock.js.map +1 -1
- package/dist/utils/css.d.ts +5 -16
- package/dist/utils/css.js.map +1 -1
- package/dist/utils/date.d.ts +17 -60
- package/dist/utils/date.js.map +1 -1
- package/dist/utils/direction.d.ts +2 -9
- package/dist/utils/direction.js.map +1 -1
- package/dist/utils/env.d.ts +2 -15
- package/dist/utils/env.js.map +1 -1
- package/dist/utils/file.d.ts +8 -46
- package/dist/utils/file.js.map +1 -1
- package/dist/utils/focus.d.ts +1 -14
- package/dist/utils/focus.js +1 -6
- package/dist/utils/focus.js.map +1 -1
- package/dist/utils/hostWarns.d.ts +3 -8
- package/dist/utils/hostWarns.js.map +1 -1
- package/dist/utils/ids.d.ts +2 -7
- package/dist/utils/ids.js.map +1 -1
- package/dist/utils/matcher.d.ts +1 -8
- package/dist/utils/matcher.js.map +1 -1
- package/dist/utils/memo.d.ts +1 -11
- package/dist/utils/memo.js.map +1 -1
- package/dist/utils/number.d.ts +1 -7
- package/dist/utils/number.js.map +1 -1
- package/dist/utils/text.d.ts +4 -23
- package/dist/utils/text.js.map +1 -1
- package/dist/utils/time.d.ts +4 -12
- package/dist/utils/time.js.map +1 -1
- package/dist/utils/vnode.d.ts +0 -10
- package/dist/utils/vnode.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VToaster.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../src/components/VToast/VToaster.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @a11y @core\n/**\n * Where notifications appear: mounted ONCE at the root, after which every `toast()` call\n * shows up here.\n *\n * One container per placement rather than one popover per notification, each stacking its\n * own with CSS. These boxes are drawn above the page at physical coordinates, anchored to\n * nothing, so individually they would all land on the same spot and keeping them apart would\n * mean measuring and offsetting each in code. The six exist at all times; empty, they cost\n * nothing and are not displayed.\n *\n * The JS covers three things the platform does not: keeping the queue and the containers in\n * step, the popover being imperative; the per-notification dismissal timers; and holding\n * them while the pointer rests on a stack OR the keyboard is inside it, so something that\n * disappears on a clock can be read and closed (WCAG 2.2.1).\n */\n\nimport { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'\n\nimport { usePopover } from '../../composables/usePopover'\nimport VToast from './VToast.vue'\nimport { dismissToast, toasts, type ToastItem, type ToastPlacement } from './state'\n\nimport { useAriaLabel } from '../../composables/useAriaLabel'\nimport { useLiveAnnouncer } from '../../composables/useLiveAnnouncer'\nimport { useMessages } from '../../i18n/state'\n\ninterface ToasterProps {\n /** Which corner notifications appear in, unless one of them asks for another. */\n placement?: ToastPlacement\n /**\n * How long a notification stays, in milliseconds, unless it asks for something else.\n * A notification given 0 stays until it is dismissed.\n */\n duration?: number\n /** What the close cross does, in words. It falls back to the design system dictionary. */\n closeLabel?: string\n /**\n * What screen readers announce for the notification areas themselves, which are\n * landmarks of the page. It falls back to the design system dictionary.\n */\n label?: string\n}\n\nconst props = withDefaults(defineProps<ToasterProps>(), {\n placement: 'bottom-right',\n duration: 5000,\n closeLabel: undefined,\n label: undefined,\n})\n\nconst m = useMessages()\nconst ariaLabel = useAriaLabel(() => props.label ?? m.value.toaster.label)\nconst resolvedCloseLabel = computed(() => props.closeLabel ?? m.value.common.close)\n\nconst PLACEMENTS: ToastPlacement[] = [\n 'top-left',\n 'top-center',\n 'top-right',\n 'bottom-left',\n 'bottom-center',\n 'bottom-right',\n]\n\n/**\n * The queue sorted into the corner each notification actually belongs to: the one it\n * asked for, or failing that the one set on this component.\n */\nconst groups = computed(() => {\n const map = new Map<ToastPlacement, ToastItem[]>()\n for (const item of toasts) {\n const placement = effectivePlacement(item)\n const list = map.get(placement)\n if (list) list.push(item)\n else map.set(placement, [item])\n }\n return map\n})\n\n/* The six containers, collected by the `v-for` itself. A plain template ref rather than a\n function one: a function written inline in the template is a new function on every\n render, which Vue answers by calling the old one with null and the new one with the\n element — twelve calls each time a notification comes or goes. */\nconst stackEls = ref<HTMLElement[]>([])\n\n/*\n * One instance of the popover plumbing PER corner. Each carries its own element, its\n * own open state — fed by that element's events — and the guards that make opening and\n * closing safe to call twice: asking the browser to show an already-shown popover\n * throws.\n *\n * The element is looked up by its placement rather than by its index: Vue does not\n * promise that a `v-for` ref array follows the order of the list.\n *\n * The containers being permanent, this runs once and never again.\n */\nconst stacks = new Map(\n PLACEMENTS.map((placement) => {\n const el = computed(\n () => stackEls.value.find((stack) => stack.dataset.placement === placement) ?? null,\n )\n return [placement, { el, ...usePopover(el) }] as const\n }),\n)\n\nfunction syncStack(placement: ToastPlacement, event: Event) {\n stacks.get(placement)?.syncShown(event)\n}\n\n/* The running countdowns, one per notification. */\nconst timers = new Map<number, ReturnType<typeof setTimeout>>()\n/* The corners holding their countdowns, one set per reason. Two sets rather than one\n because the reasons overlap and end independently: a reader tabs to a close cross, then\n moves the pointer over the stack and away, and the corner must stay held while the cross\n still has the focus — VSnackbar's two flags, per corner. */\nconst hovered = new Set<ToastPlacement>()\nconst focused = new Set<ToastPlacement>()\n\nfunction effectivePlacement(item: ToastItem): ToastPlacement {\n return item.placement ?? props.placement\n}\n\nconst isHeld = (placement: ToastPlacement) => hovered.has(placement) || focused.has(placement)\n\nfunction stopTimer(id: number) {\n const timer = timers.get(id)\n if (timer === undefined) return\n clearTimeout(timer)\n timers.delete(id)\n}\n\nfunction startTimer(item: ToastItem) {\n const duration = item.duration ?? props.duration\n if (duration <= 0 || timers.has(item.id)) return\n timers.set(\n item.id,\n setTimeout(() => {\n timers.delete(item.id)\n dismissToast(item.id)\n }, duration),\n )\n}\n\n// @core\n/**\n * Brings the page into line with the queue: it throws away the countdowns of the\n * notifications that have gone, gives every other one a countdown exactly when its corner\n * is not held, and then shows the corners holding something while hiding the empty ones.\n *\n * TRAP — the countdown follows the corner a notification is in NOW, re-derived on every\n * pass rather than remembered per notification. The default corner is a prop, so a\n * notification can change corner while the pointer rests on the old one: a memory of\n * \"already armed\" then outlived the countdown the hold had cancelled, the release looked\n * in the old corner, and the notification never got a countdown again.\n *\n * It runs once on mount — which is what makes a notification raised before this\n * component existed appear all the same — and after that on every change to the queue.\n */\nfunction sync() {\n const alive = new Set(toasts.map((item) => item.id))\n for (const id of timers.keys()) if (!alive.has(id)) stopTimer(id)\n for (const id of announced) if (!alive.has(id)) announced.delete(id)\n for (const item of toasts) {\n if (announced.has(item.id)) continue\n announced.add(item.id)\n // A failure or a warning interrupts what is being read; anything else waits.\n announce(\n [item.title, item.message].filter(Boolean).join('. '),\n item.tone === 'danger' || item.tone === 'warning',\n )\n }\n\n for (const placement of PLACEMENTS) {\n const stackEl = stacks.get(placement)?.el.value\n // TRAP — a hold is dropped here as soon as it can no longer be true, rather than\n // trusted to its closing event. A close cross leaves the page while it has the focus,\n // and an engine that sends no `focusout` for a removed element would keep the corner\n // held for good; an emptied stack is hidden under the pointer, with no `pointerleave`.\n if (!stackEl?.contains(document.activeElement)) focused.delete(placement)\n if ((groups.value.get(placement)?.length ?? 0) === 0) hovered.delete(placement)\n }\n\n for (const item of toasts) {\n if (isHeld(effectivePlacement(item))) stopTimer(item.id)\n else startTimer(item)\n }\n\n // Showing and hiding are safe to call on a container already in that state, the\n // guards living in the popover plumbing — so there is no need to remember which\n // corners are currently open.\n for (const placement of PLACEMENTS) {\n const stack = stacks.get(placement)\n if (!stack?.el.value) continue\n if ((groups.value.get(placement)?.length ?? 0) > 0) stack.show()\n else stack.hide()\n }\n}\n\n/*\n * Watching the grouped queue covers a change to the notifications AND a change to the\n * default corner. The `post` timing is load-bearing: the notification must already be\n * in the page before its container is told to show itself.\n */\nwatch(groups, sync, { flush: 'post' })\n// @ssr\n// A watcher does not run during the server render, so a notification raised\n// before this component mounted would never be picked up. Running the same\n// synchronization on mount is what brings it in.\nonMounted(sync)\n\n// @a11y\n// WCAG 2.2.1: something that disappears on a clock has to be holdable, or a\n// slow reader simply never finishes it.\n/*\n * Resting the pointer on a corner suspends its countdowns, and so does moving the keyboard\n * into it: each notification carries a close cross, a real button a reader tabs to, which\n * would otherwise vanish from under the focus ring. The same verbs and the same two\n * reasons as VSnackbar.\n *\n * Leaving restarts them from the FULL duration rather than from what was left: simpler,\n * and more generous to the reader who has just interrupted themselves.\n */\nfunction hold(placement: ToastPlacement, which: 'pointer' | 'focus') {\n const reason = which === 'pointer' ? hovered : focused\n reason.add(placement)\n for (const item of groups.value.get(placement) ?? []) stopTimer(item.id)\n}\n\nfunction release(placement: ToastPlacement, which: 'pointer' | 'focus') {\n const reason = which === 'pointer' ? hovered : focused\n reason.delete(placement)\n if (isHeld(placement)) return\n for (const item of groups.value.get(placement) ?? []) startTimer(item)\n}\n\n// @a11y\n// Every notification is said through two live regions rendered once, outside the stacks\n// (composables/useLiveAnnouncer): a card is created WITH its message, and a live region\n// inserted along with its text is not reliably announced.\nconst { polite, assertive, announce } = useLiveAnnouncer()\nconst announced = new Set<number>()\n\n// @a11y\n/*\n * A notification closed from its own cross takes the focused button with it, which would\n * drop the focus on `<body>`: it goes to the next cross of the same corner, or back to\n * where it was before the reader entered the corner. `cameFrom` is that element, taken\n * from the `focusin` that entered it.\n */\nconst cameFrom = new Map<ToastPlacement, HTMLElement>()\n\nfunction onStackFocusIn(placement: ToastPlacement, event: FocusEvent) {\n const from = event.relatedTarget as HTMLElement | null\n const stackEl = stacks.get(placement)?.el.value\n if (from && !stackEl?.contains(from)) cameFrom.set(placement, from)\n hold(placement, 'focus')\n}\n\nfunction closeFromCross(id: number) {\n const item = toasts.find((entry) => entry.id === id)\n const placement = item ? effectivePlacement(item) : undefined\n const stackEl = placement ? stacks.get(placement)?.el.value : null\n const hadFocus = !!stackEl?.contains(document.activeElement)\n dismissToast(id)\n if (!hadFocus || !placement) return\n void nextTick(() => {\n const next = stackEl?.querySelector<HTMLElement>('.v-toast-close')\n const back = cameFrom.get(placement)\n if (next) next.focus()\n else if (back?.isConnected) back.focus()\n })\n}\n\n/*\n * Two corners open at once are two landmarks, and two landmarks under one name are two\n * areas a screen reader cannot tell apart (axe `landmark-unique`). The name is then\n * numbered, in the order the corners are listed; alone on the page a corner keeps it bare.\n */\nconst openPlacements = computed(() =>\n PLACEMENTS.filter((placement) => (groups.value.get(placement)?.length ?? 0) > 0),\n)\nfunction stackLabel(placement: ToastPlacement) {\n const open = openPlacements.value\n if (open.length < 2) return ariaLabel.value\n return `${ariaLabel.value} (${open.indexOf(placement) + 1})`\n}\n\n/* The queue lives outside this component and survives it being unmounted and mounted\n again; only the countdowns are cleared here, and they are started afresh next time. */\nonBeforeUnmount(() => {\n for (const timer of timers.values()) clearTimeout(timer)\n timers.clear()\n})\n</script>\n\n<template>\n <div\n v-for=\"p in PLACEMENTS\"\n :key=\"p\"\n ref=\"stackEls\"\n class=\"v-overlay v-toast-stack\"\n popover=\"manual\"\n :data-placement=\"p\"\n role=\"region\"\n :aria-label=\"stackLabel(p)\"\n @pointerenter=\"hold(p, 'pointer')\"\n @pointerleave=\"release(p, 'pointer')\"\n @focusin=\"onStackFocusIn(p, $event)\"\n @focusout=\"release(p, 'focus')\"\n @beforetoggle=\"syncStack(p, $event)\"\n @toggle=\"syncStack(p, $event)\"\n >\n <VToast\n v-for=\"item in groups.get(p) ?? []\"\n :key=\"item.id\"\n :item=\"item\"\n :close-label=\"resolvedCloseLabel\"\n @close=\"closeFromCross\"\n />\n </div>\n <span class=\"v-visually-hidden\" role=\"status\">{{ polite }}</span>\n <span class=\"v-visually-hidden\" role=\"alert\">{{ assertive }}</span>\n</template>\n\n<style>\n@layer vectis.components {\n /*\n * The container. Its rules are an ALIGNED COPY of VSnackbar's host: the two containers are the\n * same object, a popover at a physical corner that fades when it empties, and the copy is\n * kept line for line, the one difference being the top edge, which a confirmation never takes.\n * They are not factored into styles/banner.css because that sheet is paid for by every\n * consumer, and these rules cost more than the size gate allows; change one, change the other.\n *\n * The fixed positioning and the guard hiding a closed container come from the shared\n * `.v-overlay` class, set on this same element. What is undone here is the browser's own\n * popover decoration: its border, its padding, its opaque background.\n */\n .v-toast-stack {\n margin: 0;\n border: none;\n padding: 0;\n background: transparent;\n overflow: visible;\n width: fit-content;\n }\n\n /*\n * PHYSICAL coordinates rather than logical ones: a message appears at a place on the\n * screen, and that place does not flip with the reading direction — the operating\n * system's own notifications behave the same way.\n *\n * `--banner-enter-y` is the direction each card slides in from, read by `.v-banner` in\n * styles/banner.css: the container is the only thing that knows which edge of the screen\n * it sits on. That sheet reads it with a `, 0` fallback, so dropping the declaration costs\n * the slide and nothing else, and nothing reports it.\n */\n .v-toast-stack[data-placement^='top-'] {\n top: var(--vectis-space-4);\n --banner-enter-y: calc(-1 * var(--vectis-space-4));\n }\n\n .v-toast-stack[data-placement^='bottom-'] {\n bottom: var(--vectis-space-4);\n --banner-enter-y: var(--vectis-space-4);\n }\n\n .v-toast-stack[data-placement$='-left'] {\n left: var(--vectis-space-4);\n }\n\n .v-toast-stack[data-placement$='-right'] {\n right: var(--vectis-space-4);\n }\n\n .v-toast-stack[data-placement$='-center'] {\n left: 0;\n right: 0;\n margin-inline: auto;\n }\n\n /* The container fades in and out. Animating an element that is being added to or removed\n from the page needs the two `allow-discrete` declarations and the starting values\n below; a browser missing either simply shows and hides it at once. */\n .v-toast-stack {\n opacity: 1;\n transition:\n opacity var(--vectis-duration-base) var(--vectis-ease-default),\n overlay var(--vectis-duration-base) allow-discrete,\n display var(--vectis-duration-base) allow-discrete;\n }\n\n .v-toast-stack:not(:popover-open) {\n opacity: 0;\n }\n\n @starting-style {\n .v-toast-stack:popover-open {\n opacity: 0;\n }\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-toast-stack {\n transition: none;\n }\n }\n\n /* The stack's own layout, which the snackbar's single bar has no use for. The newest\n notification should sit nearest the screen edge; the queue only ever grows at the end,\n so a stack along the top is simply drawn in reverse. */\n .v-toast-stack {\n display: flex;\n flex-direction: column;\n gap: var(--vectis-space-3);\n }\n\n .v-toast-stack[data-placement^='top-'] {\n flex-direction: column-reverse;\n }\n}\n</style>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"VToaster.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../src/components/VToast/VToaster.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @a11y @core\n/**\n * Manual popovers host notification stacks. JavaScript maintains queues, announcements and\n * per-item timers paused during hover or keyboard focus.\n */\n\nimport { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'\n\nimport { usePopover } from '../../composables/usePopover'\nimport VToast from './VToast.vue'\nimport { dismissToast, toasts, type ToastItem, type ToastPlacement } from './state'\n\nimport { useAriaLabel } from '../../composables/useAriaLabel'\nimport { useLiveAnnouncer } from '../../composables/useLiveAnnouncer'\nimport { useMessages } from '../../i18n/state'\n\ninterface ToasterProps {\n /** Which corner notifications appear in, unless one of them asks for another. */\n placement?: ToastPlacement\n /**\n * How long a notification stays, in milliseconds, unless it asks for something else.\n * A notification given 0 stays until it is dismissed.\n */\n duration?: number\n /** What the close cross does, in words. It falls back to the design system dictionary. */\n closeLabel?: string\n /**\n * What screen readers announce for the notification areas themselves, which are\n * landmarks of the page. It falls back to the design system dictionary.\n */\n label?: string\n}\n\nconst props = withDefaults(defineProps<ToasterProps>(), {\n placement: 'bottom-right',\n duration: 5000,\n closeLabel: undefined,\n label: undefined,\n})\n\nconst m = useMessages()\nconst ariaLabel = useAriaLabel(() => props.label ?? m.value.toaster.label)\nconst resolvedCloseLabel = computed(() => props.closeLabel ?? m.value.common.close)\n\nconst PLACEMENTS: ToastPlacement[] = [\n 'top-left',\n 'top-center',\n 'top-right',\n 'bottom-left',\n 'bottom-center',\n 'bottom-right',\n]\n\n/**\n * The queue sorted into the corner each notification actually belongs to: the one it\n * asked for, or failing that the one set on this component.\n */\nconst groups = computed(() => {\n const map = new Map<ToastPlacement, ToastItem[]>()\n for (const item of toasts) {\n const placement = effectivePlacement(item)\n const list = map.get(placement)\n if (list) list.push(item)\n else map.set(placement, [item])\n }\n return map\n})\n\n/*\n * The six containers, collected by the `v-for` itself. A plain template ref rather than a\n * function one: a function written inline in the template is a new function on every render,\n * which Vue answers by calling the old one with null and the new one with the element; twelve\n * calls each time a notification comes or goes.\n */\nconst stackEls = ref<HTMLElement[]>([])\n\n/*\n * The element is looked up by its placement rather than by its index: Vue does not promise that\n * a `v-for` ref array follows the order of the list. The containers being permanent, this runs\n * once and never again.\n */\nconst stacks = new Map(\n PLACEMENTS.map((placement) => {\n const el = computed(\n () => stackEls.value.find((stack) => stack.dataset.placement === placement) ?? null,\n )\n return [placement, { el, ...usePopover(el) }] as const\n }),\n)\n\nfunction syncStack(placement: ToastPlacement, event: Event) {\n stacks.get(placement)?.syncShown(event)\n}\n\nconst timers = new Map<number, ReturnType<typeof setTimeout>>()\n/*\n * Two sets rather than one because the reasons overlap and end independently: a reader tabs to\n * a close cross, then moves the pointer over the stack and away, and the corner must stay held\n * while the cross still has the focus; VSnackbar's two flags, per corner.\n */\nconst hovered = new Set<ToastPlacement>()\nconst focused = new Set<ToastPlacement>()\n\nfunction effectivePlacement(item: ToastItem): ToastPlacement {\n return item.placement ?? props.placement\n}\n\nconst isHeld = (placement: ToastPlacement) => hovered.has(placement) || focused.has(placement)\n\nfunction stopTimer(id: number) {\n const timer = timers.get(id)\n if (timer === undefined) return\n clearTimeout(timer)\n timers.delete(id)\n}\n\nfunction startTimer(item: ToastItem) {\n const duration = item.duration ?? props.duration\n if (duration <= 0 || timers.has(item.id)) return\n timers.set(\n item.id,\n setTimeout(() => {\n timers.delete(item.id)\n dismissToast(item.id)\n }, duration),\n )\n}\n\n// @core\n/**\n * Recompute each toast's corner before arming timers: moving a held toast must not leave it\n * without a countdown after release.\n */\nfunction sync() {\n const alive = new Set(toasts.map((item) => item.id))\n for (const id of timers.keys()) if (!alive.has(id)) stopTimer(id)\n for (const id of announced) if (!alive.has(id)) announced.delete(id)\n for (const item of toasts) {\n if (announced.has(item.id)) continue\n announced.add(item.id)\n announce(\n [item.title, item.message].filter(Boolean).join('. '),\n item.tone === 'danger' || item.tone === 'warning',\n )\n }\n\n for (const placement of PLACEMENTS) {\n const stackEl = stacks.get(placement)?.el.value\n // A close cross leaves the page while it has the focus, and an engine that sends no\n // `focusout` for a removed element would keep the corner held for good; an emptied stack is\n // hidden under the pointer, with no `pointerleave`.\n if (!stackEl?.contains(document.activeElement)) focused.delete(placement)\n if ((groups.value.get(placement)?.length ?? 0) === 0) hovered.delete(placement)\n }\n\n for (const item of toasts) {\n if (isHeld(effectivePlacement(item))) stopTimer(item.id)\n else startTimer(item)\n }\n\n // Showing and hiding are safe to call on a container already in that state, the guards living\n // in the popover plumbing; so there is no need to remember which corners are currently open.\n for (const placement of PLACEMENTS) {\n const stack = stacks.get(placement)\n if (!stack?.el.value) continue\n if ((groups.value.get(placement)?.length ?? 0) > 0) stack.show()\n else stack.hide()\n }\n}\n\n/*\n * The `post` timing is load-bearing: the notification must already be in the page before its\n * container is told to show itself.\n */\nwatch(groups, sync, { flush: 'post' })\n// @ssr\n// A watcher does not run during the server render, so a notification raised\n// before this component mounted would never be picked up. Running the same\n// synchronization on mount is what brings it in.\nonMounted(sync)\n\n// @a11y\n// WCAG 2.2.1: something that disappears on a clock has to be holdable, or a\n// slow reader simply never finishes it.\n/*\n * Resting the pointer on a corner suspends its countdowns, and so does moving the keyboard into\n * it: each notification carries a close cross, a real button a reader tabs to, which would\n * otherwise vanish from under the focus ring. The same verbs and the same two reasons as\n * VSnackbar.\n */\nfunction hold(placement: ToastPlacement, which: 'pointer' | 'focus') {\n const reason = which === 'pointer' ? hovered : focused\n reason.add(placement)\n for (const item of groups.value.get(placement) ?? []) stopTimer(item.id)\n}\n\nfunction release(placement: ToastPlacement, which: 'pointer' | 'focus') {\n const reason = which === 'pointer' ? hovered : focused\n reason.delete(placement)\n if (isHeld(placement)) return\n for (const item of groups.value.get(placement) ?? []) startTimer(item)\n}\n\n// @a11y\n// Every notification is said through two live regions rendered once, outside the stacks\n// (composables/useLiveAnnouncer): a card is created WITH its message, and a live region\n// inserted along with its text is not reliably announced.\nconst { polite, assertive, announce } = useLiveAnnouncer()\nconst announced = new Set<number>()\n\n// @a11y\n/*\n * A notification closed from its own cross takes the focused button with it, which would\n * drop the focus on `<body>`: it goes to the next cross of the same corner, or back to\n * where it was before the reader entered the corner. `cameFrom` is that element, taken\n * from the `focusin` that entered it.\n */\nconst cameFrom = new Map<ToastPlacement, HTMLElement>()\n\nfunction onStackFocusIn(placement: ToastPlacement, event: FocusEvent) {\n const from = event.relatedTarget as HTMLElement | null\n const stackEl = stacks.get(placement)?.el.value\n if (from && !stackEl?.contains(from)) cameFrom.set(placement, from)\n hold(placement, 'focus')\n}\n\nfunction closeFromCross(id: number) {\n const item = toasts.find((entry) => entry.id === id)\n const placement = item ? effectivePlacement(item) : undefined\n const stackEl = placement ? stacks.get(placement)?.el.value : null\n const hadFocus = !!stackEl?.contains(document.activeElement)\n dismissToast(id)\n if (!hadFocus || !placement) return\n void nextTick(() => {\n const next = stackEl?.querySelector<HTMLElement>('.v-toast-close')\n const back = cameFrom.get(placement)\n if (next) next.focus()\n else if (back?.isConnected) back.focus()\n })\n}\n\n/*\n * Two corners open at once are two landmarks, and two landmarks under one name are two\n * areas a screen reader cannot tell apart (axe `landmark-unique`). The name is then\n * numbered, in the order the corners are listed; alone on the page a corner keeps it bare.\n */\nconst openPlacements = computed(() =>\n PLACEMENTS.filter((placement) => (groups.value.get(placement)?.length ?? 0) > 0),\n)\nfunction stackLabel(placement: ToastPlacement) {\n const open = openPlacements.value\n if (open.length < 2) return ariaLabel.value\n return `${ariaLabel.value} (${open.indexOf(placement) + 1})`\n}\n\n/* The queue lives outside this component and survives it being unmounted and mounted\n again; only the countdowns are cleared here, and they are started afresh next time. */\nonBeforeUnmount(() => {\n for (const timer of timers.values()) clearTimeout(timer)\n timers.clear()\n})\n</script>\n\n<template>\n <div\n v-for=\"p in PLACEMENTS\"\n :key=\"p\"\n ref=\"stackEls\"\n class=\"v-overlay v-toast-stack\"\n popover=\"manual\"\n :data-placement=\"p\"\n role=\"region\"\n :aria-label=\"stackLabel(p)\"\n @pointerenter=\"hold(p, 'pointer')\"\n @pointerleave=\"release(p, 'pointer')\"\n @focusin=\"onStackFocusIn(p, $event)\"\n @focusout=\"release(p, 'focus')\"\n @beforetoggle=\"syncStack(p, $event)\"\n @toggle=\"syncStack(p, $event)\"\n >\n <VToast\n v-for=\"item in groups.get(p) ?? []\"\n :key=\"item.id\"\n :item=\"item\"\n :close-label=\"resolvedCloseLabel\"\n @close=\"closeFromCross\"\n />\n </div>\n <span class=\"v-visually-hidden\" role=\"status\">{{ polite }}</span>\n <span class=\"v-visually-hidden\" role=\"alert\">{{ assertive }}</span>\n</template>\n\n<style>\n@layer vectis.components {\n /*\n * Keep host rules aligned with VSnackbar; toaster also supports top placements. Sharing this\n * layout in core CSS would increase every consumer's size cost.\n */\n .v-toast-stack {\n margin: 0;\n border: none;\n padding: 0;\n background: transparent;\n overflow: visible;\n width: fit-content;\n }\n\n /*\n * Keep screen placement physical in RTL. The host supplies each card's entry direction from\n * its screen edge.\n */\n .v-toast-stack[data-placement^='top-'] {\n top: var(--vectis-space-4);\n --banner-enter-y: calc(-1 * var(--vectis-space-4));\n }\n\n .v-toast-stack[data-placement^='bottom-'] {\n bottom: var(--vectis-space-4);\n --banner-enter-y: var(--vectis-space-4);\n }\n\n .v-toast-stack[data-placement$='-left'] {\n left: var(--vectis-space-4);\n }\n\n .v-toast-stack[data-placement$='-right'] {\n right: var(--vectis-space-4);\n }\n\n .v-toast-stack[data-placement$='-center'] {\n left: 0;\n right: 0;\n margin-inline: auto;\n }\n\n /* The container fades in and out. Animating an element that is being added to or removed\n from the page needs the two `allow-discrete` declarations and the starting values\n below; a browser missing either simply shows and hides it at once. */\n .v-toast-stack {\n opacity: 1;\n transition:\n opacity var(--vectis-duration-base) var(--vectis-ease-default),\n overlay var(--vectis-duration-base) allow-discrete,\n display var(--vectis-duration-base) allow-discrete;\n }\n\n .v-toast-stack:not(:popover-open) {\n opacity: 0;\n }\n\n @starting-style {\n .v-toast-stack:popover-open {\n opacity: 0;\n }\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-toast-stack {\n transition: none;\n }\n }\n\n /* The stack's own layout, which the snackbar's single bar has no use for. The newest\n notification should sit nearest the screen edge; the queue only ever grows at the end,\n so a stack along the top is simply drawn in reverse. */\n .v-toast-stack {\n display: flex;\n flex-direction: column;\n gap: var(--vectis-space-3);\n }\n\n .v-toast-stack[data-placement^='top-'] {\n flex-direction: column-reverse;\n }\n}\n</style>\n"],"mappings":"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"}
|
|
@@ -51,13 +51,8 @@ export interface ToastItem extends ToastOptions {
|
|
|
51
51
|
/** The queue itself. It is internal, read only by VToaster, and not public API. */
|
|
52
52
|
export declare const toasts: import('vue').Reactive<ToastItem[]>;
|
|
53
53
|
/**
|
|
54
|
-
* Raises a notification, and hands back an id that `dismissToast` can use to take it
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* The two defaults that belong to the VToaster, where notifications appear and how
|
|
58
|
-
* long they stay, are deliberately NOT resolved here: they are read when the
|
|
59
|
-
* notification is rendered, so the toaster remains the single source of truth for its
|
|
60
|
-
* own settings.
|
|
54
|
+
* Raises a notification, and hands back an id that `dismissToast` can use to take it away again
|
|
55
|
+
* before its time.
|
|
61
56
|
*/
|
|
62
57
|
export declare function toast(options: ToastOptions): number;
|
|
63
58
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state.js","names":[],"sources":["../../../src/components/VToast/state.ts"],"sourcesContent":["// @ssr @core\n/**\n *
|
|
1
|
+
{"version":3,"file":"state.js","names":[],"sources":["../../../src/components/VToast/state.ts"],"sourcesContent":["// @ssr @core\n/**\n * Module state lets toast be called outside components. Call it only in browser handlers, never\n * during SSR, because the queue is shared across server requests.\n */\n\nimport { reactive } from 'vue'\n\nimport type { IconSource } from '../VIcon/types'\nimport { isDev } from '../../utils/env'\n\n/** What a notification reports: nothing in particular, information, a success, a failure or a warning. */\nexport type ToastTone = 'neutral' | 'accent' | 'danger' | 'success' | 'warning'\n\n/** How strongly a notification is painted: a tinted background with a border, or the full colour. */\nexport type ToastVariant = 'soft' | 'solid'\n\n/** The corner of the screen a notification appears in. */\nexport type ToastPlacement =\n 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'\n\n/** Everything one can say when raising a notification. */\nexport interface ToastOptions {\n /** What the notification says. */\n message: string\n /** A short heading above the message, when the message alone needs framing. */\n title?: string\n /** What the notification means, expressed as a colour, and which icon it takes by default. */\n tone?: ToastTone\n /** How strongly it is painted: a tinted background with a border, or the full colour. */\n variant?: ToastVariant\n /**\n * The icon to show. Left out, the tone brings its own; `false` removes it\n * altogether.\n */\n icon?: IconSource | false\n /**\n * How long it stays, in milliseconds. Setting it to 0 keeps it until it is\n * dismissed by hand, and leaving it out takes the duration set on the VToaster.\n */\n duration?: number\n /** Which corner it appears in. Left out, it takes the one set on the VToaster. */\n placement?: ToastPlacement\n /**\n * Takes the close cross away, leaving the notification to its duration or to\n * `dismissToast`. Keep the cross on a notification that stays until dismissed by hand.\n */\n hideClose?: boolean\n /**\n * How wide it is: a number of pixels or any CSS length. It is never allowed past the\n * width of the viewport, margins included.\n */\n width?: number | string\n}\n\n/**\n * A notification once it is in the queue, with the choices the caller left out filled\n * in and an id of its own. Internal: it is what VToaster renders.\n */\nexport interface ToastItem extends ToastOptions {\n id: number\n tone: ToastTone\n variant: ToastVariant\n hideClose: boolean\n}\n\n/** The queue itself. It is internal, read only by VToaster, and not public API. */\nexport const toasts = reactive<ToastItem[]>([])\n\nlet nextId = 0\n\n/**\n * Raises a notification, and hands back an id that `dismissToast` can use to take it away again\n * before its time.\n */\nexport function toast(options: ToastOptions): number {\n // @devwarn\n // The queue is module state, shared by every request a server renders: raised there, it\n // would reach another reader's page and differ from what the browser hydrates.\n if (isDev && typeof document === 'undefined')\n console.warn(\n '[toast] called during a server render: call it from a handler or a lifecycle hook that runs in the browser.',\n )\n const id = nextId++\n toasts.push({ tone: 'neutral', variant: 'soft', hideClose: false, ...options, id })\n return id\n}\n\n/**\n * Takes a notification away by its id, or clears every one of them when called with\n * no argument.\n */\nexport function dismissToast(id?: number): void {\n if (id === undefined) {\n toasts.length = 0\n return\n }\n const index = toasts.findIndex((item) => item.id === id)\n if (index !== -1) toasts.splice(index, 1)\n}\n"],"mappings":";;;AAmEA,IAAa,IAAS,EAAsB,CAAC,CAAC,GAE1C,IAAS;AAMb,SAAgB,EAAM,GAA+B;CAInD,AAAI,KAAS,OAAO,WAAa,OAC/B,QAAQ,KACN,6GACF;CACF,IAAM,IAAK;CAEX,OADA,EAAO,KAAK;EAAE,MAAM;EAAW,SAAS;EAAQ,WAAW;EAAO,GAAG;EAAS;CAAG,CAAC,GAC3E;AACT;AAMA,SAAgB,EAAa,GAAmB;CAC9C,IAAI,MAAO,KAAA,GAAW;EACpB,EAAO,SAAS;EAChB;CACF;CACA,IAAM,IAAQ,EAAO,WAAW,MAAS,EAAK,OAAO,CAAE;CACvD,AAAI,MAAU,MAAI,EAAO,OAAO,GAAO,CAAC;AAC1C"}
|
|
@@ -47,9 +47,8 @@ interface ToggleProps {
|
|
|
47
47
|
*/
|
|
48
48
|
fullWidth?: boolean;
|
|
49
49
|
/**
|
|
50
|
-
* How the UNSELECTED items are drawn.
|
|
51
|
-
*
|
|
52
|
-
* `variant` names the decoration of the frame instead.
|
|
50
|
+
* How the UNSELECTED items are drawn. It is named for the ITEMS because that is what it
|
|
51
|
+
* paints: on VTabs and VDataTable `variant` names the decoration of the frame instead.
|
|
53
52
|
*/
|
|
54
53
|
itemVariant?: ToggleItemVariant;
|
|
55
54
|
/**
|
|
@@ -85,12 +84,9 @@ type __VLS_Slots = {
|
|
|
85
84
|
};
|
|
86
85
|
type __VLS_ModelProps = {
|
|
87
86
|
/**
|
|
88
|
-
* What is selected, and its
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* The array is never mutated in place; each change is a new one, which is what wakes the
|
|
93
|
-
* consumer's binding. Re-clicking the selected item deselects it unless `mandatory` is set.
|
|
87
|
+
* What is selected, and its shape follows `multiple`: a single value (or `null`, which is where
|
|
88
|
+
* it starts) when one item may be chosen, and an array when several may. A null or scalar value
|
|
89
|
+
* passed in `multiple` mode is read as an empty selection.
|
|
94
90
|
*/
|
|
95
91
|
modelValue?: ToggleModelValue;
|
|
96
92
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VToggle.js","names":[],"sources":["../../../src/components/VToggle/VToggle.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @keyboard @core\n/**\n * A row of buttons where one — or several — stay pressed: choosing a text alignment,\n * a view, a set of filters. A single v-model holds what is chosen.\n *\n * Every item is a VButton, the selected one drawn in the group's tone and the others\n * quiet. The row itself is a VButtonGroup, which is where the drawing lives: joining the\n * items or leaving them apart, the lines between them, the shared size and the shared\n * elevation are that component's, and the props for them are handed straight over.\n * Nothing about hovering, focusing or disabling is written here either: it all comes\n * from VButton.\n *\n * The JavaScript covers the two things HTML has no answer for: what a click does to\n * the selection — toggling it off, or refusing to when the last choice may not be\n * given up — and moving between the items with the arrow keys.\n */\nimport { computed, provide, ref } from 'vue'\n\nimport VButtonGroup from '../VButton/VButtonGroup.vue'\nimport { toggleKey } from './context'\n\nimport { toggleValue } from '../../utils/array'\nimport { arrowNavigate, navigableItems } from '../../utils/arrowNav'\n\nimport type { ItemValue } from '../../types'\n\n/**\n * What identifies an item. It is the design system's own `ItemValue`, re-exported\n * under the family's name so a consumer typing a toggle need not reach for another module.\n */\nexport type ToggleValue = ItemValue\n/**\n * What the v-model holds, which follows `multiple`: one value or nothing when a single\n * choice is allowed, and a list of them when several are.\n */\nexport type ToggleModelValue = ToggleValue | ToggleValue[] | null\n/** How the items that are not selected are drawn. */\nexport type ToggleItemVariant = 'ghost' | 'outline'\n/** How a selected item is drawn. */\nexport type ToggleSelectedVariant = 'solid' | 'soft' | 'ghost'\n/** The colour of a selected item. */\nexport type ToggleTone = 'accent' | 'neutral' | 'danger'\n/** The height of the items, from the scale every control shares. */\nexport type ToggleSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'\n/** Whether the items run across the page or down it. */\nexport type ToggleOrientation = 'horizontal' | 'vertical'\n\ninterface ToggleProps {\n /** Allows several items to be chosen at once, which makes the v-model a list. */\n multiple?: boolean\n /**\n * Refuses to let the last chosen item be given up, so that something is always\n * selected once something has been. It is a guard and nothing more: it selects\n * nothing on its own at the start.\n */\n mandatory?: boolean\n /**\n * Leaves the items as separate buttons with a gap between them. Left out, they are\n * joined into one segmented control.\n */\n detached?: boolean\n /**\n * Takes the lines out from between the joined items, so the row reads as one frame\n * rather than as segments. It has no effect under `detached`, where the items are\n * separate buttons already.\n */\n seamless?: boolean\n /** Whether the items run across the page or down it. */\n orientation?: ToggleOrientation\n /**\n * Stretches the row across the whole inline size of its parent, every item taking an equal\n * share of it, on the terms of VButtonGroup's own `fullWidth`.\n */\n fullWidth?: boolean\n /**\n * How the UNSELECTED items are drawn. What the selected one takes is `selectedVariant`.\n * It is named for the ITEMS because that is what it paints: on VTabs and VDataTable\n * `variant` names the decoration of the frame instead.\n */\n itemVariant?: ToggleItemVariant\n /**\n * How the SELECTED item is drawn, in the group's tone: filled, tinted, or the colour\n * of its text alone.\n */\n selectedVariant?: ToggleSelectedVariant\n /** The colour a selected item takes. The others stay neutral. */\n tone?: ToggleTone\n /** The height of the items, from the scale shared by every control. */\n size?: ToggleSize\n /** Takes 4px off the height of every item. */\n compact?: boolean\n /** Raises the row off the page, on the terms of VButtonGroup's own `elevated`. */\n elevated?: boolean\n /** Makes the whole group unusable. */\n disabled?: boolean\n /**\n * Draws the selected item's icon in its filled form, a common way of reinforcing\n * that it is the one in effect.\n */\n selectedIconFilled?: boolean\n /**\n * What screen readers announce for the group, such as \"Text alignment\" or \"Filters\". It is\n * strongly recommended: no default could say what a group of buttons is for.\n */\n label?: string\n}\n\nconst props = withDefaults(defineProps<ToggleProps>(), {\n multiple: false,\n mandatory: false,\n detached: false,\n seamless: false,\n orientation: 'horizontal',\n fullWidth: false,\n itemVariant: 'ghost',\n selectedVariant: 'solid',\n tone: 'accent',\n size: 'md',\n compact: false,\n elevated: false,\n disabled: false,\n selectedIconFilled: false,\n label: undefined,\n})\n\ndefineSlots<{\n /** The items of the group. */\n default(): unknown\n}>()\n\n/**\n * What is selected, and its SHAPE follows `multiple`: a single value (or `null`, which is\n * where it starts) when one item may be chosen, and an array when several may. A null or\n * scalar value passed in `multiple` mode is read as an empty selection.\n *\n * The array is never mutated in place; each change is a new one, which is what wakes the\n * consumer's binding. Re-clicking the selected item deselects it unless `mandatory` is set.\n */\nconst model = defineModel<ToggleModelValue>({ default: null })\n\nfunction isSelected(value: ToggleValue): boolean {\n return props.multiple\n ? Array.isArray(model.value) && model.value.includes(value)\n : model.value === value\n}\n\n/*\n * What a click does to the selection.\n *\n * With several choices allowed, the list is NEVER changed in place: each write\n * produces a new one. That is what makes a consumer's own watchers and computed values\n * notice; mutating the existing list would leave some of them silent. A model still\n * holding a single value, or nothing, is treated as an empty list rather than\n * refused.\n *\n * There is no check for the disabled state here, and none is needed: a disabled button\n * emits no click at all.\n */\nfunction select(value: ToggleValue) {\n if (props.multiple) {\n const current = Array.isArray(model.value) ? model.value : []\n // `every` rather than a length of one: the removal takes out every copy of the value, so\n // a list holding it twice would otherwise be emptied past the guard.\n if (props.mandatory && current.length > 0 && current.every((v) => v === value)) return\n model.value = toggleValue(current, value)\n return\n }\n if (model.value === value) {\n if (!props.mandatory) model.value = null\n return\n }\n model.value = value\n}\n\nprovide(toggleKey, {\n isSelected,\n select,\n get itemVariant() {\n return props.itemVariant\n },\n get selectedVariant() {\n return props.selectedVariant\n },\n get tone() {\n return props.tone\n },\n get selectedIconFilled() {\n return props.selectedIconFilled\n },\n})\n\n// @keyboard @a11y\n/*\n * The arrow keys, through the shared implementation in `utils/arrowNav`.\n *\n * Every visible item stays a stop in the tab order here — the VPagination model. The\n * single-stop treatment is reserved for the patterns that require it, a row of tabs or\n * a radio group, and a group of buttons is neither. Disabled items are really disabled\n * buttons, which the selector leaves out.\n *\n * The container is read from the event rather than kept as a reference: the root is a\n * VButtonGroup, so a template ref would hand back its component instance rather than the\n * element the items are navigated inside.\n *\n * Only a key pressed ON an item is taken. An item may carry a companion whose content sits\n * inside the group (a VPopover's panel stays a DOM descendant of its trigger), and an arrow\n * typed in a field there must move the caret, not the focus.\n */\nfunction onKeydown(event: KeyboardEvent) {\n if (!(event.target as Element).matches('.v-toggle-item')) return\n const group = event.currentTarget as HTMLElement\n arrowNavigate(event, group, () => navigableItems(group, '.v-toggle-item:not(:disabled)'), {\n vertical: props.orientation === 'vertical',\n })\n}\n\nconst groupRef = ref<InstanceType<typeof VButtonGroup> | null>(null)\nconst groupEl = computed(() => groupRef.value?.el ?? null)\n\n// The root is a VButtonGroup and the items come from the consumer's slot, so a template ref\n// on the toggle reaches no button. `focus` goes to the item that stands for the current\n// choice, the one a keyboard user would expect to resume from.\ndefineExpose({\n /**\n * Moves the focus to the selected item (the first of them when several are), or to the\n * first item that can take it when nothing is selected.\n */\n focus: (options?: FocusOptions) => {\n const group = groupEl.value\n const enabled = '.v-toggle-item:not(:disabled, [aria-disabled=\"true\"])'\n const item =\n group?.querySelector<HTMLElement>(`${enabled}[aria-pressed=\"true\"]`) ??\n group?.querySelector<HTMLElement>(enabled)\n item?.focus(options)\n },\n /** The `role=\"group\"` row, which is also where the consumer's attributes land. */\n el: groupEl,\n})\n</script>\n\n<template>\n <!-- The row is a VButtonGroup, which merges the borders of the buttons among its\n children — which is why an item renders the button as its own root, and why what\n may stand between the two is the wrappers a VTooltip, a VPopover or a VBadge put\n there, and nothing else. It brings the role, its accessible name and the orientation\n attribute with it, and of the props handed to it here it forwards the size, the density,\n the elevation and the disabled state to the buttons itself.\n\n The three booleans go down as `|| undefined`, the VPagination idiom: `undefined` is\n what means \"no opinion\" to a group, where `false` is an order every item would obey,\n and a toggle left at its defaults has no opinion to give. The size is always one, the\n toggle having a default of its own.\n\n The name is resolved by the group, which sees a consumer's `aria-label` or\n `aria-labelledby` too: they fall through this component onto it.\n\n `variant` is deliberately NOT among them, and neither is the tone: the group wins\n over a button that names its own variant, which would erase the one the selected\n item carries. Both stay with the items, through the context.\n\n `data-item-variant` is a plain attribute, and it is what tells the sheet an outline row\n from a ghost one — something the items cannot say for themselves, since the\n selected one carries its own variant rather than the row's. -->\n <VButtonGroup\n ref=\"groupRef\"\n class=\"v-toggle\"\n :orientation=\"orientation\"\n :detached=\"detached\"\n :seamless=\"seamless\"\n :full-width=\"fullWidth\"\n :size=\"size\"\n :compact=\"compact || undefined\"\n :elevated=\"elevated || undefined\"\n :disabled=\"disabled || undefined\"\n :label=\"label\"\n :data-item-variant=\"itemVariant\"\n @keydown=\"onKeydown\"\n >\n <slot />\n </VButtonGroup>\n</template>\n\n<style>\n@layer vectis.components {\n /*\n * Both rules below are written for the two shapes an item can take, exactly as\n * VButtonGroup's own sheet is and for the same reason: a VTooltip, a VPopover or a\n * VBadge puts a wrapper between the row and the item, and a rule reaching for a direct\n * child alone would leave a gap in the frame around whichever item carries one. The\n * `:not(:where(.v-overlay))` guard weighs nothing and keeps a panel from passing for an\n * item; the pair is one specificity, so the arbitration with the group's seamless rules\n * holds on both shapes.\n *\n * The frame of an `outline` row, which only that row has: the colour its unselected\n * items already paint. They are drawn in the NEUTRAL tone, so this is what their\n * `--tone-border-soft` resolves to — naming that variable here instead would read the\n * SELECTED item's tone and tint one segment of the frame accent or red.\n *\n * It is a variable on the root rather than a value in the rule below because a\n * disabled segment greys, and VButton greys an outline border to a different token.\n * Nothing else declares it, so the two rules never arbitrate a border-color between\n * them.\n */\n .v-toggle[data-item-variant='outline'] {\n --toggle-frame: var(--vectis-color-border-strong);\n }\n\n .v-toggle[data-item-variant='outline'] > .v-toggle-item:is(:disabled, [aria-disabled='true']),\n .v-toggle[data-item-variant='outline']\n > :not(:where(.v-overlay, .v-button-group))\n .v-toggle-item:is(:disabled, [aria-disabled='true']):not(:where(.v-overlay *)) {\n --toggle-frame: var(--vectis-color-border);\n }\n\n /*\n * `soft` and `ghost` leave VButton's border transparent, which in an outline row\n * opens a gap in the frame for the whole width of the selected segment. Restoring it\n * on all four sides is what keeps the frame closed wherever the selection sits, in\n * either orientation and in RTL. It is not conditioned on `seamless`, and deliberately\n * so: the border is transparent whether the internal lines are drawn or not. Seamless,\n * the shared edges are cleared again by VButtonGroup's own (0,6,0) rules, which is why\n * they are written at that weight — a middle segment then keeps its top and bottom\n * alone.\n */\n .v-toggle[data-item-variant='outline']\n > .v-toggle-item[aria-pressed='true']:is([data-variant='soft'], [data-variant='ghost']),\n .v-toggle[data-item-variant='outline']\n > :not(:where(.v-overlay, .v-button-group))\n .v-toggle-item[aria-pressed='true']:is([data-variant='soft'], [data-variant='ghost']):not(\n :where(.v-overlay *)\n ) {\n border-color: var(--toggle-frame);\n }\n\n /*\n * Windows forced colors replace every background with the page's, so the pressed item would look\n * exactly like the others there: the fill is its only cue, the state being in an ARIA\n * attribute a sighted reader never sees. It takes the system's own selection pair\n * instead, and opts out of the forcing for that element alone so the pair is painted.\n *\n * TRAP — the class is repeated to reach (0,6,0). The variant, hover and active rules\n * reach (0,5,0), and with the forcing turned off any of them that still won would paint\n * its tone over the selection, with HighlightText on top of it.\n */\n @media (forced-colors: active) {\n .v-toggle-item.v-toggle-item.v-toggle-item.v-toggle-item[aria-pressed='true']:not(:disabled) {\n forced-color-adjust: none;\n background-color: Highlight;\n color: HighlightText;\n border-color: Highlight;\n outline-color: CanvasText;\n }\n }\n}\n</style>\n"],"mappings":""}
|
|
1
|
+
{"version":3,"file":"VToggle.js","names":[],"sources":["../../../src/components/VToggle/VToggle.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @keyboard @core\n/**\n * Reuse segmented native buttons with aria-pressed. JavaScript synchronizes selection and\n * provides group keyboard navigation.\n */\nimport { computed, provide, ref } from 'vue'\n\nimport VButtonGroup from '../VButton/VButtonGroup.vue'\nimport { toggleKey } from './context'\n\nimport { toggleValue } from '../../utils/array'\nimport { arrowNavigate, navigableItems } from '../../utils/arrowNav'\n\nimport type { ItemValue } from '../../types'\n\n/**\n * What identifies an item. It is the design system's own `ItemValue`, re-exported\n * under the family's name so a consumer typing a toggle need not reach for another module.\n */\nexport type ToggleValue = ItemValue\n/**\n * What the v-model holds, which follows `multiple`: one value or nothing when a single\n * choice is allowed, and a list of them when several are.\n */\nexport type ToggleModelValue = ToggleValue | ToggleValue[] | null\n/** How the items that are not selected are drawn. */\nexport type ToggleItemVariant = 'ghost' | 'outline'\n/** How a selected item is drawn. */\nexport type ToggleSelectedVariant = 'solid' | 'soft' | 'ghost'\n/** The colour of a selected item. */\nexport type ToggleTone = 'accent' | 'neutral' | 'danger'\n/** The height of the items, from the scale every control shares. */\nexport type ToggleSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'\n/** Whether the items run across the page or down it. */\nexport type ToggleOrientation = 'horizontal' | 'vertical'\n\ninterface ToggleProps {\n /** Allows several items to be chosen at once, which makes the v-model a list. */\n multiple?: boolean\n /**\n * Refuses to let the last chosen item be given up, so that something is always\n * selected once something has been. It is a guard and nothing more: it selects\n * nothing on its own at the start.\n */\n mandatory?: boolean\n /**\n * Leaves the items as separate buttons with a gap between them. Left out, they are\n * joined into one segmented control.\n */\n detached?: boolean\n /**\n * Takes the lines out from between the joined items, so the row reads as one frame\n * rather than as segments. It has no effect under `detached`, where the items are\n * separate buttons already.\n */\n seamless?: boolean\n /** Whether the items run across the page or down it. */\n orientation?: ToggleOrientation\n /**\n * Stretches the row across the whole inline size of its parent, every item taking an equal\n * share of it, on the terms of VButtonGroup's own `fullWidth`.\n */\n fullWidth?: boolean\n /**\n * How the UNSELECTED items are drawn. It is named for the ITEMS because that is what it\n * paints: on VTabs and VDataTable `variant` names the decoration of the frame instead.\n */\n itemVariant?: ToggleItemVariant\n /**\n * How the SELECTED item is drawn, in the group's tone: filled, tinted, or the colour\n * of its text alone.\n */\n selectedVariant?: ToggleSelectedVariant\n /** The colour a selected item takes. The others stay neutral. */\n tone?: ToggleTone\n /** The height of the items, from the scale shared by every control. */\n size?: ToggleSize\n /** Takes 4px off the height of every item. */\n compact?: boolean\n /** Raises the row off the page, on the terms of VButtonGroup's own `elevated`. */\n elevated?: boolean\n /** Makes the whole group unusable. */\n disabled?: boolean\n /**\n * Draws the selected item's icon in its filled form, a common way of reinforcing\n * that it is the one in effect.\n */\n selectedIconFilled?: boolean\n /**\n * What screen readers announce for the group, such as \"Text alignment\" or \"Filters\". It is\n * strongly recommended: no default could say what a group of buttons is for.\n */\n label?: string\n}\n\nconst props = withDefaults(defineProps<ToggleProps>(), {\n multiple: false,\n mandatory: false,\n detached: false,\n seamless: false,\n orientation: 'horizontal',\n fullWidth: false,\n itemVariant: 'ghost',\n selectedVariant: 'solid',\n tone: 'accent',\n size: 'md',\n compact: false,\n elevated: false,\n disabled: false,\n selectedIconFilled: false,\n label: undefined,\n})\n\ndefineSlots<{\n /** The items of the group. */\n default(): unknown\n}>()\n\n/**\n * What is selected, and its shape follows `multiple`: a single value (or `null`, which is where\n * it starts) when one item may be chosen, and an array when several may. A null or scalar value\n * passed in `multiple` mode is read as an empty selection.\n */\nconst model = defineModel<ToggleModelValue>({ default: null })\n\nfunction isSelected(value: ToggleValue): boolean {\n return props.multiple\n ? Array.isArray(model.value) && model.value.includes(value)\n : model.value === value\n}\n\n/*\n * With several choices allowed, the list is never changed in place: each write produces a new\n * one. That is what makes a consumer's own watchers and computed values notice; mutating the\n * existing list would leave some of them silent.\n */\nfunction select(value: ToggleValue) {\n if (props.multiple) {\n const current = Array.isArray(model.value) ? model.value : []\n // `every` rather than a length of one: the removal takes out every copy of the value, so\n // a list holding it twice would otherwise be emptied past the guard.\n if (props.mandatory && current.length > 0 && current.every((v) => v === value)) return\n model.value = toggleValue(current, value)\n return\n }\n if (model.value === value) {\n if (!props.mandatory) model.value = null\n return\n }\n model.value = value\n}\n\nprovide(toggleKey, {\n isSelected,\n select,\n get itemVariant() {\n return props.itemVariant\n },\n get selectedVariant() {\n return props.selectedVariant\n },\n get tone() {\n return props.tone\n },\n get selectedIconFilled() {\n return props.selectedIconFilled\n },\n})\n\n// @keyboard @a11y\n/*\n * Every visible item stays a stop in the tab order here; the VPagination model. The single-stop\n * treatment is reserved for the patterns that require it, a row of tabs or a radio group, and a\n * group of buttons is neither.\n */\nfunction onKeydown(event: KeyboardEvent) {\n if (!(event.target as Element).matches('.v-toggle-item')) return\n const group = event.currentTarget as HTMLElement\n arrowNavigate(event, group, () => navigableItems(group, '.v-toggle-item:not(:disabled)'), {\n vertical: props.orientation === 'vertical',\n })\n}\n\nconst groupRef = ref<InstanceType<typeof VButtonGroup> | null>(null)\nconst groupEl = computed(() => groupRef.value?.el ?? null)\n\n// `focus` goes to the item that stands for the current choice, the one a keyboard user would\n// expect to resume from.\ndefineExpose({\n /**\n * Moves the focus to the selected item (the first of them when several are), or to the\n * first item that can take it when nothing is selected.\n */\n focus: (options?: FocusOptions) => {\n const group = groupEl.value\n const enabled = '.v-toggle-item:not(:disabled, [aria-disabled=\"true\"])'\n const item =\n group?.querySelector<HTMLElement>(`${enabled}[aria-pressed=\"true\"]`) ??\n group?.querySelector<HTMLElement>(enabled)\n item?.focus(options)\n },\n /** The `role=\"group\"` row, which is also where the consumer's attributes land. */\n el: groupEl,\n})\n</script>\n\n<template>\n <!--\n Pass default group booleans as undefined so absent opinions do not force false over\n individual toggle items.\n -->\n <VButtonGroup\n ref=\"groupRef\"\n class=\"v-toggle\"\n :orientation=\"orientation\"\n :detached=\"detached\"\n :seamless=\"seamless\"\n :full-width=\"fullWidth\"\n :size=\"size\"\n :compact=\"compact || undefined\"\n :elevated=\"elevated || undefined\"\n :disabled=\"disabled || undefined\"\n :label=\"label\"\n :data-item-variant=\"itemVariant\"\n @keydown=\"onKeydown\"\n >\n <slot />\n </VButtonGroup>\n</template>\n\n<style>\n@layer vectis.components {\n /*\n * The `:not(:where(.v-overlay))` guard weighs nothing and keeps a panel from passing for an\n * item; the pair is one specificity, so the arbitration with the group's seamless rules holds\n * on both shapes. The frame of an `outline` row, which only that row has: the colour its\n * unselected items already paint.\n */\n .v-toggle[data-item-variant='outline'] {\n --toggle-frame: var(--vectis-color-border-strong);\n }\n\n .v-toggle[data-item-variant='outline'] > .v-toggle-item:is(:disabled, [aria-disabled='true']),\n .v-toggle[data-item-variant='outline']\n > :not(:where(.v-overlay, .v-button-group))\n .v-toggle-item:is(:disabled, [aria-disabled='true']):not(:where(.v-overlay *)) {\n --toggle-frame: var(--vectis-color-border);\n }\n\n /*\n * `soft` and `ghost` leave VButton's border transparent, which in an outline row opens a gap\n * in the frame for the whole width of the selected segment. Restoring it on all four sides is\n * what keeps the frame closed wherever the selection sits, in either orientation and in RTL.\n */\n .v-toggle[data-item-variant='outline']\n > .v-toggle-item[aria-pressed='true']:is([data-variant='soft'], [data-variant='ghost']),\n .v-toggle[data-item-variant='outline']\n > :not(:where(.v-overlay, .v-button-group))\n .v-toggle-item[aria-pressed='true']:is([data-variant='soft'], [data-variant='ghost']):not(\n :where(.v-overlay *)\n ) {\n border-color: var(--toggle-frame);\n }\n\n /*\n * The class is repeated to reach (0,6,0). The variant, hover and active rules reach (0,5,0),\n * and with the forcing turned off any of them that still won would paint its tone over the\n * selection, with HighlightText on top of it.\n */\n @media (forced-colors: active) {\n .v-toggle-item.v-toggle-item.v-toggle-item.v-toggle-item[aria-pressed='true']:not(:disabled) {\n forced-color-adjust: none;\n background-color: Highlight;\n color: HighlightText;\n border-color: Highlight;\n outline-color: CanvasText;\n }\n }\n}\n</style>\n"],"mappings":""}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VToggle.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../src/components/VToggle/VToggle.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @keyboard @core\n/**\n * A row of buttons where one — or several — stay pressed: choosing a text alignment,\n * a view, a set of filters. A single v-model holds what is chosen.\n *\n * Every item is a VButton, the selected one drawn in the group's tone and the others\n * quiet. The row itself is a VButtonGroup, which is where the drawing lives: joining the\n * items or leaving them apart, the lines between them, the shared size and the shared\n * elevation are that component's, and the props for them are handed straight over.\n * Nothing about hovering, focusing or disabling is written here either: it all comes\n * from VButton.\n *\n * The JavaScript covers the two things HTML has no answer for: what a click does to\n * the selection — toggling it off, or refusing to when the last choice may not be\n * given up — and moving between the items with the arrow keys.\n */\nimport { computed, provide, ref } from 'vue'\n\nimport VButtonGroup from '../VButton/VButtonGroup.vue'\nimport { toggleKey } from './context'\n\nimport { toggleValue } from '../../utils/array'\nimport { arrowNavigate, navigableItems } from '../../utils/arrowNav'\n\nimport type { ItemValue } from '../../types'\n\n/**\n * What identifies an item. It is the design system's own `ItemValue`, re-exported\n * under the family's name so a consumer typing a toggle need not reach for another module.\n */\nexport type ToggleValue = ItemValue\n/**\n * What the v-model holds, which follows `multiple`: one value or nothing when a single\n * choice is allowed, and a list of them when several are.\n */\nexport type ToggleModelValue = ToggleValue | ToggleValue[] | null\n/** How the items that are not selected are drawn. */\nexport type ToggleItemVariant = 'ghost' | 'outline'\n/** How a selected item is drawn. */\nexport type ToggleSelectedVariant = 'solid' | 'soft' | 'ghost'\n/** The colour of a selected item. */\nexport type ToggleTone = 'accent' | 'neutral' | 'danger'\n/** The height of the items, from the scale every control shares. */\nexport type ToggleSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'\n/** Whether the items run across the page or down it. */\nexport type ToggleOrientation = 'horizontal' | 'vertical'\n\ninterface ToggleProps {\n /** Allows several items to be chosen at once, which makes the v-model a list. */\n multiple?: boolean\n /**\n * Refuses to let the last chosen item be given up, so that something is always\n * selected once something has been. It is a guard and nothing more: it selects\n * nothing on its own at the start.\n */\n mandatory?: boolean\n /**\n * Leaves the items as separate buttons with a gap between them. Left out, they are\n * joined into one segmented control.\n */\n detached?: boolean\n /**\n * Takes the lines out from between the joined items, so the row reads as one frame\n * rather than as segments. It has no effect under `detached`, where the items are\n * separate buttons already.\n */\n seamless?: boolean\n /** Whether the items run across the page or down it. */\n orientation?: ToggleOrientation\n /**\n * Stretches the row across the whole inline size of its parent, every item taking an equal\n * share of it, on the terms of VButtonGroup's own `fullWidth`.\n */\n fullWidth?: boolean\n /**\n * How the UNSELECTED items are drawn. What the selected one takes is `selectedVariant`.\n * It is named for the ITEMS because that is what it paints: on VTabs and VDataTable\n * `variant` names the decoration of the frame instead.\n */\n itemVariant?: ToggleItemVariant\n /**\n * How the SELECTED item is drawn, in the group's tone: filled, tinted, or the colour\n * of its text alone.\n */\n selectedVariant?: ToggleSelectedVariant\n /** The colour a selected item takes. The others stay neutral. */\n tone?: ToggleTone\n /** The height of the items, from the scale shared by every control. */\n size?: ToggleSize\n /** Takes 4px off the height of every item. */\n compact?: boolean\n /** Raises the row off the page, on the terms of VButtonGroup's own `elevated`. */\n elevated?: boolean\n /** Makes the whole group unusable. */\n disabled?: boolean\n /**\n * Draws the selected item's icon in its filled form, a common way of reinforcing\n * that it is the one in effect.\n */\n selectedIconFilled?: boolean\n /**\n * What screen readers announce for the group, such as \"Text alignment\" or \"Filters\". It is\n * strongly recommended: no default could say what a group of buttons is for.\n */\n label?: string\n}\n\nconst props = withDefaults(defineProps<ToggleProps>(), {\n multiple: false,\n mandatory: false,\n detached: false,\n seamless: false,\n orientation: 'horizontal',\n fullWidth: false,\n itemVariant: 'ghost',\n selectedVariant: 'solid',\n tone: 'accent',\n size: 'md',\n compact: false,\n elevated: false,\n disabled: false,\n selectedIconFilled: false,\n label: undefined,\n})\n\ndefineSlots<{\n /** The items of the group. */\n default(): unknown\n}>()\n\n/**\n * What is selected, and its SHAPE follows `multiple`: a single value (or `null`, which is\n * where it starts) when one item may be chosen, and an array when several may. A null or\n * scalar value passed in `multiple` mode is read as an empty selection.\n *\n * The array is never mutated in place; each change is a new one, which is what wakes the\n * consumer's binding. Re-clicking the selected item deselects it unless `mandatory` is set.\n */\nconst model = defineModel<ToggleModelValue>({ default: null })\n\nfunction isSelected(value: ToggleValue): boolean {\n return props.multiple\n ? Array.isArray(model.value) && model.value.includes(value)\n : model.value === value\n}\n\n/*\n * What a click does to the selection.\n *\n * With several choices allowed, the list is NEVER changed in place: each write\n * produces a new one. That is what makes a consumer's own watchers and computed values\n * notice; mutating the existing list would leave some of them silent. A model still\n * holding a single value, or nothing, is treated as an empty list rather than\n * refused.\n *\n * There is no check for the disabled state here, and none is needed: a disabled button\n * emits no click at all.\n */\nfunction select(value: ToggleValue) {\n if (props.multiple) {\n const current = Array.isArray(model.value) ? model.value : []\n // `every` rather than a length of one: the removal takes out every copy of the value, so\n // a list holding it twice would otherwise be emptied past the guard.\n if (props.mandatory && current.length > 0 && current.every((v) => v === value)) return\n model.value = toggleValue(current, value)\n return\n }\n if (model.value === value) {\n if (!props.mandatory) model.value = null\n return\n }\n model.value = value\n}\n\nprovide(toggleKey, {\n isSelected,\n select,\n get itemVariant() {\n return props.itemVariant\n },\n get selectedVariant() {\n return props.selectedVariant\n },\n get tone() {\n return props.tone\n },\n get selectedIconFilled() {\n return props.selectedIconFilled\n },\n})\n\n// @keyboard @a11y\n/*\n * The arrow keys, through the shared implementation in `utils/arrowNav`.\n *\n * Every visible item stays a stop in the tab order here — the VPagination model. The\n * single-stop treatment is reserved for the patterns that require it, a row of tabs or\n * a radio group, and a group of buttons is neither. Disabled items are really disabled\n * buttons, which the selector leaves out.\n *\n * The container is read from the event rather than kept as a reference: the root is a\n * VButtonGroup, so a template ref would hand back its component instance rather than the\n * element the items are navigated inside.\n *\n * Only a key pressed ON an item is taken. An item may carry a companion whose content sits\n * inside the group (a VPopover's panel stays a DOM descendant of its trigger), and an arrow\n * typed in a field there must move the caret, not the focus.\n */\nfunction onKeydown(event: KeyboardEvent) {\n if (!(event.target as Element).matches('.v-toggle-item')) return\n const group = event.currentTarget as HTMLElement\n arrowNavigate(event, group, () => navigableItems(group, '.v-toggle-item:not(:disabled)'), {\n vertical: props.orientation === 'vertical',\n })\n}\n\nconst groupRef = ref<InstanceType<typeof VButtonGroup> | null>(null)\nconst groupEl = computed(() => groupRef.value?.el ?? null)\n\n// The root is a VButtonGroup and the items come from the consumer's slot, so a template ref\n// on the toggle reaches no button. `focus` goes to the item that stands for the current\n// choice, the one a keyboard user would expect to resume from.\ndefineExpose({\n /**\n * Moves the focus to the selected item (the first of them when several are), or to the\n * first item that can take it when nothing is selected.\n */\n focus: (options?: FocusOptions) => {\n const group = groupEl.value\n const enabled = '.v-toggle-item:not(:disabled, [aria-disabled=\"true\"])'\n const item =\n group?.querySelector<HTMLElement>(`${enabled}[aria-pressed=\"true\"]`) ??\n group?.querySelector<HTMLElement>(enabled)\n item?.focus(options)\n },\n /** The `role=\"group\"` row, which is also where the consumer's attributes land. */\n el: groupEl,\n})\n</script>\n\n<template>\n <!-- The row is a VButtonGroup, which merges the borders of the buttons among its\n children — which is why an item renders the button as its own root, and why what\n may stand between the two is the wrappers a VTooltip, a VPopover or a VBadge put\n there, and nothing else. It brings the role, its accessible name and the orientation\n attribute with it, and of the props handed to it here it forwards the size, the density,\n the elevation and the disabled state to the buttons itself.\n\n The three booleans go down as `|| undefined`, the VPagination idiom: `undefined` is\n what means \"no opinion\" to a group, where `false` is an order every item would obey,\n and a toggle left at its defaults has no opinion to give. The size is always one, the\n toggle having a default of its own.\n\n The name is resolved by the group, which sees a consumer's `aria-label` or\n `aria-labelledby` too: they fall through this component onto it.\n\n `variant` is deliberately NOT among them, and neither is the tone: the group wins\n over a button that names its own variant, which would erase the one the selected\n item carries. Both stay with the items, through the context.\n\n `data-item-variant` is a plain attribute, and it is what tells the sheet an outline row\n from a ghost one — something the items cannot say for themselves, since the\n selected one carries its own variant rather than the row's. -->\n <VButtonGroup\n ref=\"groupRef\"\n class=\"v-toggle\"\n :orientation=\"orientation\"\n :detached=\"detached\"\n :seamless=\"seamless\"\n :full-width=\"fullWidth\"\n :size=\"size\"\n :compact=\"compact || undefined\"\n :elevated=\"elevated || undefined\"\n :disabled=\"disabled || undefined\"\n :label=\"label\"\n :data-item-variant=\"itemVariant\"\n @keydown=\"onKeydown\"\n >\n <slot />\n </VButtonGroup>\n</template>\n\n<style>\n@layer vectis.components {\n /*\n * Both rules below are written for the two shapes an item can take, exactly as\n * VButtonGroup's own sheet is and for the same reason: a VTooltip, a VPopover or a\n * VBadge puts a wrapper between the row and the item, and a rule reaching for a direct\n * child alone would leave a gap in the frame around whichever item carries one. The\n * `:not(:where(.v-overlay))` guard weighs nothing and keeps a panel from passing for an\n * item; the pair is one specificity, so the arbitration with the group's seamless rules\n * holds on both shapes.\n *\n * The frame of an `outline` row, which only that row has: the colour its unselected\n * items already paint. They are drawn in the NEUTRAL tone, so this is what their\n * `--tone-border-soft` resolves to — naming that variable here instead would read the\n * SELECTED item's tone and tint one segment of the frame accent or red.\n *\n * It is a variable on the root rather than a value in the rule below because a\n * disabled segment greys, and VButton greys an outline border to a different token.\n * Nothing else declares it, so the two rules never arbitrate a border-color between\n * them.\n */\n .v-toggle[data-item-variant='outline'] {\n --toggle-frame: var(--vectis-color-border-strong);\n }\n\n .v-toggle[data-item-variant='outline'] > .v-toggle-item:is(:disabled, [aria-disabled='true']),\n .v-toggle[data-item-variant='outline']\n > :not(:where(.v-overlay, .v-button-group))\n .v-toggle-item:is(:disabled, [aria-disabled='true']):not(:where(.v-overlay *)) {\n --toggle-frame: var(--vectis-color-border);\n }\n\n /*\n * `soft` and `ghost` leave VButton's border transparent, which in an outline row\n * opens a gap in the frame for the whole width of the selected segment. Restoring it\n * on all four sides is what keeps the frame closed wherever the selection sits, in\n * either orientation and in RTL. It is not conditioned on `seamless`, and deliberately\n * so: the border is transparent whether the internal lines are drawn or not. Seamless,\n * the shared edges are cleared again by VButtonGroup's own (0,6,0) rules, which is why\n * they are written at that weight — a middle segment then keeps its top and bottom\n * alone.\n */\n .v-toggle[data-item-variant='outline']\n > .v-toggle-item[aria-pressed='true']:is([data-variant='soft'], [data-variant='ghost']),\n .v-toggle[data-item-variant='outline']\n > :not(:where(.v-overlay, .v-button-group))\n .v-toggle-item[aria-pressed='true']:is([data-variant='soft'], [data-variant='ghost']):not(\n :where(.v-overlay *)\n ) {\n border-color: var(--toggle-frame);\n }\n\n /*\n * Windows forced colors replace every background with the page's, so the pressed item would look\n * exactly like the others there: the fill is its only cue, the state being in an ARIA\n * attribute a sighted reader never sees. It takes the system's own selection pair\n * instead, and opts out of the forcing for that element alone so the pair is painted.\n *\n * TRAP — the class is repeated to reach (0,6,0). The variant, hover and active rules\n * reach (0,5,0), and with the forcing turned off any of them that still won would paint\n * its tone over the selection, with HighlightText on top of it.\n */\n @media (forced-colors: active) {\n .v-toggle-item.v-toggle-item.v-toggle-item.v-toggle-item[aria-pressed='true']:not(:disabled) {\n forced-color-adjust: none;\n background-color: Highlight;\n color: HighlightText;\n border-color: Highlight;\n outline-color: CanvasText;\n }\n }\n}\n</style>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4GA,IAAM,IAAQ,GA+BR,IAAQ,EAA6B,GAAA,YAAkB;EAE7D,SAAS,EAAW,GAA6B;GAC/C,OAAO,EAAM,WACT,MAAM,QAAQ,EAAM,KAAK,KAAK,EAAM,MAAM,SAAS,CAAK,IACxD,EAAM,UAAU;EACtB;EAcA,SAAS,EAAO,GAAoB;GAClC,IAAI,EAAM,UAAU;IAClB,IAAM,IAAU,MAAM,QAAQ,EAAM,KAAK,IAAI,EAAM,QAAQ,CAAC;IAG5D,IAAI,EAAM,aAAa,EAAQ,SAAS,KAAK,EAAQ,OAAO,MAAM,MAAM,CAAK,GAAG;IAChF,EAAM,QAAQ,EAAY,GAAS,CAAK;IACxC;GACF;GACA,IAAI,EAAM,UAAU,GAAO;IACzB,AAAK,EAAM,cAAW,EAAM,QAAQ;IACpC;GACF;GACA,EAAM,QAAQ;EAChB;EAEA,EAAQ,GAAW;GACjB;GACA;GACA,IAAI,cAAc;IAChB,OAAO,EAAM;GACf;GACA,IAAI,kBAAkB;IACpB,OAAO,EAAM;GACf;GACA,IAAI,OAAO;IACT,OAAO,EAAM;GACf;GACA,IAAI,qBAAqB;IACvB,OAAO,EAAM;GACf;EACF,CAAC;EAmBD,SAAS,EAAU,GAAsB;GACvC,IAAI,CAAE,EAAM,OAAmB,QAAQ,gBAAgB,GAAG;GAC1D,IAAM,IAAQ,EAAM;GACpB,EAAc,GAAO,SAAa,EAAe,GAAO,+BAA+B,GAAG,EACxF,UAAU,EAAM,gBAAgB,WAClC,CAAC;EACH;EAEA,IAAM,IAAW,EAA8C,IAAI,GAC7D,IAAU,QAAe,EAAS,OAAO,MAAM,IAAI;SAKzD,EAAa;GAKX,QAAQ,MAA2B;IACjC,IAAM,IAAQ,EAAQ,OAChB,IAAU;IAIhB,CAFE,GAAO,cAA2B,GAAG,EAAQ,sBAAsB,KACnE,GAAO,cAA2B,CAAO,EAAA,EACrC,MAAM,CAAO;GACrB;GAEA,IAAI;EACN,CAAC,mBA0BC,EAgBe,GAAA;YAfT;GAAJ,KAAI;GACJ,OAAM;GACL,aAAa,EAAA;GACb,UAAU,EAAA;GACV,UAAU,EAAA;GACV,cAAY,EAAA;GACZ,MAAM,EAAA;GACN,SAAS,EAAA,WAAW,KAAA;GACpB,UAAU,EAAA,YAAY,KAAA;GACtB,UAAU,EAAA,YAAY,KAAA;GACtB,OAAO,EAAA;GACP,qBAAmB,EAAA;GACV;;oBAEF,CAAR,EAAQ,EAAA,QAAA,SAAA,CAAA,CAAA"}
|
|
1
|
+
{"version":3,"file":"VToggle.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../src/components/VToggle/VToggle.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @keyboard @core\n/**\n * Reuse segmented native buttons with aria-pressed. JavaScript synchronizes selection and\n * provides group keyboard navigation.\n */\nimport { computed, provide, ref } from 'vue'\n\nimport VButtonGroup from '../VButton/VButtonGroup.vue'\nimport { toggleKey } from './context'\n\nimport { toggleValue } from '../../utils/array'\nimport { arrowNavigate, navigableItems } from '../../utils/arrowNav'\n\nimport type { ItemValue } from '../../types'\n\n/**\n * What identifies an item. It is the design system's own `ItemValue`, re-exported\n * under the family's name so a consumer typing a toggle need not reach for another module.\n */\nexport type ToggleValue = ItemValue\n/**\n * What the v-model holds, which follows `multiple`: one value or nothing when a single\n * choice is allowed, and a list of them when several are.\n */\nexport type ToggleModelValue = ToggleValue | ToggleValue[] | null\n/** How the items that are not selected are drawn. */\nexport type ToggleItemVariant = 'ghost' | 'outline'\n/** How a selected item is drawn. */\nexport type ToggleSelectedVariant = 'solid' | 'soft' | 'ghost'\n/** The colour of a selected item. */\nexport type ToggleTone = 'accent' | 'neutral' | 'danger'\n/** The height of the items, from the scale every control shares. */\nexport type ToggleSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'\n/** Whether the items run across the page or down it. */\nexport type ToggleOrientation = 'horizontal' | 'vertical'\n\ninterface ToggleProps {\n /** Allows several items to be chosen at once, which makes the v-model a list. */\n multiple?: boolean\n /**\n * Refuses to let the last chosen item be given up, so that something is always\n * selected once something has been. It is a guard and nothing more: it selects\n * nothing on its own at the start.\n */\n mandatory?: boolean\n /**\n * Leaves the items as separate buttons with a gap between them. Left out, they are\n * joined into one segmented control.\n */\n detached?: boolean\n /**\n * Takes the lines out from between the joined items, so the row reads as one frame\n * rather than as segments. It has no effect under `detached`, where the items are\n * separate buttons already.\n */\n seamless?: boolean\n /** Whether the items run across the page or down it. */\n orientation?: ToggleOrientation\n /**\n * Stretches the row across the whole inline size of its parent, every item taking an equal\n * share of it, on the terms of VButtonGroup's own `fullWidth`.\n */\n fullWidth?: boolean\n /**\n * How the UNSELECTED items are drawn. It is named for the ITEMS because that is what it\n * paints: on VTabs and VDataTable `variant` names the decoration of the frame instead.\n */\n itemVariant?: ToggleItemVariant\n /**\n * How the SELECTED item is drawn, in the group's tone: filled, tinted, or the colour\n * of its text alone.\n */\n selectedVariant?: ToggleSelectedVariant\n /** The colour a selected item takes. The others stay neutral. */\n tone?: ToggleTone\n /** The height of the items, from the scale shared by every control. */\n size?: ToggleSize\n /** Takes 4px off the height of every item. */\n compact?: boolean\n /** Raises the row off the page, on the terms of VButtonGroup's own `elevated`. */\n elevated?: boolean\n /** Makes the whole group unusable. */\n disabled?: boolean\n /**\n * Draws the selected item's icon in its filled form, a common way of reinforcing\n * that it is the one in effect.\n */\n selectedIconFilled?: boolean\n /**\n * What screen readers announce for the group, such as \"Text alignment\" or \"Filters\". It is\n * strongly recommended: no default could say what a group of buttons is for.\n */\n label?: string\n}\n\nconst props = withDefaults(defineProps<ToggleProps>(), {\n multiple: false,\n mandatory: false,\n detached: false,\n seamless: false,\n orientation: 'horizontal',\n fullWidth: false,\n itemVariant: 'ghost',\n selectedVariant: 'solid',\n tone: 'accent',\n size: 'md',\n compact: false,\n elevated: false,\n disabled: false,\n selectedIconFilled: false,\n label: undefined,\n})\n\ndefineSlots<{\n /** The items of the group. */\n default(): unknown\n}>()\n\n/**\n * What is selected, and its shape follows `multiple`: a single value (or `null`, which is where\n * it starts) when one item may be chosen, and an array when several may. A null or scalar value\n * passed in `multiple` mode is read as an empty selection.\n */\nconst model = defineModel<ToggleModelValue>({ default: null })\n\nfunction isSelected(value: ToggleValue): boolean {\n return props.multiple\n ? Array.isArray(model.value) && model.value.includes(value)\n : model.value === value\n}\n\n/*\n * With several choices allowed, the list is never changed in place: each write produces a new\n * one. That is what makes a consumer's own watchers and computed values notice; mutating the\n * existing list would leave some of them silent.\n */\nfunction select(value: ToggleValue) {\n if (props.multiple) {\n const current = Array.isArray(model.value) ? model.value : []\n // `every` rather than a length of one: the removal takes out every copy of the value, so\n // a list holding it twice would otherwise be emptied past the guard.\n if (props.mandatory && current.length > 0 && current.every((v) => v === value)) return\n model.value = toggleValue(current, value)\n return\n }\n if (model.value === value) {\n if (!props.mandatory) model.value = null\n return\n }\n model.value = value\n}\n\nprovide(toggleKey, {\n isSelected,\n select,\n get itemVariant() {\n return props.itemVariant\n },\n get selectedVariant() {\n return props.selectedVariant\n },\n get tone() {\n return props.tone\n },\n get selectedIconFilled() {\n return props.selectedIconFilled\n },\n})\n\n// @keyboard @a11y\n/*\n * Every visible item stays a stop in the tab order here; the VPagination model. The single-stop\n * treatment is reserved for the patterns that require it, a row of tabs or a radio group, and a\n * group of buttons is neither.\n */\nfunction onKeydown(event: KeyboardEvent) {\n if (!(event.target as Element).matches('.v-toggle-item')) return\n const group = event.currentTarget as HTMLElement\n arrowNavigate(event, group, () => navigableItems(group, '.v-toggle-item:not(:disabled)'), {\n vertical: props.orientation === 'vertical',\n })\n}\n\nconst groupRef = ref<InstanceType<typeof VButtonGroup> | null>(null)\nconst groupEl = computed(() => groupRef.value?.el ?? null)\n\n// `focus` goes to the item that stands for the current choice, the one a keyboard user would\n// expect to resume from.\ndefineExpose({\n /**\n * Moves the focus to the selected item (the first of them when several are), or to the\n * first item that can take it when nothing is selected.\n */\n focus: (options?: FocusOptions) => {\n const group = groupEl.value\n const enabled = '.v-toggle-item:not(:disabled, [aria-disabled=\"true\"])'\n const item =\n group?.querySelector<HTMLElement>(`${enabled}[aria-pressed=\"true\"]`) ??\n group?.querySelector<HTMLElement>(enabled)\n item?.focus(options)\n },\n /** The `role=\"group\"` row, which is also where the consumer's attributes land. */\n el: groupEl,\n})\n</script>\n\n<template>\n <!--\n Pass default group booleans as undefined so absent opinions do not force false over\n individual toggle items.\n -->\n <VButtonGroup\n ref=\"groupRef\"\n class=\"v-toggle\"\n :orientation=\"orientation\"\n :detached=\"detached\"\n :seamless=\"seamless\"\n :full-width=\"fullWidth\"\n :size=\"size\"\n :compact=\"compact || undefined\"\n :elevated=\"elevated || undefined\"\n :disabled=\"disabled || undefined\"\n :label=\"label\"\n :data-item-variant=\"itemVariant\"\n @keydown=\"onKeydown\"\n >\n <slot />\n </VButtonGroup>\n</template>\n\n<style>\n@layer vectis.components {\n /*\n * The `:not(:where(.v-overlay))` guard weighs nothing and keeps a panel from passing for an\n * item; the pair is one specificity, so the arbitration with the group's seamless rules holds\n * on both shapes. The frame of an `outline` row, which only that row has: the colour its\n * unselected items already paint.\n */\n .v-toggle[data-item-variant='outline'] {\n --toggle-frame: var(--vectis-color-border-strong);\n }\n\n .v-toggle[data-item-variant='outline'] > .v-toggle-item:is(:disabled, [aria-disabled='true']),\n .v-toggle[data-item-variant='outline']\n > :not(:where(.v-overlay, .v-button-group))\n .v-toggle-item:is(:disabled, [aria-disabled='true']):not(:where(.v-overlay *)) {\n --toggle-frame: var(--vectis-color-border);\n }\n\n /*\n * `soft` and `ghost` leave VButton's border transparent, which in an outline row opens a gap\n * in the frame for the whole width of the selected segment. Restoring it on all four sides is\n * what keeps the frame closed wherever the selection sits, in either orientation and in RTL.\n */\n .v-toggle[data-item-variant='outline']\n > .v-toggle-item[aria-pressed='true']:is([data-variant='soft'], [data-variant='ghost']),\n .v-toggle[data-item-variant='outline']\n > :not(:where(.v-overlay, .v-button-group))\n .v-toggle-item[aria-pressed='true']:is([data-variant='soft'], [data-variant='ghost']):not(\n :where(.v-overlay *)\n ) {\n border-color: var(--toggle-frame);\n }\n\n /*\n * The class is repeated to reach (0,6,0). The variant, hover and active rules reach (0,5,0),\n * and with the forcing turned off any of them that still won would paint its tone over the\n * selection, with HighlightText on top of it.\n */\n @media (forced-colors: active) {\n .v-toggle-item.v-toggle-item.v-toggle-item.v-toggle-item[aria-pressed='true']:not(:disabled) {\n forced-color-adjust: none;\n background-color: Highlight;\n color: HighlightText;\n border-color: Highlight;\n outline-color: CanvasText;\n }\n }\n}\n</style>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgGA,IAAM,IAAQ,GA4BR,IAAQ,EAA6B,GAAA,YAAkB;EAE7D,SAAS,EAAW,GAA6B;GAC/C,OAAO,EAAM,WACT,MAAM,QAAQ,EAAM,KAAK,KAAK,EAAM,MAAM,SAAS,CAAK,IACxD,EAAM,UAAU;EACtB;EAOA,SAAS,EAAO,GAAoB;GAClC,IAAI,EAAM,UAAU;IAClB,IAAM,IAAU,MAAM,QAAQ,EAAM,KAAK,IAAI,EAAM,QAAQ,CAAC;IAG5D,IAAI,EAAM,aAAa,EAAQ,SAAS,KAAK,EAAQ,OAAO,MAAM,MAAM,CAAK,GAAG;IAChF,EAAM,QAAQ,EAAY,GAAS,CAAK;IACxC;GACF;GACA,IAAI,EAAM,UAAU,GAAO;IACzB,AAAK,EAAM,cAAW,EAAM,QAAQ;IACpC;GACF;GACA,EAAM,QAAQ;EAChB;EAEA,EAAQ,GAAW;GACjB;GACA;GACA,IAAI,cAAc;IAChB,OAAO,EAAM;GACf;GACA,IAAI,kBAAkB;IACpB,OAAO,EAAM;GACf;GACA,IAAI,OAAO;IACT,OAAO,EAAM;GACf;GACA,IAAI,qBAAqB;IACvB,OAAO,EAAM;GACf;EACF,CAAC;EAQD,SAAS,EAAU,GAAsB;GACvC,IAAI,CAAE,EAAM,OAAmB,QAAQ,gBAAgB,GAAG;GAC1D,IAAM,IAAQ,EAAM;GACpB,EAAc,GAAO,SAAa,EAAe,GAAO,+BAA+B,GAAG,EACxF,UAAU,EAAM,gBAAgB,WAClC,CAAC;EACH;EAEA,IAAM,IAAW,EAA8C,IAAI,GAC7D,IAAU,QAAe,EAAS,OAAO,MAAM,IAAI;SAIzD,EAAa;GAKX,QAAQ,MAA2B;IACjC,IAAM,IAAQ,EAAQ,OAChB,IAAU;IAIhB,CAFE,GAAO,cAA2B,GAAG,EAAQ,sBAAsB,KACnE,GAAO,cAA2B,CAAO,EAAA,EACrC,MAAM,CAAO;GACrB;GAEA,IAAI;EACN,CAAC,mBAQC,EAgBe,GAAA;YAfT;GAAJ,KAAI;GACJ,OAAM;GACL,aAAa,EAAA;GACb,UAAU,EAAA;GACV,UAAU,EAAA;GACV,cAAY,EAAA;GACZ,MAAM,EAAA;GACN,SAAS,EAAA,WAAW,KAAA;GACpB,UAAU,EAAA,YAAY,KAAA;GACtB,UAAU,EAAA,YAAY,KAAA;GACtB,OAAO,EAAA;GACP,qBAAmB,EAAA;GACV;;oBAEF,CAAR,EAAQ,EAAA,QAAA,SAAA,CAAA,CAAA"}
|
|
@@ -10,11 +10,7 @@ interface ToggleItemProps {
|
|
|
10
10
|
label?: string;
|
|
11
11
|
/** An icon before the label: an icon name, or an explicit render. */
|
|
12
12
|
iconStart?: IconSource;
|
|
13
|
-
/**
|
|
14
|
-
* An icon after the label. It is NOT switched to its filled form by the group's
|
|
15
|
-
* `selectedIconFilled`, which names the icon standing for the item rather than one
|
|
16
|
-
* trailing it; `iconFilled` fills it for good.
|
|
17
|
-
*/
|
|
13
|
+
/** An icon after the label. */
|
|
18
14
|
iconEnd?: IconSource;
|
|
19
15
|
/**
|
|
20
16
|
* Renders `iconStart` and `iconEnd` in their filled form (the font's `FILL` axis), whether
|
|
@@ -22,10 +18,6 @@ interface ToggleItemProps {
|
|
|
22
18
|
* of the selected item when this is left out.
|
|
23
19
|
*/
|
|
24
20
|
iconFilled?: boolean;
|
|
25
|
-
/**
|
|
26
|
-
* Makes this item unusable: it no longer responds, the arrow keys skip over it, and
|
|
27
|
-
* it greys out through the colour tokens.
|
|
28
|
-
*/
|
|
29
21
|
disabled?: boolean;
|
|
30
22
|
}
|
|
31
23
|
type __VLS_Slots = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VToggleItem.js","names":[],"sources":["../../../src/components/VToggle/VToggleItem.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n *
|
|
1
|
+
{"version":3,"file":"VToggleItem.js","names":[],"sources":["../../../src/components/VToggle/VToggleItem.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * Reuse VButton as the root so the group can join direct-child borders; context adds\n * aria-pressed and selection behaviour.\n */\nimport { computed, inject } from 'vue'\n\nimport VButton from '../VButton/VButton.vue'\nimport VIcon from '../VIcon/VIcon.vue'\nimport { iconProps } from '../VIcon/iconProps'\nimport type { IconSource } from '../VIcon/types'\nimport { toggleKey } from './context'\nimport type { ToggleValue } from './VToggle.vue'\n\ninterface ToggleItemProps {\n /**\n * What choosing this item means. It is what the group's v-model holds when the item\n * is selected, and it must be unique within the group.\n */\n value: ToggleValue\n /** The visible label. The default slot replaces it. */\n label?: string\n /** An icon before the label: an icon name, or an explicit render. */\n iconStart?: IconSource\n /** An icon after the label. */\n iconEnd?: IconSource\n /**\n * Renders `iconStart` and `iconEnd` in their filled form (the font's `FILL` axis), whether\n * the item is selected or not. The group's `selectedIconFilled` still fills the start icon\n * of the selected item when this is left out.\n */\n iconFilled?: boolean\n disabled?: boolean\n}\n\nconst props = withDefaults(defineProps<ToggleItemProps>(), {\n label: undefined,\n iconStart: undefined,\n iconEnd: undefined,\n iconFilled: false,\n disabled: false,\n})\n\nconst slots = defineSlots<{\n /** The content of the item, replacing the `label` prop. */\n default?(): unknown\n /** Content before the label, which takes the place of `iconStart`. */\n start?(): unknown\n /** Content after the label, which takes the place of `iconEnd`. */\n end?(): unknown\n}>()\n\nconst toggle = inject(toggleKey, null)\n\nconst selected = computed(() => toggle != null && toggle.isSelected(props.value))\n\n// A function read by the template, never a `computed`: `slots` is not reactive, so a computed\n// would keep its first answer while a slot behind a `v-if` comes and goes.\n/**\n * An icon, on either side, and no label at all: the item becomes a square, like a VIconButton.\n * The same definition as VChip's, and the square itself is VButton's `[data-icon-only]` rule.\n */\nfunction iconOnly() {\n return (\n !props.label &&\n !slots.default &&\n Boolean(slots.start || slots.end || props.iconStart || props.iconEnd)\n )\n}\n</script>\n\n<template>\n <!--\n The filled form is asked of the icon directly, because VButton's own icon options have no\n effect on an icon handed to it through a slot.\n -->\n <VButton\n class=\"v-toggle-item\"\n :aria-pressed=\"selected ? 'true' : 'false'\"\n :variant=\"selected && toggle ? toggle.selectedVariant : toggle?.itemVariant\"\n :tone=\"selected && toggle ? toggle.tone : 'neutral'\"\n :disabled=\"disabled\"\n :data-icon-only=\"iconOnly() ? '' : undefined\"\n @click=\"toggle?.select(value)\"\n >\n <template v-if=\"iconStart || $slots.start\" #start>\n <slot name=\"start\">\n <VIcon\n v-bind=\"iconProps(iconStart!)\"\n :filled=\"iconFilled || (selected && toggle?.selectedIconFilled)\"\n />\n </slot>\n </template>\n <template v-if=\"iconEnd || $slots.end\" #end>\n <slot name=\"end\"><VIcon v-bind=\"iconProps(iconEnd!)\" :filled=\"iconFilled\" /></slot>\n </template>\n <slot v-if=\"!iconOnly()\">{{ label }}</slot>\n </VButton>\n</template>\n"],"mappings":""}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VToggleItem.vue_vue_type_script_setup_true_lang.js","names":["$slots"],"sources":["../../../src/components/VToggle/VToggleItem.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n *
|
|
1
|
+
{"version":3,"file":"VToggleItem.vue_vue_type_script_setup_true_lang.js","names":["$slots"],"sources":["../../../src/components/VToggle/VToggleItem.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * Reuse VButton as the root so the group can join direct-child borders; context adds\n * aria-pressed and selection behaviour.\n */\nimport { computed, inject } from 'vue'\n\nimport VButton from '../VButton/VButton.vue'\nimport VIcon from '../VIcon/VIcon.vue'\nimport { iconProps } from '../VIcon/iconProps'\nimport type { IconSource } from '../VIcon/types'\nimport { toggleKey } from './context'\nimport type { ToggleValue } from './VToggle.vue'\n\ninterface ToggleItemProps {\n /**\n * What choosing this item means. It is what the group's v-model holds when the item\n * is selected, and it must be unique within the group.\n */\n value: ToggleValue\n /** The visible label. The default slot replaces it. */\n label?: string\n /** An icon before the label: an icon name, or an explicit render. */\n iconStart?: IconSource\n /** An icon after the label. */\n iconEnd?: IconSource\n /**\n * Renders `iconStart` and `iconEnd` in their filled form (the font's `FILL` axis), whether\n * the item is selected or not. The group's `selectedIconFilled` still fills the start icon\n * of the selected item when this is left out.\n */\n iconFilled?: boolean\n disabled?: boolean\n}\n\nconst props = withDefaults(defineProps<ToggleItemProps>(), {\n label: undefined,\n iconStart: undefined,\n iconEnd: undefined,\n iconFilled: false,\n disabled: false,\n})\n\nconst slots = defineSlots<{\n /** The content of the item, replacing the `label` prop. */\n default?(): unknown\n /** Content before the label, which takes the place of `iconStart`. */\n start?(): unknown\n /** Content after the label, which takes the place of `iconEnd`. */\n end?(): unknown\n}>()\n\nconst toggle = inject(toggleKey, null)\n\nconst selected = computed(() => toggle != null && toggle.isSelected(props.value))\n\n// A function read by the template, never a `computed`: `slots` is not reactive, so a computed\n// would keep its first answer while a slot behind a `v-if` comes and goes.\n/**\n * An icon, on either side, and no label at all: the item becomes a square, like a VIconButton.\n * The same definition as VChip's, and the square itself is VButton's `[data-icon-only]` rule.\n */\nfunction iconOnly() {\n return (\n !props.label &&\n !slots.default &&\n Boolean(slots.start || slots.end || props.iconStart || props.iconEnd)\n )\n}\n</script>\n\n<template>\n <!--\n The filled form is asked of the icon directly, because VButton's own icon options have no\n effect on an icon handed to it through a slot.\n -->\n <VButton\n class=\"v-toggle-item\"\n :aria-pressed=\"selected ? 'true' : 'false'\"\n :variant=\"selected && toggle ? toggle.selectedVariant : toggle?.itemVariant\"\n :tone=\"selected && toggle ? toggle.tone : 'neutral'\"\n :disabled=\"disabled\"\n :data-icon-only=\"iconOnly() ? '' : undefined\"\n @click=\"toggle?.select(value)\"\n >\n <template v-if=\"iconStart || $slots.start\" #start>\n <slot name=\"start\">\n <VIcon\n v-bind=\"iconProps(iconStart!)\"\n :filled=\"iconFilled || (selected && toggle?.selectedIconFilled)\"\n />\n </slot>\n </template>\n <template v-if=\"iconEnd || $slots.end\" #end>\n <slot name=\"end\"><VIcon v-bind=\"iconProps(iconEnd!)\" :filled=\"iconFilled\" /></slot>\n </template>\n <slot v-if=\"!iconOnly()\">{{ label }}</slot>\n </VButton>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;EAmCA,IAAM,IAAQ,GAQR,IAAQ,EAAA,GASR,IAAS,EAAO,GAAW,IAAI,GAE/B,IAAW,QAAe,KAAU,QAAQ,EAAO,WAAW,EAAM,KAAK,CAAC;EAQhF,SAAS,IAAW;GAClB,OACE,CAAC,EAAM,SACP,CAAC,EAAM,WACP,GAAQ,EAAM,SAAS,EAAM,OAAO,EAAM,aAAa,EAAM;EAEjE;yBAQE,EAqBU,GAAA;GApBR,OAAM;GACL,gBAAc,EAAA,QAAQ,SAAA;GACtB,SAAS,EAAA,SAAY,EAAA,CAAA,IAAS,EAAA,CAAA,CAAM,CAAC,kBAAkB,EAAA,CAAA,CAAM,EAAE;GAC/D,MAAM,EAAA,SAAY,EAAA,CAAA,IAAS,EAAA,CAAA,CAAM,CAAC,OAAI;GACtC,UAAU,EAAA;GACV,kBAAgB,EAAQ,IAAA,KAAU,KAAA;GAClC,SAAK,AAAA,EAAA,QAAA,MAAE,EAAA,CAAA,CAAM,EAAE,OAAO,EAAA,KAAK;;oBADT,CAcN,EAAQ,IAAY,EAAA,IAAA,EAAA,IAAjC,EAA2C,EAAA,QAAA,WAAA,CAAA,SAAA,CAAA,EAAA,EAAf,EAAA,KAAK,GAAA,CAAA,CAAA,GAAA,KAAA,GAAA,CAAA,CAAA,CAAA;;MAXjB,EAAA,aAAaA,EAAAA,OAAO,QAAA;SAAQ;eAMnC,CALP,EAKO,EAAA,QAAA,SAAA,CAAA,SAAA,CAJL,EAGE,GAHF,EACU,EAER,CAAA,CAFiB,CAAC,EAAA,SAAS,GAAA,EAC1B,QAAQ,EAAA,cAAe,EAAA,SAAY,EAAA,CAAA,CAAM,EAAE,mBAAA,CAAA,GAAA,MAAA,IAAA,CAAA,QAAA,CAAA,CAAA,CAAA,CAAA,CAAA;;cAIlC,EAAA,WAAWA,EAAAA,OAAO,MAAA;SAAM;eAC6C,CAAnF,EAAmF,EAAA,QAAA,OAAA,CAAA,SAAA,CAAlE,EAA2D,GAA3D,EAAe,EAA4C,CAAA,CAAnC,CAAC,EAAA,OAAO,GAAA,EAAK,QAAQ,EAAA,WAAU,CAAA,GAAA,MAAA,IAAA,CAAA,QAAA,CAAA,CAAA,CAAA,CAAA,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.js","names":[],"sources":["../../../src/components/VToggle/context.ts"],"sourcesContent":["/**\n *
|
|
1
|
+
{"version":3,"file":"context.js","names":[],"sources":["../../../src/components/VToggle/context.ts"],"sourcesContent":["/**\n * Context getters keep selection and props reactive; comparing item values needs no\n * SSR-sensitive child registry.\n */\n\nimport type { InjectionKey } from 'vue'\n\nimport type {\n ToggleSelectedVariant,\n ToggleTone,\n ToggleValue,\n ToggleItemVariant,\n} from './VToggle.vue'\n\nexport interface ToggleContext {\n /** Whether the item carrying this value is currently selected. */\n isSelected: (value: ToggleValue) => boolean\n /** Reports that an item was clicked; the group decides what it does to the selection. */\n select: (value: ToggleValue) => void\n /** How the unselected items are drawn. */\n readonly itemVariant: ToggleItemVariant\n /** How the selected item is drawn. */\n readonly selectedVariant: ToggleSelectedVariant\n /** The colour a selected item takes. */\n readonly tone: ToggleTone\n /** Whether the selected item draws its icon filled. */\n readonly selectedIconFilled: boolean\n}\n\nexport const toggleKey: InjectionKey<ToggleContext> = Symbol('v-toggle')\n"],"mappings":";AA6BA,IAAa,IAAyC,OAAO,UAAU"}
|
|
@@ -12,28 +12,15 @@ interface TooltipProps {
|
|
|
12
12
|
* opposite side by itself when there is not enough room.
|
|
13
13
|
*/
|
|
14
14
|
placement?: TooltipPlacement;
|
|
15
|
-
/**
|
|
16
|
-
* How long the pointer must rest on the element before the tooltip appears, in
|
|
17
|
-
* milliseconds. Keyboard focus opens it at once, the intent being in no doubt there,
|
|
18
|
-
* and a delay of 0 disables the wait entirely.
|
|
19
|
-
*/
|
|
15
|
+
/** How long the pointer must rest on the element before the tooltip appears, in milliseconds. */
|
|
20
16
|
delay?: number;
|
|
21
17
|
}
|
|
22
18
|
type __VLS_Slots = {
|
|
23
|
-
/**
|
|
24
|
-
* The element the tooltip describes. Bind the `triggerProps` it receives onto it, which
|
|
25
|
-
* is what ties the two together for assistive technology, and make sure it is something
|
|
26
|
-
* that can take focus, or keyboard users will never see the tooltip.
|
|
27
|
-
*/
|
|
19
|
+
/** The element the tooltip describes. */
|
|
28
20
|
default(props: {
|
|
29
21
|
triggerProps: TooltipTriggerProps;
|
|
30
22
|
}): unknown;
|
|
31
|
-
/**
|
|
32
|
-
* Content richer than a plain string: formatting, a keyboard shortcut, an icon. It
|
|
33
|
-
* must stay NON-interactive: the description is flattened to plain text for screen
|
|
34
|
-
* readers, and nothing inside can be reached from the keyboard. Content one can interact
|
|
35
|
-
* with belongs in a panel that stays open, such as VMenu.
|
|
36
|
-
*/
|
|
23
|
+
/** Content richer than a plain string: formatting, a keyboard shortcut, an icon. */
|
|
37
24
|
content?(): unknown;
|
|
38
25
|
};
|
|
39
26
|
declare function hide(): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VTooltip.js","names":[],"sources":["../../../src/components/VTooltip/VTooltip.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * A short explanation appearing beside an element on hover or keyboard focus. A VPopover in\n * `mode=\"manual\"`, a tooltip having its own rules about when to appear and when to go.\n *\n * Those rules are why there is JS here at all: HTML has no stable way to say \"show this\n * after a delay on hover or focus\", so the component handles the delay, the pointer, the\n * focus, a press on the trigger, and Escape — which must dismiss without moving the focus\n * (WCAG 1.4.13).\n *\n * It stays as long as EITHER the pointer rests on it (the trigger or the bubble) or the\n * keyboard focus is on the trigger, and goes when both have left, after a short grace the\n * pointer needs to cross the gap onto the bubble (WCAG 1.4.13: hoverable, persistent).\n * Two more ways out: Escape, heard from anywhere on the page while it shows and spent on\n * it, and the trigger being PRESSED, for a trigger opening a panel, where the tooltip would\n * otherwise stand over what it just opened.\n *\n * Positioning is pure CSS with no generated id: the wrapper names itself as the anchor and\n * confines that name to its own subtree. The confinement is essential — a shown panel moves\n * to the top layer, which anchor resolution treats as coming after the whole document, so\n * without it every tooltip would attach to the last wrapper named on the page.\n *\n * The wrapper stays HERE rather than going through VPopover's `#trigger`: a tooltip trigger\n * is not a `popovertarget` invoker, which would toggle on click, but an element the panel\n * DESCRIBES.\n */\n\nimport { computed, onBeforeUnmount, ref, useId } from 'vue'\n\nimport VPopover from '../VPopover/VPopover.vue'\n\nimport { useTimer } from '../../composables/useTimer'\nimport { isKeyboardFocus } from '../../utils/focus'\n\n/** Which side of the element the tooltip appears on. */\nexport type TooltipPlacement =\n | 'top'\n | 'top-start'\n | 'top-end'\n | 'bottom'\n | 'bottom-start'\n | 'bottom-end'\n | 'left'\n | 'left-start'\n | 'left-end'\n | 'right'\n | 'right-start'\n | 'right-end'\n\n/** What the described element has to carry: the link to the tooltip that describes it. */\nexport type TooltipTriggerProps = {\n 'aria-describedby': string\n}\n\ninterface TooltipProps {\n /** What the tooltip says. The `#content` slot replaces it when both are given. */\n text?: string\n /**\n * Which side of the element the tooltip appears on. The browser flips it to the\n * opposite side by itself when there is not enough room.\n */\n placement?: TooltipPlacement\n /**\n * How long the pointer must rest on the element before the tooltip appears, in\n * milliseconds. Keyboard focus opens it at once, the intent being in no doubt there,\n * and a delay of 0 disables the wait entirely.\n */\n delay?: number\n}\n\nconst props = withDefaults(defineProps<TooltipProps>(), {\n text: undefined,\n placement: 'top',\n delay: 300,\n})\n\ndefineSlots<{\n /**\n * The element the tooltip describes. Bind the `triggerProps` it receives onto it, which\n * is what ties the two together for assistive technology, and make sure it is something\n * that can take focus, or keyboard users will never see the tooltip.\n */\n default(props: { triggerProps: TooltipTriggerProps }): unknown\n /**\n * Content richer than a plain string: formatting, a keyboard shortcut, an icon. It\n * must stay NON-interactive: the description is flattened to plain text for screen\n * readers, and nothing inside can be reached from the keyboard. Content one can interact\n * with belongs in a panel that stays open, such as VMenu.\n */\n content?(): unknown\n}>()\n\nconst tooltipId = useId()\n// @a11y\n/*\n * The panel is opened and closed by calling it directly rather than through\n * `v-model:open`. Keyboard focus has to open it SYNCHRONOUSLY, and a model would go\n * through VPopover's watcher, hence through a tick. There is nothing lost either way:\n * a tooltip publishes no open state anyone needs to read.\n *\n * Calling twice is harmless — the guards for that live upstream in usePopover.\n */\nconst popoverRef = ref<InstanceType<typeof VPopover> | null>(null)\n\n// The delay before appearing. useTimer is what makes it re-armable and cancels it\n// when the component goes away.\nconst timer = useTimer()\n\n/**\n * How long a tooltip the pointer has left waits before going: the time to cross the gap\n * between the trigger and the bubble, which the margin leaves empty and which no pointer\n * event covers. The bubble is a DOM descendant of the wrapper, so reaching it counts as\n * coming back and cancels the close.\n */\nconst LEAVE_GRACE = 100\n\n// The two reasons it is showing. It goes only once neither holds.\nlet hovered = false\nlet focused = false\n\n// @core\nfunction show(immediate = false) {\n // A delay of 0 runs the callback synchronously — the design system's convention,\n // and what lets keyboard focus share this code path without waiting a tick.\n timer.start(() => popoverRef.value?.show(), immediate ? 0 : props.delay)\n}\n\nfunction hide() {\n timer.cancel()\n popoverRef.value?.close()\n}\n\nfunction onPointerEnter() {\n hovered = true\n show()\n}\n\nfunction onPointerLeave() {\n hovered = false\n if (focused) return\n timer.start(() => popoverRef.value?.close(), LEAVE_GRACE)\n}\n\nfunction onPointerDown() {\n hovered = false\n focused = false\n hide()\n}\n\nfunction onFocusOut() {\n focused = false\n if (!hovered) hide()\n}\n\n// @a11y\n/*\n * The tooltip belongs to the KEYBOARD focus, and a `focusin` says the focus arrived, never\n * how it got there. Three ways in are not a Tab: a click, which the pointer already covers\n * and which the `pointerdown` above has just closed the tooltip for; a tap, which no browser\n * agrees on; and a `focus()` from code — the one a closing panel hands back to the button\n * that opened it, which would raise a tooltip over a page the reader is no longer pointing\n * at. `:focus-visible` is the browser's own answer to the question, the same one VMenu asks\n * to decide where a menu's focus lands.\n *\n * The consequence to know: a tooltip never opens on a tap. Content a touch reader needs\n * cannot live here alone.\n */\nfunction onFocusIn(event: FocusEvent) {\n if (!isKeyboardFocus(event.target)) return\n focused = true\n show(true)\n}\n\n// @keyboard @a11y\n// Escape must dismiss a tooltip opened by hover or focus without moving the focus anywhere\n// (WCAG 1.4.13), for a magnifier user whose view it may be covering. It is heard on the\n// DOCUMENT while the tooltip shows, since a hovered tooltip rarely holds the focus, and it\n// is SPENT on it: inside a VDialog the same key would otherwise also be the dialog's close\n// request.\nfunction onDocumentKeydown(event: KeyboardEvent) {\n if (event.key !== 'Escape' || event.defaultPrevented) return\n event.preventDefault()\n hovered = false\n focused = false\n hide()\n}\n\nconst listening = ref(false)\nfunction onOpenChange(open: boolean) {\n if (open === listening.value) return\n listening.value = open\n if (open) document.addEventListener('keydown', onDocumentKeydown)\n else document.removeEventListener('keydown', onDocumentKeydown)\n}\nonBeforeUnmount(() => onOpenChange(false))\n\ndefineExpose({\n /** Shows the tooltip at once, without the hover delay. */\n show: () => show(true),\n /** Hides the tooltip. */\n close: hide,\n /** The tooltip bubble. */\n el: computed(() => popoverRef.value?.el ?? null),\n})\n</script>\n\n<template>\n <!--\n TRAP — `pointerdown` is what stops a tooltip from surviving the click that acted on its\n trigger, and the case it really covers is a trigger that OPENS something: a menu, a\n dialog, a popover of any kind. The tooltip would then stand over what it opened, and\n none of the other three exits applies — the pointer has not left, the focus has not\n moved out on its own, and nobody pressed Escape.\n\n It looks redundant on Chromium and Firefox, where a click focuses the button and the\n panel then takes the focus, which fires this wrapper's `focusout`. Safari on macOS does\n NOT focus a button on click, so nothing leaves the wrapper and the tooltip stays up,\n behind the panel, until the pointer moves. Delete this line and that is the bug that\n comes back, on one browser only.\n\n It is `pointerdown` and not `click`, so the tooltip is gone BEFORE the panel appears\n rather than overlapping it for a frame. Keyboard activation sends no `pointerdown` at\n all, and needs none: focus moving into the panel closes the tooltip everywhere.\n -->\n <span\n class=\"v-tooltip\"\n @pointerenter=\"onPointerEnter\"\n @pointerleave=\"onPointerLeave\"\n @pointerdown=\"onPointerDown\"\n @focusin=\"onFocusIn\"\n @focusout=\"onFocusOut\"\n >\n <slot :trigger-props=\"{ 'aria-describedby': tooltipId }\" />\n <VPopover\n :id=\"tooltipId\"\n ref=\"popoverRef\"\n mode=\"manual\"\n anchor=\"--tooltip-anchor\"\n :placement=\"placement\"\n bare\n role=\"tooltip\"\n class=\"v-tooltip-panel\"\n @update:open=\"onOpenChange\"\n >\n <slot name=\"content\">{{ text }}</slot>\n </VPopover>\n </span>\n</template>\n\n<style>\n@layer vectis.components {\n /* The wrapper is FLEX and not inline-block, for the same reason as `.v-badge-host`:\n both are inline-level atomic boxes, so the surrounding text flows identically, but\n an inline-block establishes an inline formatting context for the trigger — which\n then sits on a line box and adds the strut's descender to the wrapper's height.\n That is not only a couple of pixels in a flex row: this wrapper is also the ANCHOR,\n so the panel is offset by them too.\n\n `align-items: center` covers what is left. A wrapper stretched by a parent in\n `align-items: stretch` would otherwise stretch the trigger with it, where the\n inline-block form left it at its natural height. */\n .v-tooltip {\n display: inline-flex;\n align-items: center;\n anchor-name: --tooltip-anchor;\n /* Confining the name to this subtree is indispensable: a visible panel moves to\n the top layer, where anchor resolution treats it as coming after the whole\n document, so without it every tooltip would attach to the LAST wrapper carrying\n this name. */\n anchor-scope: --tooltip-anchor;\n }\n\n /* The anchoring itself comes from VPopover, through the `anchor` prop.\n\n The selector is written as a compound of two classes that both land on this same\n element, even though the panel class it guards against — `.v-panel` — is never\n present here, VTooltip asking for a `bare` panel. It is written that way so it cannot\n break the day that prop changes: at equal specificity the winner between two\n component sheets is decided by the order the consumer's bundler happens to\n produce, which is nobody's decision. */\n .v-popover-panel.v-tooltip-panel {\n inline-size: max-content;\n max-inline-size: min(\n var(--vectis-control-size-tooltip-max),\n calc(100dvi - var(--vectis-space-8))\n );\n padding: var(--vectis-space-1) var(--vectis-space-2);\n /* The tooltip is painted against the page rather than with it: a dark surface in\n both themes, darker still in the dark one, so it reads as an overlay whatever\n it happens to cover. */\n background: var(--vectis-color-surface-inverse);\n color: var(--vectis-color-text-on-inverse);\n border: none;\n /* The control radius capped at half the height of a ONE-line bubble, the row recipe\n of VSideNavigationItem: a tooltip wraps past its max-inline-size, and under a pill\n override a two-line bubble would otherwise round to half of its own height. */\n border-radius: min(var(--vectis-radius-interactive), calc(0.5lh + var(--vectis-space-1)));\n box-shadow: var(--vectis-shadow-sm);\n font-family: var(--vectis-text-family);\n font-size: var(--vectis-text-caption-size);\n line-height: var(--vectis-text-caption-leading);\n }\n\n /* Windows forced colors flattens the bubble's background to Canvas and drops its shadow,\n which were its only edge: it would float over the page with no boundary at all. An\n outline draws one without moving the layout by a pixel. */\n @media (forced-colors: active) {\n .v-popover-panel.v-tooltip-panel {\n outline: 1px solid CanvasText;\n }\n }\n}\n</style>\n"],"mappings":""}
|
|
1
|
+
{"version":3,"file":"VTooltip.js","names":[],"sources":["../../../src/components/VTooltip/VTooltip.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * Use a manual VPopover because tooltip hover, focus, delay and Escape policies differ from\n * native light dismissal. Pointer movement may require keeping the bubble open.\n */\n\nimport { computed, onBeforeUnmount, ref, useId } from 'vue'\n\nimport VPopover from '../VPopover/VPopover.vue'\n\nimport { useTimer } from '../../composables/useTimer'\nimport { isKeyboardFocus } from '../../utils/focus'\n\n/** Which side of the element the tooltip appears on. */\nexport type TooltipPlacement =\n | 'top'\n | 'top-start'\n | 'top-end'\n | 'bottom'\n | 'bottom-start'\n | 'bottom-end'\n | 'left'\n | 'left-start'\n | 'left-end'\n | 'right'\n | 'right-start'\n | 'right-end'\n\n/** What the described element has to carry: the link to the tooltip that describes it. */\nexport type TooltipTriggerProps = {\n 'aria-describedby': string\n}\n\ninterface TooltipProps {\n /** What the tooltip says. The `#content` slot replaces it when both are given. */\n text?: string\n /**\n * Which side of the element the tooltip appears on. The browser flips it to the\n * opposite side by itself when there is not enough room.\n */\n placement?: TooltipPlacement\n /** How long the pointer must rest on the element before the tooltip appears, in milliseconds. */\n delay?: number\n}\n\nconst props = withDefaults(defineProps<TooltipProps>(), {\n text: undefined,\n placement: 'top',\n delay: 300,\n})\n\ndefineSlots<{\n /** The element the tooltip describes. */\n default(props: { triggerProps: TooltipTriggerProps }): unknown\n /** Content richer than a plain string: formatting, a keyboard shortcut, an icon. */\n content?(): unknown\n}>()\n\nconst tooltipId = useId()\n// @a11y\n/*\n * Keyboard focus has to open it synchronously, and a model would go through VPopover's watcher,\n * hence through a tick. There is nothing lost either way: a tooltip publishes no open state\n * anyone needs to read.\n */\nconst popoverRef = ref<InstanceType<typeof VPopover> | null>(null)\n\n// The delay before appearing. useTimer is what makes it re-armable and cancels it\n// when the component goes away.\nconst timer = useTimer()\n\n/**\n * How long a tooltip the pointer has left waits before going: the time to cross the gap between\n * the trigger and the bubble, which the margin leaves empty and which no pointer event covers.\n */\nconst LEAVE_GRACE = 100\n\nlet hovered = false\nlet focused = false\n\n// @core\nfunction show(immediate = false) {\n // A delay of 0 runs the callback synchronously; the design system's convention, and what lets\n // keyboard focus share this code path without waiting a tick.\n timer.start(() => popoverRef.value?.show(), immediate ? 0 : props.delay)\n}\n\nfunction hide() {\n timer.cancel()\n popoverRef.value?.close()\n}\n\nfunction onPointerEnter() {\n hovered = true\n show()\n}\n\nfunction onPointerLeave() {\n hovered = false\n if (focused) return\n timer.start(() => popoverRef.value?.close(), LEAVE_GRACE)\n}\n\nfunction onPointerDown() {\n hovered = false\n focused = false\n hide()\n}\n\nfunction onFocusOut() {\n focused = false\n if (!hovered) hide()\n}\n\n// @a11y\n/*\n * The tooltip belongs to the KEYBOARD focus, and a `focusin` says the focus arrived, never how\n * it got there. `:focus-visible` is the browser's own answer to the question, the same one\n * VMenu asks to decide where a menu's focus lands.\n */\nfunction onFocusIn(event: FocusEvent) {\n if (!isKeyboardFocus(event.target)) return\n focused = true\n show(true)\n}\n\n// @keyboard @a11y\n// Escape must dismiss a tooltip opened by hover or focus without moving the focus anywhere\n// (WCAG 1.4.13), for a magnifier user whose view it may be covering. It is heard on the\n// DOCUMENT while the tooltip shows, since a hovered tooltip rarely holds the focus, and it\n// is SPENT on it: inside a VDialog the same key would otherwise also be the dialog's close\n// request.\nfunction onDocumentKeydown(event: KeyboardEvent) {\n if (event.key !== 'Escape' || event.defaultPrevented) return\n event.preventDefault()\n hovered = false\n focused = false\n hide()\n}\n\nlet listening = false\nfunction onOpenChange(open: boolean) {\n if (open === listening) return\n listening = open\n if (open) document.addEventListener('keydown', onDocumentKeydown)\n else document.removeEventListener('keydown', onDocumentKeydown)\n}\nonBeforeUnmount(() => onOpenChange(false))\n\ndefineExpose({\n /** Shows the tooltip at once, without the hover delay. */\n show: () => show(true),\n /** Hides the tooltip. */\n close: hide,\n /** The tooltip bubble. */\n el: computed(() => popoverRef.value?.el ?? null),\n})\n</script>\n\n<template>\n <!--\n Close on pointerdown so a tooltip cannot remain above a dialog or menu opened by its\n trigger.\n -->\n <span\n class=\"v-tooltip\"\n @pointerenter=\"onPointerEnter\"\n @pointerleave=\"onPointerLeave\"\n @pointerdown=\"onPointerDown\"\n @focusin=\"onFocusIn\"\n @focusout=\"onFocusOut\"\n >\n <slot :trigger-props=\"{ 'aria-describedby': tooltipId }\" />\n <VPopover\n :id=\"tooltipId\"\n ref=\"popoverRef\"\n mode=\"manual\"\n anchor=\"--tooltip-anchor\"\n :placement=\"placement\"\n bare\n role=\"tooltip\"\n class=\"v-tooltip-panel\"\n @update:open=\"onOpenChange\"\n >\n <slot name=\"content\">{{ text }}</slot>\n </VPopover>\n </span>\n</template>\n\n<style>\n@layer vectis.components {\n /*\n * A wrapper stretched by a parent in `align-items: stretch` would otherwise stretch the\n * trigger with it, where the inline-block form left it at its natural height.\n */\n .v-tooltip {\n display: inline-flex;\n align-items: center;\n anchor-name: --tooltip-anchor;\n /*\n * Confining the name to this subtree is indispensable: a visible panel moves to the top\n * layer, where anchor resolution treats it as coming after the whole document, so without\n * it every tooltip would attach to the last wrapper carrying this name.\n */\n anchor-scope: --tooltip-anchor;\n }\n\n /*\n * Compound the panel classes so chrome overrides remain independent of VPopover stylesheet\n * order, including if bare changes.\n */\n .v-popover-panel.v-tooltip-panel {\n inline-size: max-content;\n max-inline-size: min(\n var(--vectis-control-size-tooltip-max),\n calc(100dvi - var(--vectis-space-8))\n );\n padding: var(--vectis-space-1) var(--vectis-space-2);\n /* The tooltip is painted against the page rather than with it: a dark surface in\n both themes, darker still in the dark one, so it reads as an overlay whatever\n it happens to cover. */\n background: var(--vectis-color-surface-inverse);\n color: var(--vectis-color-text-on-inverse);\n border: none;\n /* The control radius capped at half the height of a ONE-line bubble, the row recipe\n of VSideNavigationItem: a tooltip wraps past its max-inline-size, and under a pill\n override a two-line bubble would otherwise round to half of its own height. */\n border-radius: min(var(--vectis-radius-interactive), calc(0.5lh + var(--vectis-space-1)));\n box-shadow: var(--vectis-shadow-sm);\n font-family: var(--vectis-text-family);\n font-size: var(--vectis-text-caption-size);\n line-height: var(--vectis-text-caption-leading);\n }\n\n /* An outline draws one without moving the layout by a pixel. */\n @media (forced-colors: active) {\n .v-popover-panel.v-tooltip-panel {\n outline: 1px solid CanvasText;\n }\n }\n}\n</style>\n"],"mappings":""}
|
|
@@ -36,9 +36,9 @@ var g = 100, _ = /*@__PURE__*/ s({
|
|
|
36
36
|
function j(e) {
|
|
37
37
|
e.key !== "Escape" || e.defaultPrevented || (e.preventDefault(), S = !1, C = !1, T());
|
|
38
38
|
}
|
|
39
|
-
let M =
|
|
39
|
+
let M = !1;
|
|
40
40
|
function N(e) {
|
|
41
|
-
e !== M
|
|
41
|
+
e !== M && (M = e, e ? document.addEventListener("keydown", j) : document.removeEventListener("keydown", j));
|
|
42
42
|
}
|
|
43
43
|
return c(() => N(!1)), _({
|
|
44
44
|
show: () => w(!0),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VTooltip.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../src/components/VTooltip/VTooltip.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * A short explanation appearing beside an element on hover or keyboard focus. A VPopover in\n * `mode=\"manual\"`, a tooltip having its own rules about when to appear and when to go.\n *\n * Those rules are why there is JS here at all: HTML has no stable way to say \"show this\n * after a delay on hover or focus\", so the component handles the delay, the pointer, the\n * focus, a press on the trigger, and Escape — which must dismiss without moving the focus\n * (WCAG 1.4.13).\n *\n * It stays as long as EITHER the pointer rests on it (the trigger or the bubble) or the\n * keyboard focus is on the trigger, and goes when both have left, after a short grace the\n * pointer needs to cross the gap onto the bubble (WCAG 1.4.13: hoverable, persistent).\n * Two more ways out: Escape, heard from anywhere on the page while it shows and spent on\n * it, and the trigger being PRESSED, for a trigger opening a panel, where the tooltip would\n * otherwise stand over what it just opened.\n *\n * Positioning is pure CSS with no generated id: the wrapper names itself as the anchor and\n * confines that name to its own subtree. The confinement is essential — a shown panel moves\n * to the top layer, which anchor resolution treats as coming after the whole document, so\n * without it every tooltip would attach to the last wrapper named on the page.\n *\n * The wrapper stays HERE rather than going through VPopover's `#trigger`: a tooltip trigger\n * is not a `popovertarget` invoker, which would toggle on click, but an element the panel\n * DESCRIBES.\n */\n\nimport { computed, onBeforeUnmount, ref, useId } from 'vue'\n\nimport VPopover from '../VPopover/VPopover.vue'\n\nimport { useTimer } from '../../composables/useTimer'\nimport { isKeyboardFocus } from '../../utils/focus'\n\n/** Which side of the element the tooltip appears on. */\nexport type TooltipPlacement =\n | 'top'\n | 'top-start'\n | 'top-end'\n | 'bottom'\n | 'bottom-start'\n | 'bottom-end'\n | 'left'\n | 'left-start'\n | 'left-end'\n | 'right'\n | 'right-start'\n | 'right-end'\n\n/** What the described element has to carry: the link to the tooltip that describes it. */\nexport type TooltipTriggerProps = {\n 'aria-describedby': string\n}\n\ninterface TooltipProps {\n /** What the tooltip says. The `#content` slot replaces it when both are given. */\n text?: string\n /**\n * Which side of the element the tooltip appears on. The browser flips it to the\n * opposite side by itself when there is not enough room.\n */\n placement?: TooltipPlacement\n /**\n * How long the pointer must rest on the element before the tooltip appears, in\n * milliseconds. Keyboard focus opens it at once, the intent being in no doubt there,\n * and a delay of 0 disables the wait entirely.\n */\n delay?: number\n}\n\nconst props = withDefaults(defineProps<TooltipProps>(), {\n text: undefined,\n placement: 'top',\n delay: 300,\n})\n\ndefineSlots<{\n /**\n * The element the tooltip describes. Bind the `triggerProps` it receives onto it, which\n * is what ties the two together for assistive technology, and make sure it is something\n * that can take focus, or keyboard users will never see the tooltip.\n */\n default(props: { triggerProps: TooltipTriggerProps }): unknown\n /**\n * Content richer than a plain string: formatting, a keyboard shortcut, an icon. It\n * must stay NON-interactive: the description is flattened to plain text for screen\n * readers, and nothing inside can be reached from the keyboard. Content one can interact\n * with belongs in a panel that stays open, such as VMenu.\n */\n content?(): unknown\n}>()\n\nconst tooltipId = useId()\n// @a11y\n/*\n * The panel is opened and closed by calling it directly rather than through\n * `v-model:open`. Keyboard focus has to open it SYNCHRONOUSLY, and a model would go\n * through VPopover's watcher, hence through a tick. There is nothing lost either way:\n * a tooltip publishes no open state anyone needs to read.\n *\n * Calling twice is harmless — the guards for that live upstream in usePopover.\n */\nconst popoverRef = ref<InstanceType<typeof VPopover> | null>(null)\n\n// The delay before appearing. useTimer is what makes it re-armable and cancels it\n// when the component goes away.\nconst timer = useTimer()\n\n/**\n * How long a tooltip the pointer has left waits before going: the time to cross the gap\n * between the trigger and the bubble, which the margin leaves empty and which no pointer\n * event covers. The bubble is a DOM descendant of the wrapper, so reaching it counts as\n * coming back and cancels the close.\n */\nconst LEAVE_GRACE = 100\n\n// The two reasons it is showing. It goes only once neither holds.\nlet hovered = false\nlet focused = false\n\n// @core\nfunction show(immediate = false) {\n // A delay of 0 runs the callback synchronously — the design system's convention,\n // and what lets keyboard focus share this code path without waiting a tick.\n timer.start(() => popoverRef.value?.show(), immediate ? 0 : props.delay)\n}\n\nfunction hide() {\n timer.cancel()\n popoverRef.value?.close()\n}\n\nfunction onPointerEnter() {\n hovered = true\n show()\n}\n\nfunction onPointerLeave() {\n hovered = false\n if (focused) return\n timer.start(() => popoverRef.value?.close(), LEAVE_GRACE)\n}\n\nfunction onPointerDown() {\n hovered = false\n focused = false\n hide()\n}\n\nfunction onFocusOut() {\n focused = false\n if (!hovered) hide()\n}\n\n// @a11y\n/*\n * The tooltip belongs to the KEYBOARD focus, and a `focusin` says the focus arrived, never\n * how it got there. Three ways in are not a Tab: a click, which the pointer already covers\n * and which the `pointerdown` above has just closed the tooltip for; a tap, which no browser\n * agrees on; and a `focus()` from code — the one a closing panel hands back to the button\n * that opened it, which would raise a tooltip over a page the reader is no longer pointing\n * at. `:focus-visible` is the browser's own answer to the question, the same one VMenu asks\n * to decide where a menu's focus lands.\n *\n * The consequence to know: a tooltip never opens on a tap. Content a touch reader needs\n * cannot live here alone.\n */\nfunction onFocusIn(event: FocusEvent) {\n if (!isKeyboardFocus(event.target)) return\n focused = true\n show(true)\n}\n\n// @keyboard @a11y\n// Escape must dismiss a tooltip opened by hover or focus without moving the focus anywhere\n// (WCAG 1.4.13), for a magnifier user whose view it may be covering. It is heard on the\n// DOCUMENT while the tooltip shows, since a hovered tooltip rarely holds the focus, and it\n// is SPENT on it: inside a VDialog the same key would otherwise also be the dialog's close\n// request.\nfunction onDocumentKeydown(event: KeyboardEvent) {\n if (event.key !== 'Escape' || event.defaultPrevented) return\n event.preventDefault()\n hovered = false\n focused = false\n hide()\n}\n\nconst listening = ref(false)\nfunction onOpenChange(open: boolean) {\n if (open === listening.value) return\n listening.value = open\n if (open) document.addEventListener('keydown', onDocumentKeydown)\n else document.removeEventListener('keydown', onDocumentKeydown)\n}\nonBeforeUnmount(() => onOpenChange(false))\n\ndefineExpose({\n /** Shows the tooltip at once, without the hover delay. */\n show: () => show(true),\n /** Hides the tooltip. */\n close: hide,\n /** The tooltip bubble. */\n el: computed(() => popoverRef.value?.el ?? null),\n})\n</script>\n\n<template>\n <!--\n TRAP — `pointerdown` is what stops a tooltip from surviving the click that acted on its\n trigger, and the case it really covers is a trigger that OPENS something: a menu, a\n dialog, a popover of any kind. The tooltip would then stand over what it opened, and\n none of the other three exits applies — the pointer has not left, the focus has not\n moved out on its own, and nobody pressed Escape.\n\n It looks redundant on Chromium and Firefox, where a click focuses the button and the\n panel then takes the focus, which fires this wrapper's `focusout`. Safari on macOS does\n NOT focus a button on click, so nothing leaves the wrapper and the tooltip stays up,\n behind the panel, until the pointer moves. Delete this line and that is the bug that\n comes back, on one browser only.\n\n It is `pointerdown` and not `click`, so the tooltip is gone BEFORE the panel appears\n rather than overlapping it for a frame. Keyboard activation sends no `pointerdown` at\n all, and needs none: focus moving into the panel closes the tooltip everywhere.\n -->\n <span\n class=\"v-tooltip\"\n @pointerenter=\"onPointerEnter\"\n @pointerleave=\"onPointerLeave\"\n @pointerdown=\"onPointerDown\"\n @focusin=\"onFocusIn\"\n @focusout=\"onFocusOut\"\n >\n <slot :trigger-props=\"{ 'aria-describedby': tooltipId }\" />\n <VPopover\n :id=\"tooltipId\"\n ref=\"popoverRef\"\n mode=\"manual\"\n anchor=\"--tooltip-anchor\"\n :placement=\"placement\"\n bare\n role=\"tooltip\"\n class=\"v-tooltip-panel\"\n @update:open=\"onOpenChange\"\n >\n <slot name=\"content\">{{ text }}</slot>\n </VPopover>\n </span>\n</template>\n\n<style>\n@layer vectis.components {\n /* The wrapper is FLEX and not inline-block, for the same reason as `.v-badge-host`:\n both are inline-level atomic boxes, so the surrounding text flows identically, but\n an inline-block establishes an inline formatting context for the trigger — which\n then sits on a line box and adds the strut's descender to the wrapper's height.\n That is not only a couple of pixels in a flex row: this wrapper is also the ANCHOR,\n so the panel is offset by them too.\n\n `align-items: center` covers what is left. A wrapper stretched by a parent in\n `align-items: stretch` would otherwise stretch the trigger with it, where the\n inline-block form left it at its natural height. */\n .v-tooltip {\n display: inline-flex;\n align-items: center;\n anchor-name: --tooltip-anchor;\n /* Confining the name to this subtree is indispensable: a visible panel moves to\n the top layer, where anchor resolution treats it as coming after the whole\n document, so without it every tooltip would attach to the LAST wrapper carrying\n this name. */\n anchor-scope: --tooltip-anchor;\n }\n\n /* The anchoring itself comes from VPopover, through the `anchor` prop.\n\n The selector is written as a compound of two classes that both land on this same\n element, even though the panel class it guards against — `.v-panel` — is never\n present here, VTooltip asking for a `bare` panel. It is written that way so it cannot\n break the day that prop changes: at equal specificity the winner between two\n component sheets is decided by the order the consumer's bundler happens to\n produce, which is nobody's decision. */\n .v-popover-panel.v-tooltip-panel {\n inline-size: max-content;\n max-inline-size: min(\n var(--vectis-control-size-tooltip-max),\n calc(100dvi - var(--vectis-space-8))\n );\n padding: var(--vectis-space-1) var(--vectis-space-2);\n /* The tooltip is painted against the page rather than with it: a dark surface in\n both themes, darker still in the dark one, so it reads as an overlay whatever\n it happens to cover. */\n background: var(--vectis-color-surface-inverse);\n color: var(--vectis-color-text-on-inverse);\n border: none;\n /* The control radius capped at half the height of a ONE-line bubble, the row recipe\n of VSideNavigationItem: a tooltip wraps past its max-inline-size, and under a pill\n override a two-line bubble would otherwise round to half of its own height. */\n border-radius: min(var(--vectis-radius-interactive), calc(0.5lh + var(--vectis-space-1)));\n box-shadow: var(--vectis-shadow-sm);\n font-family: var(--vectis-text-family);\n font-size: var(--vectis-text-caption-size);\n line-height: var(--vectis-text-caption-leading);\n }\n\n /* Windows forced colors flattens the bubble's background to Canvas and drops its shadow,\n which were its only edge: it would float over the page with no boundary at all. An\n outline draws one without moving the layout by a pixel. */\n @media (forced-colors: active) {\n .v-popover-panel.v-tooltip-panel {\n outline: 1px solid CanvasText;\n }\n }\n}\n</style>\n"],"mappings":";;;;;AAkHA,IAAM,IAAc;;;;;;;;EA5CpB,IAAM,IAAQ,GAsBR,IAAY,EAAM,GAUlB,IAAa,EAA0C,IAAI,GAI3D,IAAQ,EAAS,GAWnB,IAAU,IACV,IAAU;EAGd,SAAS,EAAK,IAAY,IAAO;GAG/B,EAAM,YAAY,EAAW,OAAO,KAAK,GAAG,IAAY,IAAI,EAAM,KAAK;EACzE;EAEA,SAAS,IAAO;GAEd,AADA,EAAM,OAAO,GACb,EAAW,OAAO,MAAM;EAC1B;EAEA,SAAS,IAAiB;GAExB,AADA,IAAU,IACV,EAAK;EACP;EAEA,SAAS,IAAiB;GACxB,IAAU,IACN,MACJ,EAAM,YAAY,EAAW,OAAO,MAAM,GAAG,CAAW;EAC1D;EAEA,SAAS,IAAgB;GAGvB,AAFA,IAAU,IACV,IAAU,IACV,EAAK;EACP;EAEA,SAAS,IAAa;GAEpB,AADA,IAAU,IACL,KAAS,EAAK;EACrB;EAeA,SAAS,EAAU,GAAmB;GAC/B,EAAgB,EAAM,MAAM,MACjC,IAAU,IACV,EAAK,EAAI;EACX;EAQA,SAAS,EAAkB,GAAsB;GAC3C,EAAM,QAAQ,YAAY,EAAM,qBACpC,EAAM,eAAe,GACrB,IAAU,IACV,IAAU,IACV,EAAK;EACP;EAEA,IAAM,IAAY,EAAI,EAAK;EAC3B,SAAS,EAAa,GAAe;GAC/B,MAAS,EAAU,UACvB,EAAU,QAAQ,GACd,IAAM,SAAS,iBAAiB,WAAW,CAAiB,IAC3D,SAAS,oBAAoB,WAAW,CAAiB;EAChE;SACA,QAAsB,EAAa,EAAK,CAAC,GAEzC,EAAa;GAEX,YAAY,EAAK,EAAI;GAErB,OAAO;GAEP,IAAI,QAAe,EAAW,OAAO,MAAM,IAAI;EACjD,CAAC,mBAqBC,EAsBO,QAAA;GArBL,OAAM;GACL,gBAAc;GACd,gBAAc;GACd,eAAa;GACb,WAAS;GACT,YAAU;MAEX,EAA2D,EAAA,QAAA,WAAA,EAApD,cAAa,EAAA,oBAAwB,EAAA,CAAA,EAAS,EAAA,CAAA,GACrD,EAYW,GAAA;GAXR,IAAI,EAAA,CAAA;YACD;GAAJ,KAAI;GACJ,MAAK;GACL,QAAO;GACN,WAAW,EAAA;GACZ,MAAA;GACA,MAAK;GACL,OAAM;GACL,iBAAa;;oBAEwB,CAAtC,EAAsC,EAAA,QAAA,WAAA,CAAA,SAAA,CAAA,EAAA,EAAd,EAAA,IAAI,GAAA,CAAA,CAAA,CAAA,CAAA,CAAA"}
|
|
1
|
+
{"version":3,"file":"VTooltip.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../src/components/VTooltip/VTooltip.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * Use a manual VPopover because tooltip hover, focus, delay and Escape policies differ from\n * native light dismissal. Pointer movement may require keeping the bubble open.\n */\n\nimport { computed, onBeforeUnmount, ref, useId } from 'vue'\n\nimport VPopover from '../VPopover/VPopover.vue'\n\nimport { useTimer } from '../../composables/useTimer'\nimport { isKeyboardFocus } from '../../utils/focus'\n\n/** Which side of the element the tooltip appears on. */\nexport type TooltipPlacement =\n | 'top'\n | 'top-start'\n | 'top-end'\n | 'bottom'\n | 'bottom-start'\n | 'bottom-end'\n | 'left'\n | 'left-start'\n | 'left-end'\n | 'right'\n | 'right-start'\n | 'right-end'\n\n/** What the described element has to carry: the link to the tooltip that describes it. */\nexport type TooltipTriggerProps = {\n 'aria-describedby': string\n}\n\ninterface TooltipProps {\n /** What the tooltip says. The `#content` slot replaces it when both are given. */\n text?: string\n /**\n * Which side of the element the tooltip appears on. The browser flips it to the\n * opposite side by itself when there is not enough room.\n */\n placement?: TooltipPlacement\n /** How long the pointer must rest on the element before the tooltip appears, in milliseconds. */\n delay?: number\n}\n\nconst props = withDefaults(defineProps<TooltipProps>(), {\n text: undefined,\n placement: 'top',\n delay: 300,\n})\n\ndefineSlots<{\n /** The element the tooltip describes. */\n default(props: { triggerProps: TooltipTriggerProps }): unknown\n /** Content richer than a plain string: formatting, a keyboard shortcut, an icon. */\n content?(): unknown\n}>()\n\nconst tooltipId = useId()\n// @a11y\n/*\n * Keyboard focus has to open it synchronously, and a model would go through VPopover's watcher,\n * hence through a tick. There is nothing lost either way: a tooltip publishes no open state\n * anyone needs to read.\n */\nconst popoverRef = ref<InstanceType<typeof VPopover> | null>(null)\n\n// The delay before appearing. useTimer is what makes it re-armable and cancels it\n// when the component goes away.\nconst timer = useTimer()\n\n/**\n * How long a tooltip the pointer has left waits before going: the time to cross the gap between\n * the trigger and the bubble, which the margin leaves empty and which no pointer event covers.\n */\nconst LEAVE_GRACE = 100\n\nlet hovered = false\nlet focused = false\n\n// @core\nfunction show(immediate = false) {\n // A delay of 0 runs the callback synchronously; the design system's convention, and what lets\n // keyboard focus share this code path without waiting a tick.\n timer.start(() => popoverRef.value?.show(), immediate ? 0 : props.delay)\n}\n\nfunction hide() {\n timer.cancel()\n popoverRef.value?.close()\n}\n\nfunction onPointerEnter() {\n hovered = true\n show()\n}\n\nfunction onPointerLeave() {\n hovered = false\n if (focused) return\n timer.start(() => popoverRef.value?.close(), LEAVE_GRACE)\n}\n\nfunction onPointerDown() {\n hovered = false\n focused = false\n hide()\n}\n\nfunction onFocusOut() {\n focused = false\n if (!hovered) hide()\n}\n\n// @a11y\n/*\n * The tooltip belongs to the KEYBOARD focus, and a `focusin` says the focus arrived, never how\n * it got there. `:focus-visible` is the browser's own answer to the question, the same one\n * VMenu asks to decide where a menu's focus lands.\n */\nfunction onFocusIn(event: FocusEvent) {\n if (!isKeyboardFocus(event.target)) return\n focused = true\n show(true)\n}\n\n// @keyboard @a11y\n// Escape must dismiss a tooltip opened by hover or focus without moving the focus anywhere\n// (WCAG 1.4.13), for a magnifier user whose view it may be covering. It is heard on the\n// DOCUMENT while the tooltip shows, since a hovered tooltip rarely holds the focus, and it\n// is SPENT on it: inside a VDialog the same key would otherwise also be the dialog's close\n// request.\nfunction onDocumentKeydown(event: KeyboardEvent) {\n if (event.key !== 'Escape' || event.defaultPrevented) return\n event.preventDefault()\n hovered = false\n focused = false\n hide()\n}\n\nlet listening = false\nfunction onOpenChange(open: boolean) {\n if (open === listening) return\n listening = open\n if (open) document.addEventListener('keydown', onDocumentKeydown)\n else document.removeEventListener('keydown', onDocumentKeydown)\n}\nonBeforeUnmount(() => onOpenChange(false))\n\ndefineExpose({\n /** Shows the tooltip at once, without the hover delay. */\n show: () => show(true),\n /** Hides the tooltip. */\n close: hide,\n /** The tooltip bubble. */\n el: computed(() => popoverRef.value?.el ?? null),\n})\n</script>\n\n<template>\n <!--\n Close on pointerdown so a tooltip cannot remain above a dialog or menu opened by its\n trigger.\n -->\n <span\n class=\"v-tooltip\"\n @pointerenter=\"onPointerEnter\"\n @pointerleave=\"onPointerLeave\"\n @pointerdown=\"onPointerDown\"\n @focusin=\"onFocusIn\"\n @focusout=\"onFocusOut\"\n >\n <slot :trigger-props=\"{ 'aria-describedby': tooltipId }\" />\n <VPopover\n :id=\"tooltipId\"\n ref=\"popoverRef\"\n mode=\"manual\"\n anchor=\"--tooltip-anchor\"\n :placement=\"placement\"\n bare\n role=\"tooltip\"\n class=\"v-tooltip-panel\"\n @update:open=\"onOpenChange\"\n >\n <slot name=\"content\">{{ text }}</slot>\n </VPopover>\n </span>\n</template>\n\n<style>\n@layer vectis.components {\n /*\n * A wrapper stretched by a parent in `align-items: stretch` would otherwise stretch the\n * trigger with it, where the inline-block form left it at its natural height.\n */\n .v-tooltip {\n display: inline-flex;\n align-items: center;\n anchor-name: --tooltip-anchor;\n /*\n * Confining the name to this subtree is indispensable: a visible panel moves to the top\n * layer, where anchor resolution treats it as coming after the whole document, so without\n * it every tooltip would attach to the last wrapper carrying this name.\n */\n anchor-scope: --tooltip-anchor;\n }\n\n /*\n * Compound the panel classes so chrome overrides remain independent of VPopover stylesheet\n * order, including if bare changes.\n */\n .v-popover-panel.v-tooltip-panel {\n inline-size: max-content;\n max-inline-size: min(\n var(--vectis-control-size-tooltip-max),\n calc(100dvi - var(--vectis-space-8))\n );\n padding: var(--vectis-space-1) var(--vectis-space-2);\n /* The tooltip is painted against the page rather than with it: a dark surface in\n both themes, darker still in the dark one, so it reads as an overlay whatever\n it happens to cover. */\n background: var(--vectis-color-surface-inverse);\n color: var(--vectis-color-text-on-inverse);\n border: none;\n /* The control radius capped at half the height of a ONE-line bubble, the row recipe\n of VSideNavigationItem: a tooltip wraps past its max-inline-size, and under a pill\n override a two-line bubble would otherwise round to half of its own height. */\n border-radius: min(var(--vectis-radius-interactive), calc(0.5lh + var(--vectis-space-1)));\n box-shadow: var(--vectis-shadow-sm);\n font-family: var(--vectis-text-family);\n font-size: var(--vectis-text-caption-size);\n line-height: var(--vectis-text-caption-leading);\n }\n\n /* An outline draws one without moving the layout by a pixel. */\n @media (forced-colors: active) {\n .v-popover-panel.v-tooltip-panel {\n outline: 1px solid CanvasText;\n }\n }\n}\n</style>\n"],"mappings":";;;;;AA2EA,IAAM,IAAc;;;;;;;;EA9BpB,IAAM,IAAQ,GAaR,IAAY,EAAM,GAOlB,IAAa,EAA0C,IAAI,GAI3D,IAAQ,EAAS,GAQnB,IAAU,IACV,IAAU;EAGd,SAAS,EAAK,IAAY,IAAO;GAG/B,EAAM,YAAY,EAAW,OAAO,KAAK,GAAG,IAAY,IAAI,EAAM,KAAK;EACzE;EAEA,SAAS,IAAO;GAEd,AADA,EAAM,OAAO,GACb,EAAW,OAAO,MAAM;EAC1B;EAEA,SAAS,IAAiB;GAExB,AADA,IAAU,IACV,EAAK;EACP;EAEA,SAAS,IAAiB;GACxB,IAAU,IACN,MACJ,EAAM,YAAY,EAAW,OAAO,MAAM,GAAG,CAAW;EAC1D;EAEA,SAAS,IAAgB;GAGvB,AAFA,IAAU,IACV,IAAU,IACV,EAAK;EACP;EAEA,SAAS,IAAa;GAEpB,AADA,IAAU,IACL,KAAS,EAAK;EACrB;EAQA,SAAS,EAAU,GAAmB;GAC/B,EAAgB,EAAM,MAAM,MACjC,IAAU,IACV,EAAK,EAAI;EACX;EAQA,SAAS,EAAkB,GAAsB;GAC3C,EAAM,QAAQ,YAAY,EAAM,qBACpC,EAAM,eAAe,GACrB,IAAU,IACV,IAAU,IACV,EAAK;EACP;EAEA,IAAI,IAAY;EAChB,SAAS,EAAa,GAAe;GAC/B,MAAS,MACb,IAAY,GACR,IAAM,SAAS,iBAAiB,WAAW,CAAiB,IAC3D,SAAS,oBAAoB,WAAW,CAAiB;EAChE;SACA,QAAsB,EAAa,EAAK,CAAC,GAEzC,EAAa;GAEX,YAAY,EAAK,EAAI;GAErB,OAAO;GAEP,IAAI,QAAe,EAAW,OAAO,MAAM,IAAI;EACjD,CAAC,mBAQC,EAsBO,QAAA;GArBL,OAAM;GACL,gBAAc;GACd,gBAAc;GACd,eAAa;GACb,WAAS;GACT,YAAU;MAEX,EAA2D,EAAA,QAAA,WAAA,EAApD,cAAa,EAAA,oBAAwB,EAAA,CAAA,EAAS,EAAA,CAAA,GACrD,EAYW,GAAA;GAXR,IAAI,EAAA,CAAA;YACD;GAAJ,KAAI;GACJ,MAAK;GACL,QAAO;GACN,WAAW,EAAA;GACZ,MAAA;GACA,MAAK;GACL,OAAM;GACL,iBAAa;;oBAEwB,CAAtC,EAAsC,EAAA,QAAA,WAAA,CAAA,SAAA,CAAA,EAAA,EAAd,EAAA,IAAI,GAAA,CAAA,CAAA,CAAA,CAAA,CAAA"}
|
|
@@ -16,16 +16,12 @@ interface TypographyProps {
|
|
|
16
16
|
*/
|
|
17
17
|
as?: string;
|
|
18
18
|
/**
|
|
19
|
-
* The colour of the text. `default` sets none at all, so the text inherits from
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* The colour of the text. `default` sets none at all, so the text inherits from whatever
|
|
20
|
+
* surrounds it, which lets the same component be used on an inverted surface or inside a
|
|
21
|
+
* coloured toast.
|
|
22
22
|
*/
|
|
23
23
|
tone?: TypographyTone;
|
|
24
|
-
/**
|
|
25
|
-
* Cuts the text to one line and ends it with an ellipsis. The element needs a
|
|
26
|
-
* width to be cut against (being a block, or a flex item), otherwise there is
|
|
27
|
-
* nothing to overflow and the text simply stays whole.
|
|
28
|
-
*/
|
|
24
|
+
/** Cuts the text to one line and ends it with an ellipsis. */
|
|
29
25
|
truncate?: boolean;
|
|
30
26
|
}
|
|
31
27
|
type __VLS_Slots = {
|