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":"VMenuPanel.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../src/components/VMenu/VMenuPanel.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @a11y @keyboard @core\n/**\n * The panel a menu is drawn in. Internal to VMenu, and the same component serves both\n * levels — the root menu and every submenu.\n *\n * It rests on `popover=\"auto\"`, which gives it light dismiss, positioning against its\n * invoker, and — a submenu being rendered inside its parent panel — a native stack the\n * browser closes from the outside in. Its look comes from the shared `.v-panel`.\n *\n * It carries `tabindex=\"-1\"` so it can hold the focus itself, which it does whenever the\n * menu was opened by POINTER: no command is singled out, but the keyboard still has\n * somewhere to arrive, everything below being listened for on this element.\n *\n * The JS is the ARIA menu keyboard, which the platform does not provide:\n *\n * - arrows and Home/End move focus item to item, CONFINED to the panel the focus is in — a\n * keystroke inside a submenu also bubbles through every panel containing it, hence the\n * guard on where the event came from;\n * - Escape closes the CURRENT LEVEL only, as does ArrowLeft in a submenu, handing focus\n * back to the item that opened it;\n * - Tab closes the whole menu, a menu not being something one tabs through.\n */\n\nimport { computed, inject, ref } from 'vue'\n\nimport { usePopover } from '../../composables/usePopover'\nimport { menuInvoker, menuKey } from './context'\nimport type { MenuPanelPlacement, MenuSize } from './context'\nimport { arrowNavigate } from '../../utils/arrowNav'\nimport { isRtl } from '../../utils/direction'\nimport { cssSize } from '../../utils/css'\n\ninterface MenuPanelProps {\n /** The panel's id, set by whoever owns it. It is what the trigger points at. */\n id: string\n /** Where the panel opens relative to whatever opened it. */\n placement?: MenuPanelPlacement\n /**\n * Marks this panel as a submenu, which enables the left arrow as a way back and keeps\n * the row-height settings off it, so that it inherits its parent's instead.\n */\n submenu?: boolean\n /**\n * The row height, set by the ROOT panel only. Submenus receive no value and inherit\n * it through CSS instead.\n */\n size?: MenuSize\n /** The reduced density, again set by the ROOT panel only and inherited through CSS. */\n compact?: boolean\n /**\n * An explicit width, set by the ROOT panel only. Submenus render no width attribute\n * and keep the default one, so an inherited value cannot reach them.\n */\n width?: number | string\n /**\n * Stops the panel from being narrower than its trigger, set by the ROOT panel only:\n * a submenu's trigger is a menu item, not a control worth matching.\n */\n matchTrigger?: boolean\n}\n\nconst props = withDefaults(defineProps<MenuPanelProps>(), {\n placement: 'bottom-start',\n submenu: false,\n size: undefined,\n compact: false,\n width: undefined,\n matchTrigger: false,\n})\n\nconst emit = defineEmits<{\n /**\n * The panel opened or closed. It is reported by the browser, so it also covers a\n * dismissal nobody asked for in code.\n */\n toggle: [open: boolean]\n}>()\n\ndefineSlots<{\n /** The contents of the panel: VMenuItem, VMenuGroup and VMenuSeparator. */\n default(): unknown\n}>()\n\nconst panelEl = ref<HTMLElement | null>(null)\n// The open state, the guards keeping the calls idempotent, and the option that gives\n// a programmatically opened submenu its anchor, all live in usePopover.\nconst { shown, syncShown, show, hide } = usePopover(panelEl)\n\n// Never absent: this panel is internal, rendered only by VMenu and by the VMenuItems inside\n// one, and VMenu provides the key before either exists.\nconst menu = inject(menuKey)!\n\nfunction onToggle(event: Event) {\n syncShown(event)\n emit('toggle', shown.value)\n}\n\n// @a11y @keyboard\n/**\n * The items the keyboard may move to. Two exclusions matter: `:disabled` only ever\n * matches a `<button>`, so an inert link is recognized by its `aria-disabled`\n * instead; and the final filter keeps only the items of THIS panel, since an open\n * submenu's items are DOM descendants of it and would otherwise join the list.\n */\nfunction items(): HTMLElement[] {\n const panel = panelEl.value\n if (!panel) return []\n return [\n ...panel.querySelectorAll<HTMLElement>(\n '[role=\"menuitem\"]:not(:disabled):not([aria-disabled=\"true\"])',\n ),\n ].filter((el) => el.closest('[role=\"menu\"]') === panel)\n}\n\n// @a11y\n/** Puts the focus on the first item the keyboard can reach, when the menu opens. */\nfunction focusFirst() {\n items()[0]?.focus()\n}\n\n// @a11y\n/**\n * Puts the focus on the panel itself, singling out no command. This is where a menu\n * opened with a pointer lands: the reader has not asked for a first choice, but the\n * keyboard still has to work from the very next keystroke — and every key below is\n * listened for on this element, so leaving the focus outside would silence all of them.\n */\nfunction focusPanel() {\n panelEl.value?.focus()\n}\n\n// @keyboard @a11y\nfunction onKeydown(event: KeyboardEvent) {\n const panel = panelEl.value\n // A keystroke inside a submenu passes through every panel containing it on its way\n // up, so only the panel the focused item DIRECTLY belongs to acts on it.\n if (!panel || (event.target as Element).closest('[role=\"menu\"]') !== panel) return\n // A key held with a modifier is the browser's: Alt+Left is Back.\n if (event.altKey || event.ctrlKey || event.metaKey) return\n\n if (event.key === 'Tab') {\n menu.closeAll()\n return\n }\n // The arrow pointing BACK at the parent closes a submenu: the right one in a\n // right-to-left page, where the submenu opened on the left.\n const back = isRtl(panel) ? 'ArrowRight' : 'ArrowLeft'\n if (event.key === 'Escape' || (props.submenu && event.key === back)) {\n // Escape closes THIS level and no more. The `preventDefault` matters: left to\n // itself the browser would close the popover on its own, without handing the\n // focus back to the item that opened it — and our own hide() would already have\n // closed it anyway. At the top level, VMenu is what returns the focus to the\n // trigger.\n event.preventDefault()\n hide()\n if (props.submenu) menuInvoker(props.id)?.focus()\n return\n }\n // Coming from the panel itself — a menu opened with a pointer, where no command is\n // singled out — the up arrow goes to the LAST item, entering the list from its far\n // end the way a menu is expected to. `utils/arrowNav` starts any arrow at the\n // beginning when nothing in the list has the focus, which is the right answer for the\n // tabs, the pages and the toggle group it also serves: it is this menu that has a\n // convention of its own, so it is handled here rather than in the shared helper.\n // The case only exists BECAUSE of the pointer opening: reached from an item, the\n // arrow never lands here.\n if (event.key === 'ArrowUp' && document.activeElement === panel) {\n const list = items()\n if (list.length > 0) {\n event.preventDefault()\n list[list.length - 1]?.focus()\n return\n }\n }\n // The movement itself is the shared one from `utils/arrowNav`, but it is handed OUR\n // list rather than letting it discover the items: ours also drops the disabled\n // links and stays inside this panel, neither of which the generic selector does.\n arrowNavigate(event, panel, items, { vertical: true })\n}\n\n// The only thing set inline is an explicit width, when the prop asks for one.\nconst resolvedWidth = computed(() => cssSize(props.width) || undefined)\nconst panelStyle = computed(() =>\n resolvedWidth.value ? { '--menu-width': resolvedWidth.value } : undefined,\n)\n\n// Internal to VMenu, which drives the panel from outside: opening has to be synchronous,\n// and where the focus lands depends on whether a pointer or the keyboard asked.\ndefineExpose({\n /** Opens the panel. Pass the invoker, or the popover has no anchor to position against. */\n show,\n /** Closes it. */\n close: hide,\n /** Whether it is open, as the browser last reported it. VMenu's model is checked against it. */\n shown,\n /** Puts the focus on the first reachable item: the keyboard's way in. */\n focusFirst,\n /** Puts the focus on the panel itself: the pointer's way in, singling out no command. */\n focusPanel,\n /** The panel element, which is the popover. */\n el: panelEl,\n})\n</script>\n\n<template>\n <div\n :id=\"id\"\n ref=\"panelEl\"\n popover=\"auto\"\n role=\"menu\"\n tabindex=\"-1\"\n class=\"v-overlay v-panel v-menu v-floating\"\n :class=\"{\n /*\n * The size class goes on the ROOT panel only. It sets the whole `--control-*`\n * family — height, paddings, gap, type, icon context — and the submenus, being\n * DOM descendants, inherit every one of them.\n *\n * Adding it to the submenus as well would BREAK them: the class recomputes the\n * height from the base one without knowing about the compact attribute, which\n * submenus do not carry, so a compact menu's submenus would silently snap back\n * to full height.\n */\n 'v-control': !submenu,\n }\"\n :data-placement=\"placement\"\n :data-size=\"size\"\n :data-compact=\"compact ? '' : undefined\"\n :data-width=\"resolvedWidth ? '' : undefined\"\n :data-match-trigger=\"matchTrigger ? '' : undefined\"\n :style=\"panelStyle\"\n @beforetoggle=\"syncShown\"\n @toggle=\"onToggle\"\n @keydown=\"onKeydown\"\n >\n <slot />\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n /* The panel's surface, border, shadow and inner rhythm come from the shared\n `.v-panel` class, and its dimensions from the `v-control` class the template sets\n on the root panel — there is no size table here, the rows reading the inherited\n variables directly. Only what is specific to a dropdown menu stays below. */\n .v-menu {\n min-inline-size: var(--vectis-control-size-menu-min);\n max-inline-size: min(var(--vectis-control-size-menu-max), calc(100dvi - var(--vectis-space-8)));\n }\n\n /* The panel is focusable, but only ever from code — its `tabindex` is -1, so the Tab\n key never brings anyone here. It holds the focus when the menu was opened with a\n pointer, and a ring drawn around the whole panel would then read as the reader\n having landed on something, which is the opposite of what that state means: no\n command is singled out. The rows keep their own highlight, `.v-menu-item:focus`. */\n .v-menu:focus {\n outline: none;\n }\n\n /* Pulls a submenu up so that its first item lines up with the item that opened it,\n by exactly the panel's own padding plus its border. */\n .v-menu .v-menu[data-placement='right-start'] {\n margin-block-start: calc(-1 * (var(--vectis-space-1) + 1px));\n }\n\n /* An explicit width, which only the ROOT panel ever renders: a submenu sets no such\n attribute, so the variable it inherits stays inert there. The ceiling that keeps\n a panel inside the viewport is untouched. */\n .v-menu[data-width] {\n min-inline-size: 0;\n inline-size: var(--menu-width);\n }\n\n /* The panel may not be narrower than its trigger. This works with no anchor name of\n any kind, because the button that opened the panel is already its implicit anchor\n — VCombobox and VTimeInput, anchored to a text input, have to name theirs. Like\n the width above, only the ROOT panel renders it.\n\n It is placed AFTER the width rule, at equal specificity, so that when both are\n given the floor wins. A trigger wider than the ceiling therefore widens the panel\n past it, a minimum width beating a maximum one — which is intended. */\n .v-menu[data-match-trigger] {\n min-inline-size: anchor-size(width);\n }\n}\n</style>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA8DA,IAAM,IAAQ,GASR,IAAO,GAaP,IAAU,EAAwB,IAAI,GAGtC,EAAE,UAAO,cAAW,SAAM,YAAS,EAAW,CAAO,GAIrD,IAAO,EAAO,CAAO;EAE3B,SAAS,EAAS,GAAc;GAE9B,AADA,EAAU,CAAK,GACf,EAAK,UAAU,EAAM,KAAK;EAC5B;EASA,SAAS,IAAuB;GAC9B,IAAM,IAAQ,EAAQ;GAEtB,OADK,IACE,CACL,GAAG,EAAM,iBACP,kEACF,CACF,CAAC,CAAC,QAAQ,MAAO,EAAG,QAAQ,iBAAe,MAAM,CAAK,IALnC,CAAC;EAMtB;EAIA,SAAS,IAAa;GACpB,EAAM,CAAC,CAAC,EAAE,EAAE,MAAM;EACpB;EASA,SAAS,IAAa;GACpB,EAAQ,OAAO,MAAM;EACvB;EAGA,SAAS,EAAU,GAAsB;GACvC,IAAM,IAAQ,EAAQ;GAKtB,IAFI,CAAC,KAAU,EAAM,OAAmB,QAAQ,iBAAe,MAAM,KAEjE,EAAM,UAAU,EAAM,WAAW,EAAM,SAAS;GAEpD,IAAI,EAAM,QAAQ,OAAO;IACvB,EAAK,SAAS;IACd;GACF;GAGA,IAAM,IAAO,EAAM,CAAK,IAAI,eAAe;GAC3C,IAAI,EAAM,QAAQ,YAAa,EAAM,WAAW,EAAM,QAAQ,GAAO;IAQnE,AAFA,EAAM,eAAe,GACrB,EAAK,GACD,EAAM,WAAS,EAAY,EAAM,EAAE,CAAC,EAAE,MAAM;IAChD;GACF;GASA,IAAI,EAAM,QAAQ,aAAa,SAAS,kBAAkB,GAAO;IAC/D,IAAM,IAAO,EAAM;IACnB,IAAI,EAAK,SAAS,GAAG;KAEnB,AADA,EAAM,eAAe,GACrB,EAAK,EAAK,SAAS,EAAE,EAAE,MAAM;KAC7B;IACF;GACF;GAIA,EAAc,GAAO,GAAO,GAAO,EAAE,UAAU,GAAK,CAAC;EACvD;EAGA,IAAM,IAAgB,QAAe,EAAQ,EAAM,KAAK,KAAK,KAAA,CAAS,GAChE,IAAa,QACjB,EAAc,QAAQ,EAAE,gBAAgB,EAAc,MAAM,IAAI,KAAA,CAClE;SAIA,EAAa;GAEX;GAEA,OAAO;GAEP;GAEA;GAEA;GAEA,IAAI;EACN,CAAC,mBAIC,EA+BM,OAAA;GA9BH,IAAI,EAAA;YACD;GAAJ,KAAI;GACJ,SAAQ;GACR,MAAK;GACL,UAAS;GACT,OAAK,EAAA,CAAC,uCAAqC,EAAA,aAAA,CACygB,EAAA,QAAA,CAAA,CAAA;GAanjB,kBAAgB,EAAA;GAChB,aAAW,EAAA;GACX,gBAAc,EAAA,UAAO,KAAQ,KAAA;GAC7B,cAAY,EAAA,QAAa,KAAQ,KAAA;GACjC,sBAAoB,EAAA,eAAY,KAAQ,KAAA;GACxC,OAAK,EAAE,EAAA,KAAU;GACjB,gBAAY,AAAA,EAAA,QAAA,GAAA,MAAE,EAAA,CAAA,KAAA,EAAA,CAAA,CAAA,CAAA,GAAA,CAAA;GACN;GACC;MAEV,EAAQ,EAAA,QAAA,SAAA,CAAA,GAAA,IAAA,CAAA"}
|
|
1
|
+
{"version":3,"file":"VMenuPanel.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../src/components/VMenu/VMenuPanel.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @a11y @keyboard @core\n/**\n * Native auto popovers form the submenu stack. JavaScript maintains open models, roving focus\n * and explicit source invokers when opening imperatively.\n */\n\nimport { computed, inject, ref } from 'vue'\n\nimport { usePopover } from '../../composables/usePopover'\nimport { menuInvoker, menuKey } from './context'\nimport type { MenuPanelPlacement, MenuSize } from './context'\nimport { arrowNavigate } from '../../utils/arrowNav'\nimport { isRtl } from '../../utils/direction'\nimport { cssSize } from '../../utils/css'\n\ninterface MenuPanelProps {\n /** The panel's id, set by whoever owns it. It is what the trigger points at. */\n id: string\n /** Where the panel opens relative to whatever opened it. */\n placement?: MenuPanelPlacement\n /**\n * Marks this panel as a submenu, which enables the left arrow as a way back and keeps\n * the row-height settings off it, so that it inherits its parent's instead.\n */\n submenu?: boolean\n /**\n * The row height, set by the ROOT panel only. Submenus receive no value and inherit\n * it through CSS instead.\n */\n size?: MenuSize\n /** The reduced density, again set by the ROOT panel only and inherited through CSS. */\n compact?: boolean\n /**\n * An explicit width, set by the ROOT panel only. Submenus render no width attribute\n * and keep the default one, so an inherited value cannot reach them.\n */\n width?: number | string\n /**\n * Stops the panel from being narrower than its trigger, set by the ROOT panel only:\n * a submenu's trigger is a menu item, not a control worth matching.\n */\n matchTrigger?: boolean\n}\n\nconst props = withDefaults(defineProps<MenuPanelProps>(), {\n placement: 'bottom-start',\n submenu: false,\n size: undefined,\n compact: false,\n width: undefined,\n matchTrigger: false,\n})\n\nconst emit = defineEmits<{\n /**\n * The panel opened or closed. It is reported by the browser, so it also covers a\n * dismissal nobody asked for in code.\n */\n toggle: [open: boolean]\n}>()\n\ndefineSlots<{\n /** The contents of the panel: VMenuItem, VMenuGroup and VMenuSeparator. */\n default(): unknown\n}>()\n\nconst panelEl = ref<HTMLElement | null>(null)\n// The open state, the guards keeping the calls idempotent, and the option that gives\n// a programmatically opened submenu its anchor, all live in usePopover.\nconst { shown, syncShown, show, hide } = usePopover(panelEl)\n\n// Never absent: this panel is internal, rendered only by VMenu and by the VMenuItems inside\n// one, and VMenu provides the key before either exists.\nconst menu = inject(menuKey)!\n\nfunction onToggle(event: Event) {\n syncShown(event)\n emit('toggle', shown.value)\n}\n\n// @a11y @keyboard\n/**\n * The items the keyboard may move to. Two exclusions matter: `:disabled` only ever matches a\n * `<button>`, so an inert link is recognized by its `aria-disabled` instead; and the final\n * filter keeps only the items of this panel, since an open submenu's items are DOM descendants\n * of it and would otherwise join the list.\n */\nfunction items(): HTMLElement[] {\n const panel = panelEl.value\n if (!panel) return []\n return [\n ...panel.querySelectorAll<HTMLElement>(\n '[role=\"menuitem\"]:not(:disabled):not([aria-disabled=\"true\"])',\n ),\n ].filter((el) => el.closest('[role=\"menu\"]') === panel)\n}\n\n// @a11y\n/** Puts the focus on the first item the keyboard can reach, when the menu opens. */\nfunction focusFirst() {\n items()[0]?.focus()\n}\n\n// @a11y\n/** Puts the focus on the panel itself, singling out no command. */\nfunction focusPanel() {\n panelEl.value?.focus()\n}\n\n// @keyboard @a11y\nfunction onKeydown(event: KeyboardEvent) {\n const panel = panelEl.value\n // A keystroke inside a submenu passes through every panel containing it on its way\n // up, so only the panel the focused item DIRECTLY belongs to acts on it.\n if (!panel || (event.target as Element).closest('[role=\"menu\"]') !== panel) return\n if (event.altKey || event.ctrlKey || event.metaKey) return\n\n if (event.key === 'Tab') {\n menu.closeAll()\n return\n }\n // The arrow pointing BACK at the parent closes a submenu: the right one in a\n // right-to-left page, where the submenu opened on the left.\n const back = isRtl(panel) ? 'ArrowRight' : 'ArrowLeft'\n if (event.key === 'Escape' || (props.submenu && event.key === back)) {\n // The `preventDefault` matters: left to itself the browser would close the popover on its\n // own, without handing the focus back to the item that opened it; and our own hide() would\n // already have closed it anyway. At the top level, VMenu is what returns the focus to the\n // trigger.\n event.preventDefault()\n hide()\n if (props.submenu) menuInvoker(props.id)?.focus()\n return\n }\n // `utils/arrowNav` starts any arrow at the beginning when nothing in the list has the focus,\n // which is the right answer for the tabs, the pages and the toggle group it also serves: it\n // is this menu that has a convention of its own, so it is handled here rather than in the\n // shared helper. The case only exists BECAUSE of the pointer opening: reached from an item,\n // the arrow never lands here.\n if (event.key === 'ArrowUp' && document.activeElement === panel) {\n const list = items()\n if (list.length > 0) {\n event.preventDefault()\n list[list.length - 1]?.focus()\n return\n }\n }\n // The movement itself is the shared one from `utils/arrowNav`, but it is handed OUR\n // list rather than letting it discover the items: ours also drops the disabled\n // links and stays inside this panel, neither of which the generic selector does.\n arrowNavigate(event, panel, items, { vertical: true })\n}\n\nconst resolvedWidth = computed(() => cssSize(props.width))\n\n// Internal to VMenu, which drives the panel from outside: opening has to be synchronous,\n// and where the focus lands depends on whether a pointer or the keyboard asked.\ndefineExpose({\n /** Opens the panel. Pass the invoker, or the popover has no anchor to position against. */\n show,\n /** Closes it. */\n close: hide,\n /** Whether it is open, as the browser last reported it. VMenu's model is checked against it. */\n shown,\n /** Puts the focus on the first reachable item: the keyboard's way in. */\n focusFirst,\n /** Puts the focus on the panel itself: the pointer's way in, singling out no command. */\n focusPanel,\n /** The panel element, which is the popover. */\n el: panelEl,\n})\n</script>\n\n<template>\n <div\n :id=\"id\"\n ref=\"panelEl\"\n popover=\"auto\"\n role=\"menu\"\n tabindex=\"-1\"\n class=\"v-overlay v-panel v-menu v-floating\"\n :class=\"{\n /*\n * It sets the whole `--control-*` family; height, paddings, gap, type, icon context; and\n * the submenus, being DOM descendants, inherit every one of them. Adding it to the\n * submenus as well would BREAK them: the class recomputes the height from the base one\n * without knowing about the compact attribute, which submenus do not carry, so a compact\n * menu's submenus would silently snap back to full height.\n */\n 'v-control': !submenu,\n }\"\n :data-placement=\"placement\"\n :data-size=\"size\"\n :data-compact=\"compact ? '' : undefined\"\n :data-width=\"resolvedWidth ? '' : undefined\"\n :data-match-trigger=\"matchTrigger ? '' : undefined\"\n :style=\"{ '--menu-width': resolvedWidth }\"\n @beforetoggle=\"syncShown\"\n @toggle=\"onToggle\"\n @keydown=\"onKeydown\"\n >\n <slot />\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n /*\n * The panel's surface, border, shadow and inner rhythm come from the shared `.v-panel` class,\n * and its dimensions from the `v-control` class the template sets on the root panel; there is\n * no size table here, the rows reading the inherited variables directly. Only what is\n * specific to a dropdown menu stays below.\n */\n .v-menu {\n min-inline-size: var(--vectis-control-size-menu-min);\n max-inline-size: min(var(--vectis-control-size-menu-max), calc(100dvi - var(--vectis-space-8)));\n }\n\n /*\n * The panel is focusable, but only ever from code; its `tabindex` is -1, so the Tab key never\n * brings anyone here. It holds the focus when the menu was opened with a pointer, and a ring\n * drawn around the whole panel would then read as the reader having landed on something,\n * which is the opposite of what that state means: no command is singled out.\n */\n .v-menu:focus {\n outline: none;\n }\n\n /* Pulls a submenu up so that its first item lines up with the item that opened it,\n by exactly the panel's own padding plus its border. */\n .v-menu .v-menu[data-placement='right-start'] {\n margin-block-start: calc(-1 * (var(--vectis-space-1) + 1px));\n }\n\n /* An explicit width, which only the ROOT panel ever renders: a submenu sets no such\n attribute, so the variable it inherits stays inert there. The ceiling that keeps\n a panel inside the viewport is untouched. */\n .v-menu[data-width] {\n min-inline-size: 0;\n inline-size: var(--menu-width);\n }\n\n /*\n * This works with no anchor name of any kind, because the button that opened the panel is\n * already its implicit anchor; VCombobox and VTimeInput, anchored to a text input, have to\n * name theirs. Like the width above, only the ROOT panel renders it.\n */\n .v-menu[data-match-trigger] {\n min-inline-size: anchor-size(width);\n }\n}\n</style>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA6CA,IAAM,IAAQ,GASR,IAAO,GAaP,IAAU,EAAwB,IAAI,GAGtC,EAAE,UAAO,cAAW,SAAM,YAAS,EAAW,CAAO,GAIrD,IAAO,EAAO,CAAO;EAE3B,SAAS,EAAS,GAAc;GAE9B,AADA,EAAU,CAAK,GACf,EAAK,UAAU,EAAM,KAAK;EAC5B;EASA,SAAS,IAAuB;GAC9B,IAAM,IAAQ,EAAQ;GAEtB,OADK,IACE,CACL,GAAG,EAAM,iBACP,kEACF,CACF,CAAC,CAAC,QAAQ,MAAO,EAAG,QAAQ,iBAAe,MAAM,CAAK,IALnC,CAAC;EAMtB;EAIA,SAAS,IAAa;GACpB,EAAM,CAAC,CAAC,EAAE,EAAE,MAAM;EACpB;EAIA,SAAS,IAAa;GACpB,EAAQ,OAAO,MAAM;EACvB;EAGA,SAAS,EAAU,GAAsB;GACvC,IAAM,IAAQ,EAAQ;GAItB,IADI,CAAC,KAAU,EAAM,OAAmB,QAAQ,iBAAe,MAAM,KACjE,EAAM,UAAU,EAAM,WAAW,EAAM,SAAS;GAEpD,IAAI,EAAM,QAAQ,OAAO;IACvB,EAAK,SAAS;IACd;GACF;GAGA,IAAM,IAAO,EAAM,CAAK,IAAI,eAAe;GAC3C,IAAI,EAAM,QAAQ,YAAa,EAAM,WAAW,EAAM,QAAQ,GAAO;IAOnE,AAFA,EAAM,eAAe,GACrB,EAAK,GACD,EAAM,WAAS,EAAY,EAAM,EAAE,CAAC,EAAE,MAAM;IAChD;GACF;GAMA,IAAI,EAAM,QAAQ,aAAa,SAAS,kBAAkB,GAAO;IAC/D,IAAM,IAAO,EAAM;IACnB,IAAI,EAAK,SAAS,GAAG;KAEnB,AADA,EAAM,eAAe,GACrB,EAAK,EAAK,SAAS,EAAE,EAAE,MAAM;KAC7B;IACF;GACF;GAIA,EAAc,GAAO,GAAO,GAAO,EAAE,UAAU,GAAK,CAAC;EACvD;EAEA,IAAM,IAAgB,QAAe,EAAQ,EAAM,KAAK,CAAC;SAIzD,EAAa;GAEX;GAEA,OAAO;GAEP;GAEA;GAEA;GAEA,IAAI;EACN,CAAC,mBAIC,EA4BM,OAAA;GA3BH,IAAI,EAAA;YACD;GAAJ,KAAI;GACJ,SAAQ;GACR,MAAK;GACL,UAAS;GACT,OAAK,EAAA,CAAC,uCAAqC,EAAA,aAAA,CACgc,EAAA,QAAA,CAAA,CAAA;GAU1e,kBAAgB,EAAA;GAChB,aAAW,EAAA;GACX,gBAAc,EAAA,UAAO,KAAQ,KAAA;GAC7B,cAAY,EAAA,QAAa,KAAQ,KAAA;GACjC,sBAAoB,EAAA,eAAY,KAAQ,KAAA;GACxC,OAAK,EAAA,EAAA,gBAAoB,EAAA,MAAa,CAAA;GACtC,gBAAY,AAAA,EAAA,QAAA,GAAA,MAAE,EAAA,CAAA,KAAA,EAAA,CAAA,CAAA,CAAA,GAAA,CAAA;GACN;GACC;MAEV,EAAQ,EAAA,QAAA,SAAA,CAAA,GAAA,IAAA,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VMenuSeparator.js","names":[],"sources":["../../../src/components/VMenu/VMenuSeparator.vue"],"sourcesContent":["<script setup lang=\"ts\">\
|
|
1
|
+
{"version":3,"file":"VMenuSeparator.js","names":[],"sources":["../../../src/components/VMenu/VMenuSeparator.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport VSeparator from '../VSeparator/VSeparator.vue'\n</script>\n\n<template>\n <VSeparator class=\"v-menu-separator\" />\n</template>\n\n<style>\n@layer vectis.components {\n /*\n * The selector compounds both classes because `.v-separator` sets `margin: 0` from a sheet of\n * its own: at equal specificity the winner would be whichever of the two the consumer's\n * bundler happens to put last. The space above and below comes from the panel's own gap, so\n * no margin is added here.\n */\n .v-separator.v-menu-separator {\n margin-inline: calc(-1 * var(--vectis-space-1));\n }\n}\n</style>\n"],"mappings":""}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VMenuSeparator.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../src/components/VMenu/VMenuSeparator.vue"],"sourcesContent":["<script setup lang=\"ts\">\
|
|
1
|
+
{"version":3,"file":"VMenuSeparator.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../src/components/VMenu/VMenuSeparator.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport VSeparator from '../VSeparator/VSeparator.vue'\n</script>\n\n<template>\n <VSeparator class=\"v-menu-separator\" />\n</template>\n\n<style>\n@layer vectis.components {\n /*\n * The selector compounds both classes because `.v-separator` sets `margin: 0` from a sheet of\n * its own: at equal specificity the winner would be whichever of the two the consumer's\n * bundler happens to put last. The space above and below comes from the panel's own gap, so\n * no margin is added here.\n */\n .v-separator.v-menu-separator {\n margin-inline: calc(-1 * var(--vectis-space-1));\n }\n}\n</style>\n"],"mappings":";;;;;;yBAKE,EAAuC,GAAA,EAA3B,OAAM,mBAAkB,CAAA"}
|
|
@@ -4,19 +4,11 @@ export interface MenuContext {
|
|
|
4
4
|
}
|
|
5
5
|
export declare const menuKey: InjectionKey<MenuContext>;
|
|
6
6
|
/**
|
|
7
|
-
* How long the pointer must rest on an item before its submenu opens, and how long it
|
|
8
|
-
*
|
|
9
|
-
* panel crossed on the way elsewhere from flashing open.
|
|
7
|
+
* How long the pointer must rest on an item before its submenu opens, and how long it must stay
|
|
8
|
+
* away before it closes, in milliseconds.
|
|
10
9
|
*/
|
|
11
10
|
export declare const SUBMENU_HOVER_DELAY = 150;
|
|
12
|
-
/**
|
|
13
|
-
* Finds the element that opens a given panel. It is both what the panel positions
|
|
14
|
-
* itself against and where the focus must return when it closes.
|
|
15
|
-
*
|
|
16
|
-
* It is found by searching the document rather than kept as a reference, because the
|
|
17
|
-
* outermost panel's trigger is rendered by the consumer, in a slot the panel has no
|
|
18
|
-
* access to.
|
|
19
|
-
*/
|
|
11
|
+
/** Finds the element that opens a given panel. */
|
|
20
12
|
export declare function menuInvoker(id: string): HTMLElement | null;
|
|
21
13
|
/** The height of the rows: 32, 40 or 48 pixels. This is public API. */
|
|
22
14
|
export type MenuSize = 'sm' | 'md' | 'lg';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.js","names":[],"sources":["../../../src/components/VMenu/context.ts"],"sourcesContent":["/**\n * What the menu passes down to everything inside it. Choosing a command closes the
|
|
1
|
+
{"version":3,"file":"context.js","names":[],"sources":["../../../src/components/VMenu/context.ts"],"sourcesContent":["/**\n * What the menu passes down to everything inside it. Choosing a command closes the whole menu,\n * however deep in a submenu it was, and closing the outermost panel is all it takes: the\n * submenus are rendered inside it, and the browser closes a stack of popovers from the outside\n * in.\n */\n\nimport type { InjectionKey } from 'vue'\n\nexport interface MenuContext {\n closeAll: () => void\n}\n\nexport const menuKey: InjectionKey<MenuContext> = Symbol('v-menu')\n\n/**\n * How long the pointer must rest on an item before its submenu opens, and how long it must stay\n * away before it closes, in milliseconds.\n */\nexport const SUBMENU_HOVER_DELAY = 150\n\n// @a11y\n/** Finds the element that opens a given panel. */\nexport function menuInvoker(id: string): HTMLElement | null {\n // @fallback\n /*\n * There the id is one we BUILD, so awkward characters can simply be dropped; here it is the\n * panel's real `id` attribute; a generated one, whose prefix the consumer can set through\n * `app.config.idPrefix`; and rewriting it would leave the selector looking for an element\n * that does not exist. Escaping the two characters able to close the quoted string is enough,\n * and it is deliberately not done with `CSS.escape`: jsdom provides no `CSS` object at all,\n * so that call would throw in every unit test.\n */\n return document.querySelector(`[popovertarget=\"${id.replace(/[\"\\\\]/g, '\\\\$&')}\"]`)\n}\n\n/** The height of the rows: 32, 40 or 48 pixels. This is public API. */\nexport type MenuSize = 'sm' | 'md' | 'lg'\n\n/** Where the menu itself may open, relative to its trigger. This is public API. */\nexport type MenuPlacement =\n 'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end'\n\n/**\n * The same list plus the sideways placement submenus use. It is internal: a submenu's\n * position is not the consumer's to choose.\n */\nexport type MenuPanelPlacement = MenuPlacement | 'right-start'\n"],"mappings":";AAaA,IAAa,IAAqC,OAAO,QAAQ;AAUjE,SAAgB,EAAY,GAAgC;CAU1D,OAAO,SAAS,cAAc,mBAAmB,EAAG,QAAQ,UAAU,MAAM,EAAE,GAAG;AACnF"}
|
|
@@ -23,10 +23,8 @@ interface PaginationProps {
|
|
|
23
23
|
*/
|
|
24
24
|
length?: number;
|
|
25
25
|
/**
|
|
26
|
-
* How many slots to render, ellipses counted among them, so the row keeps exactly
|
|
27
|
-
*
|
|
28
|
-
* to show around the current page, so five is the effective minimum. Left out, every
|
|
29
|
-
* page is rendered.
|
|
26
|
+
* How many slots to render, ellipses counted among them, so the row keeps exactly the same
|
|
27
|
+
* width whichever page is current. Left out, every page is rendered.
|
|
30
28
|
*/
|
|
31
29
|
totalVisible?: number;
|
|
32
30
|
/**
|
|
@@ -41,9 +39,9 @@ interface PaginationProps {
|
|
|
41
39
|
*/
|
|
42
40
|
seamless?: boolean;
|
|
43
41
|
/**
|
|
44
|
-
* How the pages OTHER than the current one, and the controls, are drawn.
|
|
45
|
-
*
|
|
46
|
-
*
|
|
42
|
+
* How the pages OTHER than the current one, and the controls, are drawn. It is named for the
|
|
43
|
+
* ITEMS because that is what it paints: on VTabs and VDataTable `variant` names the
|
|
44
|
+
* decoration of the frame instead.
|
|
47
45
|
*/
|
|
48
46
|
itemVariant?: PaginationItemVariant;
|
|
49
47
|
/**
|
|
@@ -57,11 +55,7 @@ interface PaginationProps {
|
|
|
57
55
|
size?: PaginationSize;
|
|
58
56
|
/** Takes 4px off the height of every button. */
|
|
59
57
|
compact?: boolean;
|
|
60
|
-
/**
|
|
61
|
-
* Raises the row off the page, on the terms of VButton's own `elevated`. Joined, the
|
|
62
|
-
* shadow belongs to the ROW and not to each pill, which is what stops it falling into
|
|
63
|
-
* the joints; detached, every button carries its own.
|
|
64
|
-
*/
|
|
58
|
+
/** Raises the row off the page, on the terms of VButton's own `elevated`. */
|
|
65
59
|
elevated?: boolean;
|
|
66
60
|
/**
|
|
67
61
|
* Where the row sits in the space it is given. It only matters in responsive mode,
|
|
@@ -69,9 +63,8 @@ interface PaginationProps {
|
|
|
69
63
|
*/
|
|
70
64
|
align?: PaginationAlign;
|
|
71
65
|
/**
|
|
72
|
-
* The previous and next buttons on either side of the pages: what they show, or `false`
|
|
73
|
-
*
|
|
74
|
-
* VCarousel's `controls` already use.
|
|
66
|
+
* The previous and next buttons on either side of the pages: what they show, or `false` to
|
|
67
|
+
* leave them out.
|
|
75
68
|
*/
|
|
76
69
|
controls?: PaginationControls;
|
|
77
70
|
/** The icon of the previous control: an icon name, or an explicit render. */
|
|
@@ -111,11 +104,7 @@ interface PaginationProps {
|
|
|
111
104
|
* system dictionary.
|
|
112
105
|
*/
|
|
113
106
|
pageLabel?: (page: number) => string;
|
|
114
|
-
/**
|
|
115
|
-
* The address of a page. Given, every pill and both controls render as links, the
|
|
116
|
-
* previous and next ones carrying `rel="prev"` and `rel="next"`; a click still updates
|
|
117
|
-
* the model, then the browser follows the link. Left out, they are buttons.
|
|
118
|
-
*/
|
|
107
|
+
/** The address of a page. Left out, they are buttons. */
|
|
119
108
|
href?: (page: number) => string;
|
|
120
109
|
}
|
|
121
110
|
type __VLS_Props = PaginationProps;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VPagination.js","names":[],"sources":["../../../src/components/VPagination/VPagination.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @keyboard @core\n/**\n * The row of page numbers under a long list. Every pill is a VButton and the previous\n * and next controls are buttons too, so nothing about hovering, focusing or disabling\n * is written again here.\n *\n * A long list has more pages than fit on a line, and the row deals with that TWICE,\n * for two different reasons. First logically: `totalVisible` fixes how many slots are\n * rendered at all, and the pages that do not fit are replaced by an ellipsis — pure\n * arithmetic, identical on the server and in the browser. Then visually: as the space\n * actually available narrows, the neighbours of the current page are hidden one step\n * at a time, and that half is entirely CSS, the row asking about its own width rather\n * than being measured from code.\n *\n * Given `href`, every pill and both controls are links, so a list of products can be\n * paged by address: middle-clicked into a new tab, bookmarked, followed by a crawler.\n *\n * The only behavioural JavaScript is the keyboard, explained where it is written.\n */\nimport { computed, h, nextTick, ref } from 'vue'\nimport type { FunctionalComponent } from 'vue'\n\nimport VButton from '../VButton/VButton.vue'\nimport VButtonGroup from '../VButton/VButtonGroup.vue'\nimport VIcon from '../VIcon/VIcon.vue'\nimport { iconProps } from '../VIcon/iconProps'\nimport { chevron_left as chevronLeftIcon } from '../VIcon/icons/chevron_left'\nimport { chevron_right as chevronRightIcon } from '../VIcon/icons/chevron_right'\nimport { more_horiz as moreHorizIcon } from '../VIcon/icons/more_horiz'\nimport type { IconSource } from '../VIcon/types'\nimport VIconButton from '../VIconButton/VIconButton.vue'\n\nimport { arrowNavigate, navigableItems } from '../../utils/arrowNav'\nimport { clamp } from '../../utils/number'\nimport { resolveMatcher } from '../../utils/matcher'\n\nimport { useAriaLabel } from '../../composables/useAriaLabel'\nimport { useMessages } from '../../i18n/state'\n\n/** How the pages that are not the current one are drawn. */\nexport type PaginationItemVariant = 'ghost' | 'outline'\n\n/** How the current page is drawn. */\nexport type PaginationSelectedVariant = 'solid' | 'soft' | 'ghost'\n\n/** The colour of the current page. */\nexport type PaginationTone = 'accent' | 'neutral' | 'danger'\n\n/** The height of the pills, from the scale every control shares. */\nexport type PaginationSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'\n\n/** Where the row sits in the width it is given. */\nexport type PaginationAlign = 'start' | 'center' | 'end'\n\n/** Whether the previous and next controls are rendered, and what they show. */\nexport type PaginationControls = false | 'icon' | 'text' | 'both'\n\n/**\n * Which pages cannot be reached, given either as a list of page numbers or as a function\n * answering that question for a page.\n */\nexport type PaginationMatcher = number[] | ((page: number) => boolean)\n\ninterface PaginationProps {\n /**\n * How many pages there are in all. It is 1 by default, which renders a single page: the\n * real count almost always has to be given.\n */\n length?: number\n /**\n * How many slots to render, ellipses counted among them, so the row keeps exactly\n * the same width whichever page is current. Below five there would be nothing left\n * to show around the current page, so five is the effective minimum. Left out, every\n * page is rendered.\n */\n totalVisible?: number\n\n /**\n * Separates the buttons instead of joining them into one segmented control. It is the\n * word VButtonGroup and VToggle use for the same question, in the same direction.\n */\n detached?: boolean\n /**\n * Takes the lines out from between the joined buttons, so the row reads as one frame rather\n * than as segments, on the terms of VButtonGroup's own `seamless`. It has no effect under\n * `detached`.\n */\n seamless?: boolean\n /**\n * How the pages OTHER than the current one, and the controls, are drawn. What the current\n * page takes is `selectedVariant`. 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?: PaginationItemVariant\n /**\n * How the current page is drawn, in the row's tone: filled, tinted, or the colour of its\n * text alone. The same choice as VToggle's `selectedVariant`.\n */\n selectedVariant?: PaginationSelectedVariant\n /** The colour the current page takes. The other pages and the controls stay neutral. */\n tone?: PaginationTone\n /** The height of the buttons, from the scale shared by every control. */\n size?: PaginationSize\n /** Takes 4px off the height of every button. */\n compact?: boolean\n /**\n * Raises the row off the page, on the terms of VButton's own `elevated`. Joined, the\n * shadow belongs to the ROW and not to each pill, which is what stops it falling into\n * the joints; detached, every button carries its own.\n */\n elevated?: boolean\n /**\n * Where the row sits in the space it is given. It only matters in responsive mode,\n * where the row takes the whole width available.\n */\n align?: PaginationAlign\n\n /**\n * The previous and next buttons on either side of the pages: what they show, or `false`\n * to leave them out. One prop rather than two, the shape VFilePicker's `preview` and\n * VCarousel's `controls` already use.\n */\n controls?: PaginationControls\n /** The icon of the previous control: an icon name, or an explicit render. */\n prevIcon?: IconSource\n /** The icon of the next control: an icon name, or an explicit render. */\n nextIcon?: IconSource\n /**\n * The wording of the previous control, used both as its visible text and as what\n * screen readers announce. It falls back to the design system dictionary.\n */\n prevText?: string\n /**\n * The wording of the next control, used both as its visible text and as what screen\n * readers announce. It falls back to the design system dictionary.\n */\n nextText?: string\n\n /** Makes the whole component unusable. */\n disabled?: boolean\n /**\n * Which pages cannot be reached, as a list or as a function. The previous and next\n * controls step OVER them rather than stopping at one.\n */\n disabledPages?: PaginationMatcher\n\n /**\n * Lets the row shed pages as the space narrows, by asking about its own width. It is\n * off by default, because it makes the row take the full width available.\n */\n responsive?: boolean\n\n /**\n * What screen readers announce for the navigation itself. It falls back to the\n * design system dictionary.\n */\n label?: string\n /**\n * How a page is announced. A pill shows a bare number, which alone means nothing to\n * a screen reader: this is what turns it into \"Page 3\". It falls back to the design\n * system dictionary.\n */\n pageLabel?: (page: number) => string\n /**\n * The address of a page. Given, every pill and both controls render as links, the\n * previous and next ones carrying `rel=\"prev\"` and `rel=\"next\"`; a click still updates\n * the model, then the browser follows the link. Left out, they are buttons.\n */\n href?: (page: number) => string\n}\n\nconst props = withDefaults(defineProps<PaginationProps>(), {\n length: 1,\n totalVisible: undefined,\n detached: false,\n seamless: false,\n itemVariant: 'ghost',\n selectedVariant: 'solid',\n tone: 'accent',\n size: 'md',\n compact: false,\n elevated: false,\n align: 'start',\n controls: 'icon',\n prevIcon: () => chevronLeftIcon,\n nextIcon: () => chevronRightIcon,\n prevText: undefined,\n nextText: undefined,\n disabled: false,\n disabledPages: undefined,\n responsive: false,\n label: undefined,\n pageLabel: undefined,\n href: undefined,\n})\n\nconst emit = defineEmits<{\n /**\n * A page was chosen from the row, a pill or a control, with the click that chose it. It\n * comes before the model changes, so under `href` a single-page application calls\n * `preventDefault()` on the event and hands the address to its router instead of letting\n * the browser load it. A click that opens the link elsewhere (a modifier key held) is not\n * a choice made here and emits nothing.\n */\n navigate: [page: number, event: MouseEvent]\n}>()\n\n// The prop wins over the dictionary, and above both, a consumer's own aria-label or\n// aria-labelledby still wins — that arbitration is what `useAriaLabel` is for.\nconst m = useMessages()\nconst ariaLabel = useAriaLabel(() => props.label ?? m.value.pagination.label)\nconst resolvedPrevText = computed(() => props.prevText ?? m.value.pagination.previous)\nconst resolvedNextText = computed(() => props.nextText ?? m.value.pagination.next)\n\n/** The page being shown, counted from 1. It starts on the first. */\nconst page = defineModel<number>({ default: 1 })\n\n/** One slot in the row: either a page, or the ellipsis standing for those left out. */\ntype PaginationItem =\n | { kind: 'page'; key: string; page: number; edge: boolean; distance: number }\n | { kind: 'gap'; key: string }\n\n// A length or a model that is not a whole number (`NaN` while a page size is still loading,\n// a computed `2.5`) is brought back to a page that exists, or no pill would ever match it.\nconst total = computed(() => Math.max(Math.trunc(props.length) || 1, 1))\nconst currentPage = computed(() => clamp(Math.round(page.value) || 1, 1, total.value))\n\nconst isPageDisabled = computed(() => resolveMatcher(props.disabledPages))\n\n// @a11y\n// A pill shows nothing but a number, which on its own tells a screen reader\n// nothing at all; this is what has it announced as \"Page 3\".\nfunction pageLabelFor(n: number): string {\n return props.pageLabel ? props.pageLabel(n) : m.value.pagination.page(n)\n}\n\n/**\n * Works out which slots the row holds.\n *\n * The count is CONSTANT: `totalVisible` counts everything rendered, the ellipses\n * included, so the row never changes width as one moves through the pages. The window\n * of pages around the current one is centred on it, and near either end it SHIFTS\n * rather than shrinking — which is what keeps that count constant there too.\n *\n * Each page also carries how far it is from the current one. That distance is what the\n * responsive hiding sorts by, and it is capped at three: past that, the most distant\n * neighbours all disappear together at the first step.\n */\nconst items = computed<PaginationItem[]>(() => {\n const count = total.value\n const current = currentPage.value\n\n const pageItem = (n: number): PaginationItem => ({\n kind: 'page',\n key: `page-${n}`,\n page: n,\n edge: n === 1 || n === count,\n distance: Math.min(Math.abs(n - current), 3),\n })\n const pages = (from: number, to: number): PaginationItem[] => {\n const out: PaginationItem[] = []\n for (let n = from; n <= to; n++) out.push(pageItem(n))\n return out\n }\n // Keyed by the side it stands on, never by the page it follows: there is at most one\n // ellipsis per side, and the left one always comes after the first page while the right\n // one always comes before the last. A key derived from the window would change on every\n // navigation, so the right-hand ellipsis would be destroyed and rebuilt each time the\n // window slid, for a node that never changes.\n const gap = (side: 'start' | 'end'): PaginationItem => ({ kind: 'gap', key: `gap-${side}` })\n\n // With no limit given, nothing is left out. Below five slots there would be no room\n // for the first page, an ellipsis, the current page, another ellipsis and the last,\n // so that is the floor.\n const limit = Math.trunc(props.totalVisible ?? count)\n const visible = Number.isNaN(limit) ? count : Math.max(limit, 5)\n if (visible >= count) return pages(1, count)\n\n const start = current - Math.floor((visible - 5) / 2)\n const end = start + (visible - 5)\n\n // Close to either end, an ellipsis on that side would stand for no missing page at\n // all. The window is stretched to the bound instead, which both avoids that and\n // keeps the number of slots exactly the same.\n if (start <= 3) return [...pages(1, visible - 2), gap('end'), pageItem(count)]\n if (end >= count - 2) return [pageItem(1), gap('start'), ...pages(count - visible + 3, count)]\n return [pageItem(1), gap('start'), ...pages(start, end), gap('end'), pageItem(count)]\n})\n\n/**\n * Where a control leads: the nearest page in that direction that can actually be\n * reached, disabled ones being stepped over rather than stopped at.\n *\n * Answering `undefined` means there is none left, which disables the control — and\n * that single answer also covers being on the first or the last page, with no separate\n * test for the ends.\n */\nfunction step(direction: -1 | 1): number | undefined {\n for (let n = currentPage.value + direction; n >= 1 && n <= total.value; n += direction) {\n if (!isPageDisabled.value(n)) return n\n }\n return undefined\n}\n\nconst prevTarget = computed(() => step(-1))\nconst nextTarget = computed(() => step(1))\nconst prevDisabled = computed(() => props.disabled || prevTarget.value === undefined)\nconst nextDisabled = computed(() => props.disabled || nextTarget.value === undefined)\n\nfunction goTo(n: number | undefined) {\n if (n === undefined) return\n page.value = clamp(n, 1, total.value)\n}\n\n/** The address a pill or a control links to, or `undefined` when the row is made of buttons. */\nfunction hrefFor(n: number): string | undefined {\n return props.href?.(n)\n}\n\n// @core\n/*\n * A link clicked with a modifier held (Ctrl or Cmd for a new tab, Shift for a window, Alt for a\n * download) opens somewhere ELSE: the page on screen stays where it is, so the model must not\n * move. The test the routers themselves make before taking a click over.\n */\nfunction opensElsewhere(event: MouseEvent): boolean {\n return (\n props.href !== undefined &&\n (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)\n )\n}\n\nfunction choose(event: MouseEvent, n: number | undefined) {\n if (n === undefined || opensElsewhere(event)) return\n emit('navigate', n, event)\n goTo(n)\n}\n\n/** Whether a pill or a control can no longer be used: a disabled button, or an inert link. */\nfunction isUnusable(el: HTMLElement): boolean {\n return el.matches(':disabled, [aria-disabled=\"true\"]')\n}\n\n// @a11y\n/*\n * The previous and next controls disable themselves once no page is left in their direction,\n * and a focused button that becomes disabled hands the focus to <body>: a keyboard reader\n * pressing \"Next page\" on the second to last page would be sent back to the top of the\n * document by the next Tab. The focus goes to the page just reached instead, which is always\n * rendered, the current page being the one pill the responsive steps never hide.\n *\n * The focus is read BEFORE the change: a pointer click may not focus a button at all (Safari),\n * and a control that did not hold the focus has nothing to hand on.\n */\nasync function goFromControl(event: MouseEvent, n: number | undefined) {\n const control = event.currentTarget as HTMLElement\n const hadFocus = control === document.activeElement\n choose(event, n)\n if (!hadFocus) return\n await nextTick()\n if (isUnusable(control)) {\n navEl.value?.querySelector<HTMLElement>('[aria-current=\"page\"]')?.focus()\n }\n}\n\n/*\n * The previous and next controls, written ONCE: the two differ only by their direction, and\n * a pair of template blocks drifting apart on the next change is exactly what this avoids.\n * A functional component because the two sit at either end of the row, with the pages in\n * between, where a `v-for` over two descriptors could not put them.\n *\n * The icon goes on the side the control points to — before the word going back, after it\n * going forward — and every icon mirrors in a right-to-left page, an arrow pointing at a\n * physical direction.\n *\n * The control is named explicitly even though its label is visible: at narrow widths that\n * label is hidden and only the icon remains, and the name a screen reader announces has to\n * survive that.\n *\n * As a link it points at the page it leads to. With none left it still receives an address\n * (the current page's), which VButton drops once disabled: the control stays the same `<a>`,\n * turned inert, rather than swapping to a `<button>` under a focus `goFromControl` has to read.\n * The `rel` goes with the address, an inert link leading nowhere.\n */\nconst PageControl: FunctionalComponent<{ side: 'prev' | 'next' }> = ({ side }) => {\n const prev = side === 'prev'\n const label = prev ? resolvedPrevText.value : resolvedNextText.value\n const icon = prev ? props.prevIcon : props.nextIcon\n const target = prev ? prevTarget.value : nextTarget.value\n const disabled = prev ? prevDisabled.value : nextDisabled.value\n const common = {\n class: 'v-pagination-control',\n variant: props.itemVariant,\n disabled,\n href: hrefFor(target ?? currentPage.value),\n rel: props.href && !disabled ? side : undefined,\n onClick: (event: MouseEvent) => goFromControl(event, target),\n }\n const glyph = () => h(VIcon, { ...iconProps(icon), mirrored: true })\n if (props.controls === 'icon') return h(VIconButton, { ...common, label }, glyph)\n return h(\n VButton,\n { ...common, tone: 'neutral', 'aria-label': label },\n {\n ...(props.controls === 'both' ? { [prev ? 'start' : 'end']: glyph } : {}),\n default: () => h('span', { class: 'v-pagination-control-label' }, label),\n },\n )\n}\n\nconst navEl = ref<HTMLElement | null>(null)\n\n// @keyboard @a11y\n/**\n * The arrow keys, through the shared implementation in `utils/arrowNav`. Tab is left\n * alone: every visible pill remains a stop in the tab order, as in any list of links.\n *\n * The pills the responsive rules have hidden are left out by the helper, which skips\n * anything not displayed — so they cannot be focused into.\n */\nfunction onKeydown(event: KeyboardEvent) {\n const nav = navEl.value\n if (!nav) return\n arrowNavigate(event, nav, () =>\n navigableItems(nav, '.v-pagination-page:not(:disabled, [aria-disabled=\"true\"])'),\n )\n}\n\n// The pills are rendered by a VButtonGroup inside the nav, so a template ref reaches none of\n// them. `focus` goes to the current page, which every responsive step keeps on screen.\ndefineExpose({\n // Two queries, not one selector list: a list matches in DOM order, which would hand the\n // focus to the previous control ahead of the current page. `a[href]` because an inert link\n // has lost its address, and with it the ability to take the focus.\n /** Moves the focus to the current page, or to the first control that can take it. */\n focus: (options?: FocusOptions) =>\n (\n navEl.value?.querySelector<HTMLElement>(\n '[aria-current=\"page\"]:is(button, a[href]):not(:disabled)',\n ) ?? navEl.value?.querySelector<HTMLElement>(':is(button, a[href]):not(:disabled)')\n )?.focus(options),\n /** The `<nav>` element, which is also where the consumer's attributes land. */\n el: navEl,\n})\n</script>\n\n<template>\n <nav\n ref=\"navEl\"\n class=\"v-pagination\"\n :aria-label=\"ariaLabel\"\n :data-align=\"align\"\n :data-item-variant=\"itemVariant\"\n :data-controls=\"controls || undefined\"\n :data-responsive=\"responsive ? '' : undefined\"\n @keydown=\"onKeydown\"\n >\n <!-- The row is a VButtonGroup either way: joined it merges the borders of its DIRECT\n button children, and detached it spaces them, which is all this prop asks of it.\n That is why there is no list markup wrapping the pills, and why the ellipsis is\n itself an inert button rather than a plain span: anything else between two pills\n would break the seam. -->\n <!--\n The size, the density and the elevation are handed to the GROUP, which gives them to\n every button inside, rather than repeated on each child. The elevation has to be there\n in any case, since that is where VButtonGroup draws it: joined, the row takes the shadow\n and the segments give theirs up, or it would fall into every joint.\n\n `|| undefined` keeps the group opinion-free when the row does not ask, `undefined`\n being what means \"no opinion\" there where `false` is an order. The size is always an\n opinion, the pagination having a default of its own.\n -->\n <VButtonGroup\n class=\"v-pagination-items\"\n :detached=\"detached\"\n :seamless=\"seamless\"\n :size=\"size\"\n :compact=\"compact || undefined\"\n :elevated=\"elevated || undefined\"\n >\n <PageControl v-if=\"controls\" side=\"prev\" />\n\n <template v-for=\"item in items\" :key=\"item.key\">\n <VButton\n v-if=\"item.kind === 'page'\"\n class=\"v-pagination-page\"\n :variant=\"item.page === currentPage ? selectedVariant : itemVariant\"\n :tone=\"item.page === currentPage ? tone : 'neutral'\"\n :disabled=\"disabled || isPageDisabled(item.page)\"\n :aria-label=\"pageLabelFor(item.page)\"\n :aria-current=\"item.page === currentPage ? 'page' : undefined\"\n :href=\"hrefFor(item.page)\"\n :data-edge=\"item.edge ? '' : undefined\"\n :data-distance=\"!item.edge && item.distance > 0 ? item.distance : undefined\"\n @click=\"choose($event, item.page)\"\n >\n {{ item.page }}\n </VButton>\n <!-- The ellipsis is a disabled icon-only button rather than a span: being a button,\n it keeps the joined row's seam continuous and follows the size and density like\n everything else. Disabling it takes it out of the tab order, and it is hidden\n from screen readers, which have the page numbers themselves — which is also why\n it is a VButton and not a VIconButton, whose required label would name something\n nobody can reach. -->\n <VButton\n v-else\n class=\"v-pagination-ellipsis\"\n aria-hidden=\"true\"\n data-icon-only\n :variant=\"itemVariant\"\n tone=\"neutral\"\n disabled\n >\n <template #start>\n <VIcon :name=\"moreHorizIcon\" />\n </template>\n </VButton>\n </template>\n\n <PageControl v-if=\"controls\" side=\"next\" />\n </VButtonGroup>\n </nav>\n</template>\n\n<style>\n@layer vectis.components {\n .v-pagination {\n display: flex;\n }\n\n /*\n * Making the row a query container is reserved for responsive mode, since every\n * query below depends on it — and it is not free.\n *\n * A container of this kind computes its width WITHOUT looking at its content, so the\n * row has to be block-level: as an inline box it would measure zero and hide\n * everything at once. It therefore takes its parent's whole width, which is exactly\n * what makes the hiding follow the space the component was actually given, and what\n * obliges a flex parent to grant it one.\n *\n * Outside responsive mode the row keeps a width of its own and sits like any other\n * content. Either way the alignment is asked for explicitly and never inferred.\n */\n .v-pagination[data-responsive] {\n container-type: inline-size;\n container-name: v-pagination;\n }\n\n .v-pagination[data-align='center'] {\n justify-content: center;\n }\n\n .v-pagination[data-align='end'] {\n justify-content: flex-end;\n }\n\n /* Qualified by an attribute VButton always renders, which is what makes both of these\n beat that button's own padding and its own transition whatever order the two sheets\n end up in — the VIconButton idiom. */\n .v-pagination-page[data-size] {\n /* A one-digit pill is square, and widens by itself past that. The height variable\n is set by the shared size class on this very element, so this single rule covers\n all five sizes and their compact forms. */\n min-inline-size: var(--control-height);\n padding-inline: var(--vectis-space-2);\n\n /*\n * TRAP — the highlight changes instantly, and this must not be \"restored for\n * consistency\". VButton fades `background-color` over `--vectis-duration-fast`, which is\n * right for a button that stays put and wrong here: truncation SHIFTS its window, so from\n * one page to the next the highlight keeps its slot but changes ELEMENT, and the outgoing\n * and incoming pills are both half-tinted for a few frames — read as a flicker, with\n * nothing in the console.\n *\n * No duration avoids it: symmetric fades overlap, asymmetric ones leave either no\n * highlight or a trailing one. Keeping the fade for hover alone is not expressible\n * either — a transition resolves from the state being ENTERED, and \"no longer current\"\n * is the same state as \"no longer hovered\". So the hover fade goes too, on the pills\n * only; the controls never change active state and keep theirs.\n */\n transition: none;\n }\n\n /* The ellipsis is a disabled button only as a technical device; to the reader it is\n not a control that has been turned off, so it does not take the forbidden\n cursor. Qualified like the pills: VButton's disabled cursor weighs (0,2,0), and a bare\n class loses to it whatever the sheet order. */\n .v-pagination-ellipsis[data-size] {\n cursor: default;\n }\n\n /*\n * The responsive half of the truncation. The nav queries ITS OWN width, so the steps follow\n * the space the component was given rather than the viewport — a sidebar and a full-width\n * page behave differently, as they should, with nothing measured from code.\n *\n * The thresholds are rem LITERALS, a container query accepting no variables, and are\n * calibrated on `md`: a pill and a control are each 2.5rem plus a 0.25rem gutter, so\n * 2.75rem apiece — thirteen slots need ~35.5rem, eleven ~30, nine ~24.5, hence the three\n * with a little margin. The extremes (the largest size, five-digit page numbers) are what\n * `totalVisible` and `responsive: false` are for.\n *\n * Each threshold is the width NEEDED to show that level, never what remains after hiding:\n * written the other way round the row overflows for the whole interval before the next\n * step takes effect.\n *\n * The most distant neighbours go first. The edges and the current page carry no distance,\n * so no rule here can reach them. `display: none` also takes a hidden pill out of the tab\n * order and the a11y tree, which is intended. No ellipsis replaces one either — it is\n * exactly as wide as the pill it would stand for, so it frees nothing.\n */\n @container v-pagination (max-inline-size: 36rem) {\n .v-pagination[data-responsive] .v-pagination-page[data-distance='3'] {\n display: none;\n }\n }\n\n @container v-pagination (max-inline-size: 31rem) {\n .v-pagination[data-responsive] .v-pagination-page[data-distance='2'] {\n display: none;\n }\n\n /* A control showing both its icon and its label is about four times as wide as a\n pill, so dropping its label frees more room than sacrificing another page —\n which is why it happens before the last step. It is never done when the control\n shows text alone: there would be nothing left to click. */\n .v-pagination[data-responsive][data-controls='both'] .v-pagination-control-label {\n display: none;\n }\n }\n\n @container v-pagination (max-inline-size: 25rem) {\n .v-pagination[data-responsive] .v-pagination-page[data-distance='1'] {\n display: none;\n }\n }\n\n /*\n * The frame of an `outline` row: the colour its other pages already paint. They are drawn\n * in the NEUTRAL tone, so this is what their `--tone-border-soft` resolves to; naming that\n * variable here instead would read the CURRENT page's tone and tint one segment of the\n * frame accent or red. A disabled page greys its outline to a different token, hence a\n * variable the second rule changes. VToggle's `--toggle-frame` is the same device.\n *\n * `soft` and `ghost` leave VButton's border transparent, which in an outline row opens a\n * gap in the frame for the whole width of the current page (the `SelectedVariants` play\n * function). Restoring it on all four sides keeps the frame closed wherever the current\n * page sits; seamless, the shared edges are cleared again by VButtonGroup's own (0,6,0)\n * rules, which these (0,5,0) ones stay below.\n */\n .v-pagination[data-item-variant='outline'] {\n --pagination-frame: var(--vectis-color-border-strong);\n }\n\n .v-pagination[data-item-variant='outline']\n > *\n > .v-pagination-page:is(:disabled, [aria-disabled='true']) {\n --pagination-frame: var(--vectis-color-border);\n }\n\n .v-pagination[data-item-variant='outline']\n > *\n > .v-pagination-page[aria-current='page']:is([data-variant='soft'], [data-variant='ghost']) {\n border-color: var(--pagination-frame);\n }\n\n /*\n * Windows forced colors replace every background with the page's, so the current page 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-pagination-page.v-pagination-page.v-pagination-page.v-pagination-page[aria-current='page']:not(\n :disabled,\n [aria-disabled='true']\n ) {\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":"VPagination.js","names":[],"sources":["../../../src/components/VPagination/VPagination.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @keyboard @core\n/**\n * Native buttons activate pages. JavaScript builds bounded page windows and provides roving\n * keyboard navigation.\n */\nimport { computed, h, nextTick, ref } from 'vue'\nimport type { FunctionalComponent } from 'vue'\n\nimport VButton from '../VButton/VButton.vue'\nimport VButtonGroup from '../VButton/VButtonGroup.vue'\nimport VIcon from '../VIcon/VIcon.vue'\nimport { iconProps } from '../VIcon/iconProps'\nimport { chevron_left as chevronLeftIcon } from '../VIcon/icons/chevron_left'\nimport { chevron_right as chevronRightIcon } from '../VIcon/icons/chevron_right'\nimport { more_horiz as moreHorizIcon } from '../VIcon/icons/more_horiz'\nimport type { IconSource } from '../VIcon/types'\nimport VIconButton from '../VIconButton/VIconButton.vue'\n\nimport { arrowNavigate, navigableItems } from '../../utils/arrowNav'\nimport { clamp } from '../../utils/number'\nimport { resolveMatcher } from '../../utils/matcher'\n\nimport { useAriaLabel } from '../../composables/useAriaLabel'\nimport { useMessages } from '../../i18n/state'\n\n/** How the pages that are not the current one are drawn. */\nexport type PaginationItemVariant = 'ghost' | 'outline'\n\n/** How the current page is drawn. */\nexport type PaginationSelectedVariant = 'solid' | 'soft' | 'ghost'\n\n/** The colour of the current page. */\nexport type PaginationTone = 'accent' | 'neutral' | 'danger'\n\n/** The height of the pills, from the scale every control shares. */\nexport type PaginationSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'\n\n/** Where the row sits in the width it is given. */\nexport type PaginationAlign = 'start' | 'center' | 'end'\n\n/** Whether the previous and next controls are rendered, and what they show. */\nexport type PaginationControls = false | 'icon' | 'text' | 'both'\n\n/**\n * Which pages cannot be reached, given either as a list of page numbers or as a function\n * answering that question for a page.\n */\nexport type PaginationMatcher = number[] | ((page: number) => boolean)\n\ninterface PaginationProps {\n /**\n * How many pages there are in all. It is 1 by default, which renders a single page: the\n * real count almost always has to be given.\n */\n length?: number\n /**\n * How many slots to render, ellipses counted among them, so the row keeps exactly the same\n * width whichever page is current. Left out, every page is rendered.\n */\n totalVisible?: number\n\n /**\n * Separates the buttons instead of joining them into one segmented control. It is the\n * word VButtonGroup and VToggle use for the same question, in the same direction.\n */\n detached?: boolean\n /**\n * Takes the lines out from between the joined buttons, so the row reads as one frame rather\n * than as segments, on the terms of VButtonGroup's own `seamless`. It has no effect under\n * `detached`.\n */\n seamless?: boolean\n /**\n * How the pages OTHER than the current one, and the controls, are drawn. It is named for the\n * ITEMS because that is what it paints: on VTabs and VDataTable `variant` names the\n * decoration of the frame instead.\n */\n itemVariant?: PaginationItemVariant\n /**\n * How the current page is drawn, in the row's tone: filled, tinted, or the colour of its\n * text alone. The same choice as VToggle's `selectedVariant`.\n */\n selectedVariant?: PaginationSelectedVariant\n /** The colour the current page takes. The other pages and the controls stay neutral. */\n tone?: PaginationTone\n /** The height of the buttons, from the scale shared by every control. */\n size?: PaginationSize\n /** Takes 4px off the height of every button. */\n compact?: boolean\n /** Raises the row off the page, on the terms of VButton's own `elevated`. */\n elevated?: boolean\n /**\n * Where the row sits in the space it is given. It only matters in responsive mode,\n * where the row takes the whole width available.\n */\n align?: PaginationAlign\n\n /**\n * The previous and next buttons on either side of the pages: what they show, or `false` to\n * leave them out.\n */\n controls?: PaginationControls\n /** The icon of the previous control: an icon name, or an explicit render. */\n prevIcon?: IconSource\n /** The icon of the next control: an icon name, or an explicit render. */\n nextIcon?: IconSource\n /**\n * The wording of the previous control, used both as its visible text and as what\n * screen readers announce. It falls back to the design system dictionary.\n */\n prevText?: string\n /**\n * The wording of the next control, used both as its visible text and as what screen\n * readers announce. It falls back to the design system dictionary.\n */\n nextText?: string\n\n /** Makes the whole component unusable. */\n disabled?: boolean\n /**\n * Which pages cannot be reached, as a list or as a function. The previous and next\n * controls step OVER them rather than stopping at one.\n */\n disabledPages?: PaginationMatcher\n\n /**\n * Lets the row shed pages as the space narrows, by asking about its own width. It is\n * off by default, because it makes the row take the full width available.\n */\n responsive?: boolean\n\n /**\n * What screen readers announce for the navigation itself. It falls back to the\n * design system dictionary.\n */\n label?: string\n /**\n * How a page is announced. A pill shows a bare number, which alone means nothing to\n * a screen reader: this is what turns it into \"Page 3\". It falls back to the design\n * system dictionary.\n */\n pageLabel?: (page: number) => string\n /** The address of a page. Left out, they are buttons. */\n href?: (page: number) => string\n}\n\nconst props = withDefaults(defineProps<PaginationProps>(), {\n length: 1,\n totalVisible: undefined,\n detached: false,\n seamless: false,\n itemVariant: 'ghost',\n selectedVariant: 'solid',\n tone: 'accent',\n size: 'md',\n compact: false,\n elevated: false,\n align: 'start',\n controls: 'icon',\n prevIcon: () => chevronLeftIcon,\n nextIcon: () => chevronRightIcon,\n prevText: undefined,\n nextText: undefined,\n disabled: false,\n disabledPages: undefined,\n responsive: false,\n label: undefined,\n pageLabel: undefined,\n href: undefined,\n})\n\nconst emit = defineEmits<{\n /**\n * A page was chosen from the row, a pill or a control, with the click that chose it. It comes\n * before the model changes, so under `href` a single-page application calls\n * `preventDefault()` on the event and hands the address to its router instead of letting the\n * browser load it.\n */\n navigate: [page: number, event: MouseEvent]\n}>()\n\n// The prop wins over the dictionary, and above both, a consumer's own aria-label or\n// aria-labelledby still wins; that arbitration is what `useAriaLabel` is for.\nconst m = useMessages()\nconst ariaLabel = useAriaLabel(() => props.label ?? m.value.pagination.label)\nconst resolvedPrevText = computed(() => props.prevText ?? m.value.pagination.previous)\nconst resolvedNextText = computed(() => props.nextText ?? m.value.pagination.next)\n\n/** The page being shown, counted from 1. It starts on the first. */\nconst page = defineModel<number>({ default: 1 })\n\n/** One slot in the row: either a page, or the ellipsis standing for those left out. */\ntype PaginationItem =\n | { kind: 'page'; key: string; page: number; edge: boolean; distance: number }\n | { kind: 'gap'; key: string }\n\n// A length or a model that is not a whole number (`NaN` while a page size is still loading,\n// a computed `2.5`) is brought back to a page that exists, or no pill would ever match it.\nconst total = computed(() => Math.max(Math.trunc(props.length) || 1, 1))\nconst currentPage = computed(() => clamp(Math.round(page.value) || 1, 1, total.value))\n\nconst isPageDisabled = computed(() => resolveMatcher(props.disabledPages))\n\n// @a11y\n// A pill shows nothing but a number, which on its own tells a screen reader\n// nothing at all; this is what has it announced as \"Page 3\".\nfunction pageLabelFor(n: number): string {\n return props.pageLabel ? props.pageLabel(n) : m.value.pagination.page(n)\n}\n\n/** Works out which slots the row holds. */\nconst items = computed<PaginationItem[]>(() => {\n const count = total.value\n const current = currentPage.value\n\n const pageItem = (n: number): PaginationItem => ({\n kind: 'page',\n key: `page-${n}`,\n page: n,\n edge: n === 1 || n === count,\n distance: Math.min(Math.abs(n - current), 3),\n })\n const pages = (from: number, to: number): PaginationItem[] => {\n const out: PaginationItem[] = []\n for (let n = from; n <= to; n++) out.push(pageItem(n))\n return out\n }\n // Keyed by the side it stands on, never by the page it follows: there is at most one\n // ellipsis per side, and the left one always comes after the first page while the right\n // one always comes before the last. A key derived from the window would change on every\n // navigation, so the right-hand ellipsis would be destroyed and rebuilt each time the\n // window slid, for a node that never changes.\n const gap = (side: 'start' | 'end'): PaginationItem => ({ kind: 'gap', key: `gap-${side}` })\n\n // With no limit given, nothing is left out. Below five slots there would be no room\n // for the first page, an ellipsis, the current page, another ellipsis and the last,\n // so that is the floor.\n const limit = Math.trunc(props.totalVisible ?? count)\n const visible = Number.isNaN(limit) ? count : Math.max(limit, 5)\n if (visible >= count) return pages(1, count)\n\n const start = current - Math.floor((visible - 5) / 2)\n const end = start + (visible - 5)\n\n // The window is stretched to the bound instead, which both avoids that and keeps the number\n // of slots exactly the same.\n if (start <= 3) return [...pages(1, visible - 2), gap('end'), pageItem(count)]\n if (end >= count - 2) return [pageItem(1), gap('start'), ...pages(count - visible + 3, count)]\n return [pageItem(1), gap('start'), ...pages(start, end), gap('end'), pageItem(count)]\n})\n\n/**\n * Where a control leads: the nearest page in that direction that can actually be reached,\n * disabled ones being stepped over rather than stopped at. Answering `undefined` means there is\n * none left, which disables the control; and that single answer also covers being on the first\n * or the last page, with no separate test for the ends.\n */\nfunction step(direction: -1 | 1): number | undefined {\n for (let n = currentPage.value + direction; n >= 1 && n <= total.value; n += direction) {\n if (!isPageDisabled.value(n)) return n\n }\n return undefined\n}\n\nconst prevTarget = computed(() => step(-1))\nconst nextTarget = computed(() => step(1))\nconst prevDisabled = computed(() => props.disabled || prevTarget.value === undefined)\nconst nextDisabled = computed(() => props.disabled || nextTarget.value === undefined)\n\nfunction goTo(n: number | undefined) {\n if (n === undefined) return\n page.value = clamp(n, 1, total.value)\n}\n\n/** The address a pill or a control links to, or `undefined` when the row is made of buttons. */\nfunction hrefFor(n: number): string | undefined {\n return props.href?.(n)\n}\n\n// @core\n/*\n * A link clicked with a modifier held (Ctrl or Cmd for a new tab, Shift for a window, Alt for a\n * download) opens somewhere ELSE: the page on screen stays where it is, so the model must not\n * move. The test the routers themselves make before taking a click over.\n */\nfunction opensElsewhere(event: MouseEvent): boolean {\n return (\n props.href !== undefined &&\n (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)\n )\n}\n\nfunction choose(event: MouseEvent, n: number | undefined) {\n if (n === undefined || opensElsewhere(event)) return\n emit('navigate', n, event)\n goTo(n)\n}\n\nfunction isUnusable(el: HTMLElement): boolean {\n return el.matches(':disabled, [aria-disabled=\"true\"]')\n}\n\n// @a11y\n/*\n * The previous and next controls disable themselves once no page is left in their direction,\n * and a focused button that becomes disabled hands the focus to <body>: a keyboard reader\n * pressing \"Next page\" on the second to last page would be sent back to the top of the document\n * by the next Tab. The focus goes to the page just reached instead, which is always rendered,\n * the current page being the one pill the responsive steps never hide.\n */\nasync function goFromControl(event: MouseEvent, n: number | undefined) {\n const control = event.currentTarget as HTMLElement\n const hadFocus = control === document.activeElement\n choose(event, n)\n if (!hadFocus) return\n await nextTick()\n if (isUnusable(control)) {\n navEl.value?.querySelector<HTMLElement>('[aria-current=\"page\"]')?.focus()\n }\n}\n\n/*\n * The previous and next controls, written once: the two differ only by their direction, and a\n * pair of template blocks drifting apart on the next change is exactly what this avoids. A\n * functional component because the two sit at either end of the row, with the pages in between,\n * where a `v-for` over two descriptors could not put them.\n */\nconst PageControl: FunctionalComponent<{ side: 'prev' | 'next' }> = ({ side }) => {\n const prev = side === 'prev'\n const label = prev ? resolvedPrevText.value : resolvedNextText.value\n const icon = prev ? props.prevIcon : props.nextIcon\n const target = prev ? prevTarget.value : nextTarget.value\n const disabled = prev ? prevDisabled.value : nextDisabled.value\n const common = {\n class: 'v-pagination-control',\n variant: props.itemVariant,\n disabled,\n href: hrefFor(target ?? currentPage.value),\n rel: props.href && !disabled ? side : undefined,\n onClick: (event: MouseEvent) => goFromControl(event, target),\n }\n const glyph = () => h(VIcon, { ...iconProps(icon), mirrored: true })\n if (props.controls === 'icon') return h(VIconButton, { ...common, label }, glyph)\n return h(\n VButton,\n { ...common, tone: 'neutral', 'aria-label': label },\n {\n ...(props.controls === 'both' ? { [prev ? 'start' : 'end']: glyph } : {}),\n default: () => h('span', { class: 'v-pagination-control-label' }, label),\n },\n )\n}\n\nconst navEl = ref<HTMLElement | null>(null)\n\n// @keyboard @a11y\n/**\n * The arrow keys, through the shared implementation in `utils/arrowNav`. The pills the\n * responsive rules have hidden are left out by the helper, which skips anything not displayed;\n * so they cannot be focused into.\n */\nfunction onKeydown(event: KeyboardEvent) {\n const nav = navEl.value\n if (!nav) return\n arrowNavigate(event, nav, () =>\n navigableItems(nav, '.v-pagination-page:not(:disabled, [aria-disabled=\"true\"])'),\n )\n}\n\n// The pills are rendered by a VButtonGroup inside the nav, so a template ref reaches none of\n// them. `focus` goes to the current page, which every responsive step keeps on screen.\ndefineExpose({\n // Two queries, not one selector list: a list matches in DOM order, which would hand the\n // focus to the previous control ahead of the current page. `a[href]` because an inert link\n // has lost its address, and with it the ability to take the focus.\n /** Moves the focus to the current page, or to the first control that can take it. */\n focus: (options?: FocusOptions) =>\n (\n navEl.value?.querySelector<HTMLElement>(\n '[aria-current=\"page\"]:is(button, a[href]):not(:disabled)',\n ) ?? navEl.value?.querySelector<HTMLElement>(':is(button, a[href]):not(:disabled)')\n )?.focus(options),\n /** The `<nav>` element, which is also where the consumer's attributes land. */\n el: navEl,\n})\n</script>\n\n<template>\n <nav\n ref=\"navEl\"\n class=\"v-pagination\"\n :aria-label=\"ariaLabel\"\n :data-align=\"align\"\n :data-item-variant=\"itemVariant\"\n :data-controls=\"controls || undefined\"\n :data-responsive=\"responsive ? '' : undefined\"\n @keydown=\"onKeydown\"\n >\n <!-- The row is a VButtonGroup either way: joined it merges the borders of its DIRECT\n button children, and detached it spaces them, which is all this prop asks of it.\n That is why there is no list markup wrapping the pills, and why the ellipsis is\n itself an inert button rather than a plain span: anything else between two pills\n would break the seam. -->\n <!--\n `|| undefined` keeps the group opinion-free when the row does not ask, `undefined` being\n what means \"no opinion\" there where `false` is an order. The size is always an opinion,\n the pagination having a default of its own.\n -->\n <VButtonGroup\n class=\"v-pagination-items\"\n :detached=\"detached\"\n :seamless=\"seamless\"\n :size=\"size\"\n :compact=\"compact || undefined\"\n :elevated=\"elevated || undefined\"\n >\n <PageControl v-if=\"controls\" side=\"prev\" />\n\n <template v-for=\"item in items\" :key=\"item.key\">\n <VButton\n v-if=\"item.kind === 'page'\"\n class=\"v-pagination-page\"\n :variant=\"item.page === currentPage ? selectedVariant : itemVariant\"\n :tone=\"item.page === currentPage ? tone : 'neutral'\"\n :disabled=\"disabled || isPageDisabled(item.page)\"\n :aria-label=\"pageLabelFor(item.page)\"\n :aria-current=\"item.page === currentPage ? 'page' : undefined\"\n :href=\"hrefFor(item.page)\"\n :data-edge=\"item.edge ? '' : undefined\"\n :data-distance=\"!item.edge && item.distance > 0 ? item.distance : undefined\"\n @click=\"choose($event, item.page)\"\n >\n {{ item.page }}\n </VButton>\n <!--\n The ellipsis is a disabled icon-only button rather than a span: being a button, it\n keeps the joined row's seam continuous and follows the size and density like\n everything else. Disabling it takes it out of the tab order, and it is hidden from\n screen readers, which have the page numbers themselves; which is also why it is a\n VButton and not a VIconButton, whose required label would name something nobody can\n reach.\n -->\n <VButton\n v-else\n class=\"v-pagination-ellipsis\"\n aria-hidden=\"true\"\n data-icon-only\n :variant=\"itemVariant\"\n tone=\"neutral\"\n disabled\n >\n <template #start>\n <VIcon :name=\"moreHorizIcon\" />\n </template>\n </VButton>\n </template>\n\n <PageControl v-if=\"controls\" side=\"next\" />\n </VButtonGroup>\n </nav>\n</template>\n\n<style>\n@layer vectis.components {\n .v-pagination {\n display: flex;\n }\n\n /*\n * A container of this kind computes its width WITHOUT looking at its content, so the row has\n * to be block-level: as an inline box it would measure zero and hide everything at once. It\n * therefore takes its parent's whole width, which is exactly what makes the hiding follow the\n * space the component was actually given, and what obliges a flex parent to grant it one.\n */\n .v-pagination[data-responsive] {\n container-type: inline-size;\n container-name: v-pagination;\n }\n\n .v-pagination[data-align='center'] {\n justify-content: center;\n }\n\n .v-pagination[data-align='end'] {\n justify-content: flex-end;\n }\n\n /*\n * Qualified by an attribute VButton always renders, which makes both of these beat that\n * button's own padding and its own transition whatever order the two sheets end up in; the\n * VIconButton idiom.\n */\n .v-pagination-page[data-size] {\n /* A one-digit pill is square, and widens by itself past that. The height variable\n is set by the shared size class on this very element, so this single rule covers\n all five sizes and their compact forms. */\n min-inline-size: var(--control-height);\n padding-inline: var(--vectis-space-2);\n\n /*\n * The highlight changes instantly, and this must not be \"restored for consistency\". VButton\n * fades `background-color` over `--vectis-duration-fast`, which is right for a button that\n * stays put and wrong here: truncation SHIFTS its window, so from one page to the next the\n * highlight keeps its slot but changes ELEMENT, and the outgoing and incoming pills are\n * both half-tinted for a few frames; read as a flicker, with nothing in the console.\n */\n transition: none;\n }\n\n /* The ellipsis is a disabled button only as a technical device; to the reader it is\n not a control that has been turned off, so it does not take the forbidden\n cursor. Qualified like the pills: VButton's disabled cursor weighs (0,2,0), and a bare\n class loses to it whatever the sheet order. */\n .v-pagination-ellipsis[data-size] {\n cursor: default;\n }\n\n /*\n * Each threshold is the width NEEDED to show that level, never what remains after hiding:\n * written the other way round the row overflows for the whole interval before the next step\n * takes effect. The most distant neighbours go first.\n */\n @container v-pagination (max-inline-size: 36rem) {\n .v-pagination[data-responsive] .v-pagination-page[data-distance='3'] {\n display: none;\n }\n }\n\n @container v-pagination (max-inline-size: 31rem) {\n .v-pagination[data-responsive] .v-pagination-page[data-distance='2'] {\n display: none;\n }\n\n /* It is never done when the control shows text alone: there would be nothing left to click. */\n .v-pagination[data-responsive][data-controls='both'] .v-pagination-control-label {\n display: none;\n }\n }\n\n @container v-pagination (max-inline-size: 25rem) {\n .v-pagination[data-responsive] .v-pagination-page[data-distance='1'] {\n display: none;\n }\n }\n\n /*\n * A disabled page greys its outline to a different token, hence a variable the second rule\n * changes. VToggle's `--toggle-frame` is the same device.\n */\n .v-pagination[data-item-variant='outline'] {\n --pagination-frame: var(--vectis-color-border-strong);\n }\n\n .v-pagination[data-item-variant='outline']\n > *\n > .v-pagination-page:is(:disabled, [aria-disabled='true']) {\n --pagination-frame: var(--vectis-color-border);\n }\n\n .v-pagination[data-item-variant='outline']\n > *\n > .v-pagination-page[aria-current='page']:is([data-variant='soft'], [data-variant='ghost']) {\n border-color: var(--pagination-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-pagination-page.v-pagination-page.v-pagination-page.v-pagination-page[aria-current='page']:not(\n :disabled,\n [aria-disabled='true']\n ) {\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":"VPagination.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../src/components/VPagination/VPagination.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @keyboard @core\n/**\n * The row of page numbers under a long list. Every pill is a VButton and the previous\n * and next controls are buttons too, so nothing about hovering, focusing or disabling\n * is written again here.\n *\n * A long list has more pages than fit on a line, and the row deals with that TWICE,\n * for two different reasons. First logically: `totalVisible` fixes how many slots are\n * rendered at all, and the pages that do not fit are replaced by an ellipsis — pure\n * arithmetic, identical on the server and in the browser. Then visually: as the space\n * actually available narrows, the neighbours of the current page are hidden one step\n * at a time, and that half is entirely CSS, the row asking about its own width rather\n * than being measured from code.\n *\n * Given `href`, every pill and both controls are links, so a list of products can be\n * paged by address: middle-clicked into a new tab, bookmarked, followed by a crawler.\n *\n * The only behavioural JavaScript is the keyboard, explained where it is written.\n */\nimport { computed, h, nextTick, ref } from 'vue'\nimport type { FunctionalComponent } from 'vue'\n\nimport VButton from '../VButton/VButton.vue'\nimport VButtonGroup from '../VButton/VButtonGroup.vue'\nimport VIcon from '../VIcon/VIcon.vue'\nimport { iconProps } from '../VIcon/iconProps'\nimport { chevron_left as chevronLeftIcon } from '../VIcon/icons/chevron_left'\nimport { chevron_right as chevronRightIcon } from '../VIcon/icons/chevron_right'\nimport { more_horiz as moreHorizIcon } from '../VIcon/icons/more_horiz'\nimport type { IconSource } from '../VIcon/types'\nimport VIconButton from '../VIconButton/VIconButton.vue'\n\nimport { arrowNavigate, navigableItems } from '../../utils/arrowNav'\nimport { clamp } from '../../utils/number'\nimport { resolveMatcher } from '../../utils/matcher'\n\nimport { useAriaLabel } from '../../composables/useAriaLabel'\nimport { useMessages } from '../../i18n/state'\n\n/** How the pages that are not the current one are drawn. */\nexport type PaginationItemVariant = 'ghost' | 'outline'\n\n/** How the current page is drawn. */\nexport type PaginationSelectedVariant = 'solid' | 'soft' | 'ghost'\n\n/** The colour of the current page. */\nexport type PaginationTone = 'accent' | 'neutral' | 'danger'\n\n/** The height of the pills, from the scale every control shares. */\nexport type PaginationSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'\n\n/** Where the row sits in the width it is given. */\nexport type PaginationAlign = 'start' | 'center' | 'end'\n\n/** Whether the previous and next controls are rendered, and what they show. */\nexport type PaginationControls = false | 'icon' | 'text' | 'both'\n\n/**\n * Which pages cannot be reached, given either as a list of page numbers or as a function\n * answering that question for a page.\n */\nexport type PaginationMatcher = number[] | ((page: number) => boolean)\n\ninterface PaginationProps {\n /**\n * How many pages there are in all. It is 1 by default, which renders a single page: the\n * real count almost always has to be given.\n */\n length?: number\n /**\n * How many slots to render, ellipses counted among them, so the row keeps exactly\n * the same width whichever page is current. Below five there would be nothing left\n * to show around the current page, so five is the effective minimum. Left out, every\n * page is rendered.\n */\n totalVisible?: number\n\n /**\n * Separates the buttons instead of joining them into one segmented control. It is the\n * word VButtonGroup and VToggle use for the same question, in the same direction.\n */\n detached?: boolean\n /**\n * Takes the lines out from between the joined buttons, so the row reads as one frame rather\n * than as segments, on the terms of VButtonGroup's own `seamless`. It has no effect under\n * `detached`.\n */\n seamless?: boolean\n /**\n * How the pages OTHER than the current one, and the controls, are drawn. What the current\n * page takes is `selectedVariant`. 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?: PaginationItemVariant\n /**\n * How the current page is drawn, in the row's tone: filled, tinted, or the colour of its\n * text alone. The same choice as VToggle's `selectedVariant`.\n */\n selectedVariant?: PaginationSelectedVariant\n /** The colour the current page takes. The other pages and the controls stay neutral. */\n tone?: PaginationTone\n /** The height of the buttons, from the scale shared by every control. */\n size?: PaginationSize\n /** Takes 4px off the height of every button. */\n compact?: boolean\n /**\n * Raises the row off the page, on the terms of VButton's own `elevated`. Joined, the\n * shadow belongs to the ROW and not to each pill, which is what stops it falling into\n * the joints; detached, every button carries its own.\n */\n elevated?: boolean\n /**\n * Where the row sits in the space it is given. It only matters in responsive mode,\n * where the row takes the whole width available.\n */\n align?: PaginationAlign\n\n /**\n * The previous and next buttons on either side of the pages: what they show, or `false`\n * to leave them out. One prop rather than two, the shape VFilePicker's `preview` and\n * VCarousel's `controls` already use.\n */\n controls?: PaginationControls\n /** The icon of the previous control: an icon name, or an explicit render. */\n prevIcon?: IconSource\n /** The icon of the next control: an icon name, or an explicit render. */\n nextIcon?: IconSource\n /**\n * The wording of the previous control, used both as its visible text and as what\n * screen readers announce. It falls back to the design system dictionary.\n */\n prevText?: string\n /**\n * The wording of the next control, used both as its visible text and as what screen\n * readers announce. It falls back to the design system dictionary.\n */\n nextText?: string\n\n /** Makes the whole component unusable. */\n disabled?: boolean\n /**\n * Which pages cannot be reached, as a list or as a function. The previous and next\n * controls step OVER them rather than stopping at one.\n */\n disabledPages?: PaginationMatcher\n\n /**\n * Lets the row shed pages as the space narrows, by asking about its own width. It is\n * off by default, because it makes the row take the full width available.\n */\n responsive?: boolean\n\n /**\n * What screen readers announce for the navigation itself. It falls back to the\n * design system dictionary.\n */\n label?: string\n /**\n * How a page is announced. A pill shows a bare number, which alone means nothing to\n * a screen reader: this is what turns it into \"Page 3\". It falls back to the design\n * system dictionary.\n */\n pageLabel?: (page: number) => string\n /**\n * The address of a page. Given, every pill and both controls render as links, the\n * previous and next ones carrying `rel=\"prev\"` and `rel=\"next\"`; a click still updates\n * the model, then the browser follows the link. Left out, they are buttons.\n */\n href?: (page: number) => string\n}\n\nconst props = withDefaults(defineProps<PaginationProps>(), {\n length: 1,\n totalVisible: undefined,\n detached: false,\n seamless: false,\n itemVariant: 'ghost',\n selectedVariant: 'solid',\n tone: 'accent',\n size: 'md',\n compact: false,\n elevated: false,\n align: 'start',\n controls: 'icon',\n prevIcon: () => chevronLeftIcon,\n nextIcon: () => chevronRightIcon,\n prevText: undefined,\n nextText: undefined,\n disabled: false,\n disabledPages: undefined,\n responsive: false,\n label: undefined,\n pageLabel: undefined,\n href: undefined,\n})\n\nconst emit = defineEmits<{\n /**\n * A page was chosen from the row, a pill or a control, with the click that chose it. It\n * comes before the model changes, so under `href` a single-page application calls\n * `preventDefault()` on the event and hands the address to its router instead of letting\n * the browser load it. A click that opens the link elsewhere (a modifier key held) is not\n * a choice made here and emits nothing.\n */\n navigate: [page: number, event: MouseEvent]\n}>()\n\n// The prop wins over the dictionary, and above both, a consumer's own aria-label or\n// aria-labelledby still wins — that arbitration is what `useAriaLabel` is for.\nconst m = useMessages()\nconst ariaLabel = useAriaLabel(() => props.label ?? m.value.pagination.label)\nconst resolvedPrevText = computed(() => props.prevText ?? m.value.pagination.previous)\nconst resolvedNextText = computed(() => props.nextText ?? m.value.pagination.next)\n\n/** The page being shown, counted from 1. It starts on the first. */\nconst page = defineModel<number>({ default: 1 })\n\n/** One slot in the row: either a page, or the ellipsis standing for those left out. */\ntype PaginationItem =\n | { kind: 'page'; key: string; page: number; edge: boolean; distance: number }\n | { kind: 'gap'; key: string }\n\n// A length or a model that is not a whole number (`NaN` while a page size is still loading,\n// a computed `2.5`) is brought back to a page that exists, or no pill would ever match it.\nconst total = computed(() => Math.max(Math.trunc(props.length) || 1, 1))\nconst currentPage = computed(() => clamp(Math.round(page.value) || 1, 1, total.value))\n\nconst isPageDisabled = computed(() => resolveMatcher(props.disabledPages))\n\n// @a11y\n// A pill shows nothing but a number, which on its own tells a screen reader\n// nothing at all; this is what has it announced as \"Page 3\".\nfunction pageLabelFor(n: number): string {\n return props.pageLabel ? props.pageLabel(n) : m.value.pagination.page(n)\n}\n\n/**\n * Works out which slots the row holds.\n *\n * The count is CONSTANT: `totalVisible` counts everything rendered, the ellipses\n * included, so the row never changes width as one moves through the pages. The window\n * of pages around the current one is centred on it, and near either end it SHIFTS\n * rather than shrinking — which is what keeps that count constant there too.\n *\n * Each page also carries how far it is from the current one. That distance is what the\n * responsive hiding sorts by, and it is capped at three: past that, the most distant\n * neighbours all disappear together at the first step.\n */\nconst items = computed<PaginationItem[]>(() => {\n const count = total.value\n const current = currentPage.value\n\n const pageItem = (n: number): PaginationItem => ({\n kind: 'page',\n key: `page-${n}`,\n page: n,\n edge: n === 1 || n === count,\n distance: Math.min(Math.abs(n - current), 3),\n })\n const pages = (from: number, to: number): PaginationItem[] => {\n const out: PaginationItem[] = []\n for (let n = from; n <= to; n++) out.push(pageItem(n))\n return out\n }\n // Keyed by the side it stands on, never by the page it follows: there is at most one\n // ellipsis per side, and the left one always comes after the first page while the right\n // one always comes before the last. A key derived from the window would change on every\n // navigation, so the right-hand ellipsis would be destroyed and rebuilt each time the\n // window slid, for a node that never changes.\n const gap = (side: 'start' | 'end'): PaginationItem => ({ kind: 'gap', key: `gap-${side}` })\n\n // With no limit given, nothing is left out. Below five slots there would be no room\n // for the first page, an ellipsis, the current page, another ellipsis and the last,\n // so that is the floor.\n const limit = Math.trunc(props.totalVisible ?? count)\n const visible = Number.isNaN(limit) ? count : Math.max(limit, 5)\n if (visible >= count) return pages(1, count)\n\n const start = current - Math.floor((visible - 5) / 2)\n const end = start + (visible - 5)\n\n // Close to either end, an ellipsis on that side would stand for no missing page at\n // all. The window is stretched to the bound instead, which both avoids that and\n // keeps the number of slots exactly the same.\n if (start <= 3) return [...pages(1, visible - 2), gap('end'), pageItem(count)]\n if (end >= count - 2) return [pageItem(1), gap('start'), ...pages(count - visible + 3, count)]\n return [pageItem(1), gap('start'), ...pages(start, end), gap('end'), pageItem(count)]\n})\n\n/**\n * Where a control leads: the nearest page in that direction that can actually be\n * reached, disabled ones being stepped over rather than stopped at.\n *\n * Answering `undefined` means there is none left, which disables the control — and\n * that single answer also covers being on the first or the last page, with no separate\n * test for the ends.\n */\nfunction step(direction: -1 | 1): number | undefined {\n for (let n = currentPage.value + direction; n >= 1 && n <= total.value; n += direction) {\n if (!isPageDisabled.value(n)) return n\n }\n return undefined\n}\n\nconst prevTarget = computed(() => step(-1))\nconst nextTarget = computed(() => step(1))\nconst prevDisabled = computed(() => props.disabled || prevTarget.value === undefined)\nconst nextDisabled = computed(() => props.disabled || nextTarget.value === undefined)\n\nfunction goTo(n: number | undefined) {\n if (n === undefined) return\n page.value = clamp(n, 1, total.value)\n}\n\n/** The address a pill or a control links to, or `undefined` when the row is made of buttons. */\nfunction hrefFor(n: number): string | undefined {\n return props.href?.(n)\n}\n\n// @core\n/*\n * A link clicked with a modifier held (Ctrl or Cmd for a new tab, Shift for a window, Alt for a\n * download) opens somewhere ELSE: the page on screen stays where it is, so the model must not\n * move. The test the routers themselves make before taking a click over.\n */\nfunction opensElsewhere(event: MouseEvent): boolean {\n return (\n props.href !== undefined &&\n (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)\n )\n}\n\nfunction choose(event: MouseEvent, n: number | undefined) {\n if (n === undefined || opensElsewhere(event)) return\n emit('navigate', n, event)\n goTo(n)\n}\n\n/** Whether a pill or a control can no longer be used: a disabled button, or an inert link. */\nfunction isUnusable(el: HTMLElement): boolean {\n return el.matches(':disabled, [aria-disabled=\"true\"]')\n}\n\n// @a11y\n/*\n * The previous and next controls disable themselves once no page is left in their direction,\n * and a focused button that becomes disabled hands the focus to <body>: a keyboard reader\n * pressing \"Next page\" on the second to last page would be sent back to the top of the\n * document by the next Tab. The focus goes to the page just reached instead, which is always\n * rendered, the current page being the one pill the responsive steps never hide.\n *\n * The focus is read BEFORE the change: a pointer click may not focus a button at all (Safari),\n * and a control that did not hold the focus has nothing to hand on.\n */\nasync function goFromControl(event: MouseEvent, n: number | undefined) {\n const control = event.currentTarget as HTMLElement\n const hadFocus = control === document.activeElement\n choose(event, n)\n if (!hadFocus) return\n await nextTick()\n if (isUnusable(control)) {\n navEl.value?.querySelector<HTMLElement>('[aria-current=\"page\"]')?.focus()\n }\n}\n\n/*\n * The previous and next controls, written ONCE: the two differ only by their direction, and\n * a pair of template blocks drifting apart on the next change is exactly what this avoids.\n * A functional component because the two sit at either end of the row, with the pages in\n * between, where a `v-for` over two descriptors could not put them.\n *\n * The icon goes on the side the control points to — before the word going back, after it\n * going forward — and every icon mirrors in a right-to-left page, an arrow pointing at a\n * physical direction.\n *\n * The control is named explicitly even though its label is visible: at narrow widths that\n * label is hidden and only the icon remains, and the name a screen reader announces has to\n * survive that.\n *\n * As a link it points at the page it leads to. With none left it still receives an address\n * (the current page's), which VButton drops once disabled: the control stays the same `<a>`,\n * turned inert, rather than swapping to a `<button>` under a focus `goFromControl` has to read.\n * The `rel` goes with the address, an inert link leading nowhere.\n */\nconst PageControl: FunctionalComponent<{ side: 'prev' | 'next' }> = ({ side }) => {\n const prev = side === 'prev'\n const label = prev ? resolvedPrevText.value : resolvedNextText.value\n const icon = prev ? props.prevIcon : props.nextIcon\n const target = prev ? prevTarget.value : nextTarget.value\n const disabled = prev ? prevDisabled.value : nextDisabled.value\n const common = {\n class: 'v-pagination-control',\n variant: props.itemVariant,\n disabled,\n href: hrefFor(target ?? currentPage.value),\n rel: props.href && !disabled ? side : undefined,\n onClick: (event: MouseEvent) => goFromControl(event, target),\n }\n const glyph = () => h(VIcon, { ...iconProps(icon), mirrored: true })\n if (props.controls === 'icon') return h(VIconButton, { ...common, label }, glyph)\n return h(\n VButton,\n { ...common, tone: 'neutral', 'aria-label': label },\n {\n ...(props.controls === 'both' ? { [prev ? 'start' : 'end']: glyph } : {}),\n default: () => h('span', { class: 'v-pagination-control-label' }, label),\n },\n )\n}\n\nconst navEl = ref<HTMLElement | null>(null)\n\n// @keyboard @a11y\n/**\n * The arrow keys, through the shared implementation in `utils/arrowNav`. Tab is left\n * alone: every visible pill remains a stop in the tab order, as in any list of links.\n *\n * The pills the responsive rules have hidden are left out by the helper, which skips\n * anything not displayed — so they cannot be focused into.\n */\nfunction onKeydown(event: KeyboardEvent) {\n const nav = navEl.value\n if (!nav) return\n arrowNavigate(event, nav, () =>\n navigableItems(nav, '.v-pagination-page:not(:disabled, [aria-disabled=\"true\"])'),\n )\n}\n\n// The pills are rendered by a VButtonGroup inside the nav, so a template ref reaches none of\n// them. `focus` goes to the current page, which every responsive step keeps on screen.\ndefineExpose({\n // Two queries, not one selector list: a list matches in DOM order, which would hand the\n // focus to the previous control ahead of the current page. `a[href]` because an inert link\n // has lost its address, and with it the ability to take the focus.\n /** Moves the focus to the current page, or to the first control that can take it. */\n focus: (options?: FocusOptions) =>\n (\n navEl.value?.querySelector<HTMLElement>(\n '[aria-current=\"page\"]:is(button, a[href]):not(:disabled)',\n ) ?? navEl.value?.querySelector<HTMLElement>(':is(button, a[href]):not(:disabled)')\n )?.focus(options),\n /** The `<nav>` element, which is also where the consumer's attributes land. */\n el: navEl,\n})\n</script>\n\n<template>\n <nav\n ref=\"navEl\"\n class=\"v-pagination\"\n :aria-label=\"ariaLabel\"\n :data-align=\"align\"\n :data-item-variant=\"itemVariant\"\n :data-controls=\"controls || undefined\"\n :data-responsive=\"responsive ? '' : undefined\"\n @keydown=\"onKeydown\"\n >\n <!-- The row is a VButtonGroup either way: joined it merges the borders of its DIRECT\n button children, and detached it spaces them, which is all this prop asks of it.\n That is why there is no list markup wrapping the pills, and why the ellipsis is\n itself an inert button rather than a plain span: anything else between two pills\n would break the seam. -->\n <!--\n The size, the density and the elevation are handed to the GROUP, which gives them to\n every button inside, rather than repeated on each child. The elevation has to be there\n in any case, since that is where VButtonGroup draws it: joined, the row takes the shadow\n and the segments give theirs up, or it would fall into every joint.\n\n `|| undefined` keeps the group opinion-free when the row does not ask, `undefined`\n being what means \"no opinion\" there where `false` is an order. The size is always an\n opinion, the pagination having a default of its own.\n -->\n <VButtonGroup\n class=\"v-pagination-items\"\n :detached=\"detached\"\n :seamless=\"seamless\"\n :size=\"size\"\n :compact=\"compact || undefined\"\n :elevated=\"elevated || undefined\"\n >\n <PageControl v-if=\"controls\" side=\"prev\" />\n\n <template v-for=\"item in items\" :key=\"item.key\">\n <VButton\n v-if=\"item.kind === 'page'\"\n class=\"v-pagination-page\"\n :variant=\"item.page === currentPage ? selectedVariant : itemVariant\"\n :tone=\"item.page === currentPage ? tone : 'neutral'\"\n :disabled=\"disabled || isPageDisabled(item.page)\"\n :aria-label=\"pageLabelFor(item.page)\"\n :aria-current=\"item.page === currentPage ? 'page' : undefined\"\n :href=\"hrefFor(item.page)\"\n :data-edge=\"item.edge ? '' : undefined\"\n :data-distance=\"!item.edge && item.distance > 0 ? item.distance : undefined\"\n @click=\"choose($event, item.page)\"\n >\n {{ item.page }}\n </VButton>\n <!-- The ellipsis is a disabled icon-only button rather than a span: being a button,\n it keeps the joined row's seam continuous and follows the size and density like\n everything else. Disabling it takes it out of the tab order, and it is hidden\n from screen readers, which have the page numbers themselves — which is also why\n it is a VButton and not a VIconButton, whose required label would name something\n nobody can reach. -->\n <VButton\n v-else\n class=\"v-pagination-ellipsis\"\n aria-hidden=\"true\"\n data-icon-only\n :variant=\"itemVariant\"\n tone=\"neutral\"\n disabled\n >\n <template #start>\n <VIcon :name=\"moreHorizIcon\" />\n </template>\n </VButton>\n </template>\n\n <PageControl v-if=\"controls\" side=\"next\" />\n </VButtonGroup>\n </nav>\n</template>\n\n<style>\n@layer vectis.components {\n .v-pagination {\n display: flex;\n }\n\n /*\n * Making the row a query container is reserved for responsive mode, since every\n * query below depends on it — and it is not free.\n *\n * A container of this kind computes its width WITHOUT looking at its content, so the\n * row has to be block-level: as an inline box it would measure zero and hide\n * everything at once. It therefore takes its parent's whole width, which is exactly\n * what makes the hiding follow the space the component was actually given, and what\n * obliges a flex parent to grant it one.\n *\n * Outside responsive mode the row keeps a width of its own and sits like any other\n * content. Either way the alignment is asked for explicitly and never inferred.\n */\n .v-pagination[data-responsive] {\n container-type: inline-size;\n container-name: v-pagination;\n }\n\n .v-pagination[data-align='center'] {\n justify-content: center;\n }\n\n .v-pagination[data-align='end'] {\n justify-content: flex-end;\n }\n\n /* Qualified by an attribute VButton always renders, which is what makes both of these\n beat that button's own padding and its own transition whatever order the two sheets\n end up in — the VIconButton idiom. */\n .v-pagination-page[data-size] {\n /* A one-digit pill is square, and widens by itself past that. The height variable\n is set by the shared size class on this very element, so this single rule covers\n all five sizes and their compact forms. */\n min-inline-size: var(--control-height);\n padding-inline: var(--vectis-space-2);\n\n /*\n * TRAP — the highlight changes instantly, and this must not be \"restored for\n * consistency\". VButton fades `background-color` over `--vectis-duration-fast`, which is\n * right for a button that stays put and wrong here: truncation SHIFTS its window, so from\n * one page to the next the highlight keeps its slot but changes ELEMENT, and the outgoing\n * and incoming pills are both half-tinted for a few frames — read as a flicker, with\n * nothing in the console.\n *\n * No duration avoids it: symmetric fades overlap, asymmetric ones leave either no\n * highlight or a trailing one. Keeping the fade for hover alone is not expressible\n * either — a transition resolves from the state being ENTERED, and \"no longer current\"\n * is the same state as \"no longer hovered\". So the hover fade goes too, on the pills\n * only; the controls never change active state and keep theirs.\n */\n transition: none;\n }\n\n /* The ellipsis is a disabled button only as a technical device; to the reader it is\n not a control that has been turned off, so it does not take the forbidden\n cursor. Qualified like the pills: VButton's disabled cursor weighs (0,2,0), and a bare\n class loses to it whatever the sheet order. */\n .v-pagination-ellipsis[data-size] {\n cursor: default;\n }\n\n /*\n * The responsive half of the truncation. The nav queries ITS OWN width, so the steps follow\n * the space the component was given rather than the viewport — a sidebar and a full-width\n * page behave differently, as they should, with nothing measured from code.\n *\n * The thresholds are rem LITERALS, a container query accepting no variables, and are\n * calibrated on `md`: a pill and a control are each 2.5rem plus a 0.25rem gutter, so\n * 2.75rem apiece — thirteen slots need ~35.5rem, eleven ~30, nine ~24.5, hence the three\n * with a little margin. The extremes (the largest size, five-digit page numbers) are what\n * `totalVisible` and `responsive: false` are for.\n *\n * Each threshold is the width NEEDED to show that level, never what remains after hiding:\n * written the other way round the row overflows for the whole interval before the next\n * step takes effect.\n *\n * The most distant neighbours go first. The edges and the current page carry no distance,\n * so no rule here can reach them. `display: none` also takes a hidden pill out of the tab\n * order and the a11y tree, which is intended. No ellipsis replaces one either — it is\n * exactly as wide as the pill it would stand for, so it frees nothing.\n */\n @container v-pagination (max-inline-size: 36rem) {\n .v-pagination[data-responsive] .v-pagination-page[data-distance='3'] {\n display: none;\n }\n }\n\n @container v-pagination (max-inline-size: 31rem) {\n .v-pagination[data-responsive] .v-pagination-page[data-distance='2'] {\n display: none;\n }\n\n /* A control showing both its icon and its label is about four times as wide as a\n pill, so dropping its label frees more room than sacrificing another page —\n which is why it happens before the last step. It is never done when the control\n shows text alone: there would be nothing left to click. */\n .v-pagination[data-responsive][data-controls='both'] .v-pagination-control-label {\n display: none;\n }\n }\n\n @container v-pagination (max-inline-size: 25rem) {\n .v-pagination[data-responsive] .v-pagination-page[data-distance='1'] {\n display: none;\n }\n }\n\n /*\n * The frame of an `outline` row: the colour its other pages already paint. They are drawn\n * in the NEUTRAL tone, so this is what their `--tone-border-soft` resolves to; naming that\n * variable here instead would read the CURRENT page's tone and tint one segment of the\n * frame accent or red. A disabled page greys its outline to a different token, hence a\n * variable the second rule changes. VToggle's `--toggle-frame` is the same device.\n *\n * `soft` and `ghost` leave VButton's border transparent, which in an outline row opens a\n * gap in the frame for the whole width of the current page (the `SelectedVariants` play\n * function). Restoring it on all four sides keeps the frame closed wherever the current\n * page sits; seamless, the shared edges are cleared again by VButtonGroup's own (0,6,0)\n * rules, which these (0,5,0) ones stay below.\n */\n .v-pagination[data-item-variant='outline'] {\n --pagination-frame: var(--vectis-color-border-strong);\n }\n\n .v-pagination[data-item-variant='outline']\n > *\n > .v-pagination-page:is(:disabled, [aria-disabled='true']) {\n --pagination-frame: var(--vectis-color-border);\n }\n\n .v-pagination[data-item-variant='outline']\n > *\n > .v-pagination-page[aria-current='page']:is([data-variant='soft'], [data-variant='ghost']) {\n border-color: var(--pagination-frame);\n }\n\n /*\n * Windows forced colors replace every background with the page's, so the current page 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-pagination-page.v-pagination-page.v-pagination-page.v-pagination-page[aria-current='page']:not(\n :disabled,\n [aria-disabled='true']\n ) {\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":"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"}
|
|
1
|
+
{"version":3,"file":"VPagination.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../src/components/VPagination/VPagination.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @keyboard @core\n/**\n * Native buttons activate pages. JavaScript builds bounded page windows and provides roving\n * keyboard navigation.\n */\nimport { computed, h, nextTick, ref } from 'vue'\nimport type { FunctionalComponent } from 'vue'\n\nimport VButton from '../VButton/VButton.vue'\nimport VButtonGroup from '../VButton/VButtonGroup.vue'\nimport VIcon from '../VIcon/VIcon.vue'\nimport { iconProps } from '../VIcon/iconProps'\nimport { chevron_left as chevronLeftIcon } from '../VIcon/icons/chevron_left'\nimport { chevron_right as chevronRightIcon } from '../VIcon/icons/chevron_right'\nimport { more_horiz as moreHorizIcon } from '../VIcon/icons/more_horiz'\nimport type { IconSource } from '../VIcon/types'\nimport VIconButton from '../VIconButton/VIconButton.vue'\n\nimport { arrowNavigate, navigableItems } from '../../utils/arrowNav'\nimport { clamp } from '../../utils/number'\nimport { resolveMatcher } from '../../utils/matcher'\n\nimport { useAriaLabel } from '../../composables/useAriaLabel'\nimport { useMessages } from '../../i18n/state'\n\n/** How the pages that are not the current one are drawn. */\nexport type PaginationItemVariant = 'ghost' | 'outline'\n\n/** How the current page is drawn. */\nexport type PaginationSelectedVariant = 'solid' | 'soft' | 'ghost'\n\n/** The colour of the current page. */\nexport type PaginationTone = 'accent' | 'neutral' | 'danger'\n\n/** The height of the pills, from the scale every control shares. */\nexport type PaginationSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'\n\n/** Where the row sits in the width it is given. */\nexport type PaginationAlign = 'start' | 'center' | 'end'\n\n/** Whether the previous and next controls are rendered, and what they show. */\nexport type PaginationControls = false | 'icon' | 'text' | 'both'\n\n/**\n * Which pages cannot be reached, given either as a list of page numbers or as a function\n * answering that question for a page.\n */\nexport type PaginationMatcher = number[] | ((page: number) => boolean)\n\ninterface PaginationProps {\n /**\n * How many pages there are in all. It is 1 by default, which renders a single page: the\n * real count almost always has to be given.\n */\n length?: number\n /**\n * How many slots to render, ellipses counted among them, so the row keeps exactly the same\n * width whichever page is current. Left out, every page is rendered.\n */\n totalVisible?: number\n\n /**\n * Separates the buttons instead of joining them into one segmented control. It is the\n * word VButtonGroup and VToggle use for the same question, in the same direction.\n */\n detached?: boolean\n /**\n * Takes the lines out from between the joined buttons, so the row reads as one frame rather\n * than as segments, on the terms of VButtonGroup's own `seamless`. It has no effect under\n * `detached`.\n */\n seamless?: boolean\n /**\n * How the pages OTHER than the current one, and the controls, are drawn. It is named for the\n * ITEMS because that is what it paints: on VTabs and VDataTable `variant` names the\n * decoration of the frame instead.\n */\n itemVariant?: PaginationItemVariant\n /**\n * How the current page is drawn, in the row's tone: filled, tinted, or the colour of its\n * text alone. The same choice as VToggle's `selectedVariant`.\n */\n selectedVariant?: PaginationSelectedVariant\n /** The colour the current page takes. The other pages and the controls stay neutral. */\n tone?: PaginationTone\n /** The height of the buttons, from the scale shared by every control. */\n size?: PaginationSize\n /** Takes 4px off the height of every button. */\n compact?: boolean\n /** Raises the row off the page, on the terms of VButton's own `elevated`. */\n elevated?: boolean\n /**\n * Where the row sits in the space it is given. It only matters in responsive mode,\n * where the row takes the whole width available.\n */\n align?: PaginationAlign\n\n /**\n * The previous and next buttons on either side of the pages: what they show, or `false` to\n * leave them out.\n */\n controls?: PaginationControls\n /** The icon of the previous control: an icon name, or an explicit render. */\n prevIcon?: IconSource\n /** The icon of the next control: an icon name, or an explicit render. */\n nextIcon?: IconSource\n /**\n * The wording of the previous control, used both as its visible text and as what\n * screen readers announce. It falls back to the design system dictionary.\n */\n prevText?: string\n /**\n * The wording of the next control, used both as its visible text and as what screen\n * readers announce. It falls back to the design system dictionary.\n */\n nextText?: string\n\n /** Makes the whole component unusable. */\n disabled?: boolean\n /**\n * Which pages cannot be reached, as a list or as a function. The previous and next\n * controls step OVER them rather than stopping at one.\n */\n disabledPages?: PaginationMatcher\n\n /**\n * Lets the row shed pages as the space narrows, by asking about its own width. It is\n * off by default, because it makes the row take the full width available.\n */\n responsive?: boolean\n\n /**\n * What screen readers announce for the navigation itself. It falls back to the\n * design system dictionary.\n */\n label?: string\n /**\n * How a page is announced. A pill shows a bare number, which alone means nothing to\n * a screen reader: this is what turns it into \"Page 3\". It falls back to the design\n * system dictionary.\n */\n pageLabel?: (page: number) => string\n /** The address of a page. Left out, they are buttons. */\n href?: (page: number) => string\n}\n\nconst props = withDefaults(defineProps<PaginationProps>(), {\n length: 1,\n totalVisible: undefined,\n detached: false,\n seamless: false,\n itemVariant: 'ghost',\n selectedVariant: 'solid',\n tone: 'accent',\n size: 'md',\n compact: false,\n elevated: false,\n align: 'start',\n controls: 'icon',\n prevIcon: () => chevronLeftIcon,\n nextIcon: () => chevronRightIcon,\n prevText: undefined,\n nextText: undefined,\n disabled: false,\n disabledPages: undefined,\n responsive: false,\n label: undefined,\n pageLabel: undefined,\n href: undefined,\n})\n\nconst emit = defineEmits<{\n /**\n * A page was chosen from the row, a pill or a control, with the click that chose it. It comes\n * before the model changes, so under `href` a single-page application calls\n * `preventDefault()` on the event and hands the address to its router instead of letting the\n * browser load it.\n */\n navigate: [page: number, event: MouseEvent]\n}>()\n\n// The prop wins over the dictionary, and above both, a consumer's own aria-label or\n// aria-labelledby still wins; that arbitration is what `useAriaLabel` is for.\nconst m = useMessages()\nconst ariaLabel = useAriaLabel(() => props.label ?? m.value.pagination.label)\nconst resolvedPrevText = computed(() => props.prevText ?? m.value.pagination.previous)\nconst resolvedNextText = computed(() => props.nextText ?? m.value.pagination.next)\n\n/** The page being shown, counted from 1. It starts on the first. */\nconst page = defineModel<number>({ default: 1 })\n\n/** One slot in the row: either a page, or the ellipsis standing for those left out. */\ntype PaginationItem =\n | { kind: 'page'; key: string; page: number; edge: boolean; distance: number }\n | { kind: 'gap'; key: string }\n\n// A length or a model that is not a whole number (`NaN` while a page size is still loading,\n// a computed `2.5`) is brought back to a page that exists, or no pill would ever match it.\nconst total = computed(() => Math.max(Math.trunc(props.length) || 1, 1))\nconst currentPage = computed(() => clamp(Math.round(page.value) || 1, 1, total.value))\n\nconst isPageDisabled = computed(() => resolveMatcher(props.disabledPages))\n\n// @a11y\n// A pill shows nothing but a number, which on its own tells a screen reader\n// nothing at all; this is what has it announced as \"Page 3\".\nfunction pageLabelFor(n: number): string {\n return props.pageLabel ? props.pageLabel(n) : m.value.pagination.page(n)\n}\n\n/** Works out which slots the row holds. */\nconst items = computed<PaginationItem[]>(() => {\n const count = total.value\n const current = currentPage.value\n\n const pageItem = (n: number): PaginationItem => ({\n kind: 'page',\n key: `page-${n}`,\n page: n,\n edge: n === 1 || n === count,\n distance: Math.min(Math.abs(n - current), 3),\n })\n const pages = (from: number, to: number): PaginationItem[] => {\n const out: PaginationItem[] = []\n for (let n = from; n <= to; n++) out.push(pageItem(n))\n return out\n }\n // Keyed by the side it stands on, never by the page it follows: there is at most one\n // ellipsis per side, and the left one always comes after the first page while the right\n // one always comes before the last. A key derived from the window would change on every\n // navigation, so the right-hand ellipsis would be destroyed and rebuilt each time the\n // window slid, for a node that never changes.\n const gap = (side: 'start' | 'end'): PaginationItem => ({ kind: 'gap', key: `gap-${side}` })\n\n // With no limit given, nothing is left out. Below five slots there would be no room\n // for the first page, an ellipsis, the current page, another ellipsis and the last,\n // so that is the floor.\n const limit = Math.trunc(props.totalVisible ?? count)\n const visible = Number.isNaN(limit) ? count : Math.max(limit, 5)\n if (visible >= count) return pages(1, count)\n\n const start = current - Math.floor((visible - 5) / 2)\n const end = start + (visible - 5)\n\n // The window is stretched to the bound instead, which both avoids that and keeps the number\n // of slots exactly the same.\n if (start <= 3) return [...pages(1, visible - 2), gap('end'), pageItem(count)]\n if (end >= count - 2) return [pageItem(1), gap('start'), ...pages(count - visible + 3, count)]\n return [pageItem(1), gap('start'), ...pages(start, end), gap('end'), pageItem(count)]\n})\n\n/**\n * Where a control leads: the nearest page in that direction that can actually be reached,\n * disabled ones being stepped over rather than stopped at. Answering `undefined` means there is\n * none left, which disables the control; and that single answer also covers being on the first\n * or the last page, with no separate test for the ends.\n */\nfunction step(direction: -1 | 1): number | undefined {\n for (let n = currentPage.value + direction; n >= 1 && n <= total.value; n += direction) {\n if (!isPageDisabled.value(n)) return n\n }\n return undefined\n}\n\nconst prevTarget = computed(() => step(-1))\nconst nextTarget = computed(() => step(1))\nconst prevDisabled = computed(() => props.disabled || prevTarget.value === undefined)\nconst nextDisabled = computed(() => props.disabled || nextTarget.value === undefined)\n\nfunction goTo(n: number | undefined) {\n if (n === undefined) return\n page.value = clamp(n, 1, total.value)\n}\n\n/** The address a pill or a control links to, or `undefined` when the row is made of buttons. */\nfunction hrefFor(n: number): string | undefined {\n return props.href?.(n)\n}\n\n// @core\n/*\n * A link clicked with a modifier held (Ctrl or Cmd for a new tab, Shift for a window, Alt for a\n * download) opens somewhere ELSE: the page on screen stays where it is, so the model must not\n * move. The test the routers themselves make before taking a click over.\n */\nfunction opensElsewhere(event: MouseEvent): boolean {\n return (\n props.href !== undefined &&\n (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)\n )\n}\n\nfunction choose(event: MouseEvent, n: number | undefined) {\n if (n === undefined || opensElsewhere(event)) return\n emit('navigate', n, event)\n goTo(n)\n}\n\nfunction isUnusable(el: HTMLElement): boolean {\n return el.matches(':disabled, [aria-disabled=\"true\"]')\n}\n\n// @a11y\n/*\n * The previous and next controls disable themselves once no page is left in their direction,\n * and a focused button that becomes disabled hands the focus to <body>: a keyboard reader\n * pressing \"Next page\" on the second to last page would be sent back to the top of the document\n * by the next Tab. The focus goes to the page just reached instead, which is always rendered,\n * the current page being the one pill the responsive steps never hide.\n */\nasync function goFromControl(event: MouseEvent, n: number | undefined) {\n const control = event.currentTarget as HTMLElement\n const hadFocus = control === document.activeElement\n choose(event, n)\n if (!hadFocus) return\n await nextTick()\n if (isUnusable(control)) {\n navEl.value?.querySelector<HTMLElement>('[aria-current=\"page\"]')?.focus()\n }\n}\n\n/*\n * The previous and next controls, written once: the two differ only by their direction, and a\n * pair of template blocks drifting apart on the next change is exactly what this avoids. A\n * functional component because the two sit at either end of the row, with the pages in between,\n * where a `v-for` over two descriptors could not put them.\n */\nconst PageControl: FunctionalComponent<{ side: 'prev' | 'next' }> = ({ side }) => {\n const prev = side === 'prev'\n const label = prev ? resolvedPrevText.value : resolvedNextText.value\n const icon = prev ? props.prevIcon : props.nextIcon\n const target = prev ? prevTarget.value : nextTarget.value\n const disabled = prev ? prevDisabled.value : nextDisabled.value\n const common = {\n class: 'v-pagination-control',\n variant: props.itemVariant,\n disabled,\n href: hrefFor(target ?? currentPage.value),\n rel: props.href && !disabled ? side : undefined,\n onClick: (event: MouseEvent) => goFromControl(event, target),\n }\n const glyph = () => h(VIcon, { ...iconProps(icon), mirrored: true })\n if (props.controls === 'icon') return h(VIconButton, { ...common, label }, glyph)\n return h(\n VButton,\n { ...common, tone: 'neutral', 'aria-label': label },\n {\n ...(props.controls === 'both' ? { [prev ? 'start' : 'end']: glyph } : {}),\n default: () => h('span', { class: 'v-pagination-control-label' }, label),\n },\n )\n}\n\nconst navEl = ref<HTMLElement | null>(null)\n\n// @keyboard @a11y\n/**\n * The arrow keys, through the shared implementation in `utils/arrowNav`. The pills the\n * responsive rules have hidden are left out by the helper, which skips anything not displayed;\n * so they cannot be focused into.\n */\nfunction onKeydown(event: KeyboardEvent) {\n const nav = navEl.value\n if (!nav) return\n arrowNavigate(event, nav, () =>\n navigableItems(nav, '.v-pagination-page:not(:disabled, [aria-disabled=\"true\"])'),\n )\n}\n\n// The pills are rendered by a VButtonGroup inside the nav, so a template ref reaches none of\n// them. `focus` goes to the current page, which every responsive step keeps on screen.\ndefineExpose({\n // Two queries, not one selector list: a list matches in DOM order, which would hand the\n // focus to the previous control ahead of the current page. `a[href]` because an inert link\n // has lost its address, and with it the ability to take the focus.\n /** Moves the focus to the current page, or to the first control that can take it. */\n focus: (options?: FocusOptions) =>\n (\n navEl.value?.querySelector<HTMLElement>(\n '[aria-current=\"page\"]:is(button, a[href]):not(:disabled)',\n ) ?? navEl.value?.querySelector<HTMLElement>(':is(button, a[href]):not(:disabled)')\n )?.focus(options),\n /** The `<nav>` element, which is also where the consumer's attributes land. */\n el: navEl,\n})\n</script>\n\n<template>\n <nav\n ref=\"navEl\"\n class=\"v-pagination\"\n :aria-label=\"ariaLabel\"\n :data-align=\"align\"\n :data-item-variant=\"itemVariant\"\n :data-controls=\"controls || undefined\"\n :data-responsive=\"responsive ? '' : undefined\"\n @keydown=\"onKeydown\"\n >\n <!-- The row is a VButtonGroup either way: joined it merges the borders of its DIRECT\n button children, and detached it spaces them, which is all this prop asks of it.\n That is why there is no list markup wrapping the pills, and why the ellipsis is\n itself an inert button rather than a plain span: anything else between two pills\n would break the seam. -->\n <!--\n `|| undefined` keeps the group opinion-free when the row does not ask, `undefined` being\n what means \"no opinion\" there where `false` is an order. The size is always an opinion,\n the pagination having a default of its own.\n -->\n <VButtonGroup\n class=\"v-pagination-items\"\n :detached=\"detached\"\n :seamless=\"seamless\"\n :size=\"size\"\n :compact=\"compact || undefined\"\n :elevated=\"elevated || undefined\"\n >\n <PageControl v-if=\"controls\" side=\"prev\" />\n\n <template v-for=\"item in items\" :key=\"item.key\">\n <VButton\n v-if=\"item.kind === 'page'\"\n class=\"v-pagination-page\"\n :variant=\"item.page === currentPage ? selectedVariant : itemVariant\"\n :tone=\"item.page === currentPage ? tone : 'neutral'\"\n :disabled=\"disabled || isPageDisabled(item.page)\"\n :aria-label=\"pageLabelFor(item.page)\"\n :aria-current=\"item.page === currentPage ? 'page' : undefined\"\n :href=\"hrefFor(item.page)\"\n :data-edge=\"item.edge ? '' : undefined\"\n :data-distance=\"!item.edge && item.distance > 0 ? item.distance : undefined\"\n @click=\"choose($event, item.page)\"\n >\n {{ item.page }}\n </VButton>\n <!--\n The ellipsis is a disabled icon-only button rather than a span: being a button, it\n keeps the joined row's seam continuous and follows the size and density like\n everything else. Disabling it takes it out of the tab order, and it is hidden from\n screen readers, which have the page numbers themselves; which is also why it is a\n VButton and not a VIconButton, whose required label would name something nobody can\n reach.\n -->\n <VButton\n v-else\n class=\"v-pagination-ellipsis\"\n aria-hidden=\"true\"\n data-icon-only\n :variant=\"itemVariant\"\n tone=\"neutral\"\n disabled\n >\n <template #start>\n <VIcon :name=\"moreHorizIcon\" />\n </template>\n </VButton>\n </template>\n\n <PageControl v-if=\"controls\" side=\"next\" />\n </VButtonGroup>\n </nav>\n</template>\n\n<style>\n@layer vectis.components {\n .v-pagination {\n display: flex;\n }\n\n /*\n * A container of this kind computes its width WITHOUT looking at its content, so the row has\n * to be block-level: as an inline box it would measure zero and hide everything at once. It\n * therefore takes its parent's whole width, which is exactly what makes the hiding follow the\n * space the component was actually given, and what obliges a flex parent to grant it one.\n */\n .v-pagination[data-responsive] {\n container-type: inline-size;\n container-name: v-pagination;\n }\n\n .v-pagination[data-align='center'] {\n justify-content: center;\n }\n\n .v-pagination[data-align='end'] {\n justify-content: flex-end;\n }\n\n /*\n * Qualified by an attribute VButton always renders, which makes both of these beat that\n * button's own padding and its own transition whatever order the two sheets end up in; the\n * VIconButton idiom.\n */\n .v-pagination-page[data-size] {\n /* A one-digit pill is square, and widens by itself past that. The height variable\n is set by the shared size class on this very element, so this single rule covers\n all five sizes and their compact forms. */\n min-inline-size: var(--control-height);\n padding-inline: var(--vectis-space-2);\n\n /*\n * The highlight changes instantly, and this must not be \"restored for consistency\". VButton\n * fades `background-color` over `--vectis-duration-fast`, which is right for a button that\n * stays put and wrong here: truncation SHIFTS its window, so from one page to the next the\n * highlight keeps its slot but changes ELEMENT, and the outgoing and incoming pills are\n * both half-tinted for a few frames; read as a flicker, with nothing in the console.\n */\n transition: none;\n }\n\n /* The ellipsis is a disabled button only as a technical device; to the reader it is\n not a control that has been turned off, so it does not take the forbidden\n cursor. Qualified like the pills: VButton's disabled cursor weighs (0,2,0), and a bare\n class loses to it whatever the sheet order. */\n .v-pagination-ellipsis[data-size] {\n cursor: default;\n }\n\n /*\n * Each threshold is the width NEEDED to show that level, never what remains after hiding:\n * written the other way round the row overflows for the whole interval before the next step\n * takes effect. The most distant neighbours go first.\n */\n @container v-pagination (max-inline-size: 36rem) {\n .v-pagination[data-responsive] .v-pagination-page[data-distance='3'] {\n display: none;\n }\n }\n\n @container v-pagination (max-inline-size: 31rem) {\n .v-pagination[data-responsive] .v-pagination-page[data-distance='2'] {\n display: none;\n }\n\n /* It is never done when the control shows text alone: there would be nothing left to click. */\n .v-pagination[data-responsive][data-controls='both'] .v-pagination-control-label {\n display: none;\n }\n }\n\n @container v-pagination (max-inline-size: 25rem) {\n .v-pagination[data-responsive] .v-pagination-page[data-distance='1'] {\n display: none;\n }\n }\n\n /*\n * A disabled page greys its outline to a different token, hence a variable the second rule\n * changes. VToggle's `--toggle-frame` is the same device.\n */\n .v-pagination[data-item-variant='outline'] {\n --pagination-frame: var(--vectis-color-border-strong);\n }\n\n .v-pagination[data-item-variant='outline']\n > *\n > .v-pagination-page:is(:disabled, [aria-disabled='true']) {\n --pagination-frame: var(--vectis-color-border);\n }\n\n .v-pagination[data-item-variant='outline']\n > *\n > .v-pagination-page[aria-current='page']:is([data-variant='soft'], [data-variant='ghost']) {\n border-color: var(--pagination-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-pagination-page.v-pagination-page.v-pagination-page.v-pagination-page[aria-current='page']:not(\n :disabled,\n [aria-disabled='true']\n ) {\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":"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"}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
export type PopoverPlacement = 'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'left' | 'left-start' | 'left-end' | 'right' | 'right-start' | 'right-end';
|
|
2
2
|
/**
|
|
3
|
-
* What the trigger has to carry for the browser to open the panel and for assistive
|
|
4
|
-
*
|
|
5
|
-
* no role and no `aria-haspopup` are imposed, since only the consumer knows what kind
|
|
6
|
-
* of panel it is opening.
|
|
3
|
+
* What the trigger has to carry for the browser to open the panel and for assistive technology
|
|
4
|
+
* to know what it controls.
|
|
7
5
|
*/
|
|
8
6
|
export type PopoverTriggerProps = {
|
|
9
7
|
popovertarget: string;
|
|
@@ -24,11 +22,7 @@ interface PopoverProps {
|
|
|
24
22
|
* opposite side by itself when there is not enough room.
|
|
25
23
|
*/
|
|
26
24
|
placement?: PopoverPlacement;
|
|
27
|
-
/**
|
|
28
|
-
* How the panel closes. `auto` lets the browser dismiss it on a click outside or on
|
|
29
|
-
* Escape, and stack it with other panels; `manual` leaves everything to the
|
|
30
|
-
* consumer, which is what a panel with its own focus and dismissal rules needs.
|
|
31
|
-
*/
|
|
25
|
+
/** How the panel closes. */
|
|
32
26
|
mode?: PopoverMode;
|
|
33
27
|
/**
|
|
34
28
|
* The name of an anchor the consumer has set on its own control, written as a CSS
|
|
@@ -43,11 +37,7 @@ interface PopoverProps {
|
|
|
43
37
|
* asks for, as VDatePicker does.
|
|
44
38
|
*/
|
|
45
39
|
bare?: boolean;
|
|
46
|
-
/**
|
|
47
|
-
* Stops the panel being narrower than whatever it is anchored to. It is a FLOOR, so a
|
|
48
|
-
* panel with a width of its own still grows past it rather than being clamped to the
|
|
49
|
-
* trigger, which is what a list of long labels under a short field wants.
|
|
50
|
-
*/
|
|
40
|
+
/** Stops the panel being narrower than whatever it is anchored to. */
|
|
51
41
|
matchTrigger?: boolean;
|
|
52
42
|
}
|
|
53
43
|
type __VLS_Props = PopoverProps;
|
|
@@ -64,10 +54,9 @@ type __VLS_Slots = {
|
|
|
64
54
|
};
|
|
65
55
|
type __VLS_ModelProps = {
|
|
66
56
|
/**
|
|
67
|
-
* Whether the panel is showing.
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* instead, which is what VTooltip and the pickers do.
|
|
57
|
+
* Whether the panel is showing. Setting it opens and closes the panel; a consumer needing the
|
|
58
|
+
* change to be synchronous uses the exposed `show`/ `close` instead, which VTooltip and the
|
|
59
|
+
* pickers do.
|
|
71
60
|
*/
|
|
72
61
|
'open'?: boolean;
|
|
73
62
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VPopover.js","names":[],"sources":["../../../src/components/VPopover/VPopover.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @core\n/**\n * A panel that floats above the page, next to whatever opened it. It is built on the\n * browser's own popover support and positions itself entirely in CSS, so there is no\n * positioning library involved and no JavaScript measuring anything.\n *\n * What it carries is deliberately only the PLUMBING every panel of the design system\n * needs: the popover element and how it opens, the bridge between the browser's own\n * open state and `v-model:open`, the anchoring and the placement, and the surface a\n * panel is drawn on — its background, border and shadow, which `bare` strips away.\n *\n * What it carries NOT is just as deliberate: no ARIA role, no keyboard, no focus\n * management and no rule about when to close. Those are exactly what makes a menu\n * different from a listbox or a tooltip, and they stay with the components building\n * on it — VTooltip, VCombobox, VDateInput, VTimeInput. Turning this into one panel\n * with a `role` prop would put all four sets of behaviour back into a single file.\n *\n * There are two ways of anchoring it, and they exclude one another:\n *\n * - the `#trigger` slot, where the component wraps the trigger and names that\n * wrapper as the anchor. The name is shared by every instance and confined to each\n * wrapper's own subtree, which is what makes a panel resolve ITS trigger rather\n * than the last one named on the page — a real risk, since an open panel moves to\n * the top layer and counts as coming after the whole document.\n * - the `anchor` prop, for a consumer that already owns its root and its control —\n * VCombobox and the pickers do. They name the anchor themselves and pass the name\n * in, and the wrapper then disappears from the layout entirely.\n *\n * The only JavaScript is the bridge between `v-model:open` and the browser's own\n * imperative popover methods.\n */\nimport { computed, ref, useId } from 'vue'\n\nimport { usePopover, usePopoverModel } from '../../composables/usePopover'\n\nexport type PopoverPlacement =\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/**\n * What the trigger has to carry for the browser to open the panel and for assistive\n * technology to know what it controls. It is the disclosure pattern and nothing more:\n * no role and no `aria-haspopup` are imposed, since only the consumer knows what kind\n * of panel it is opening.\n */\nexport type PopoverTriggerProps = {\n popovertarget: string\n 'aria-expanded': boolean\n 'aria-controls': string\n}\n\n/** Whether the browser dismisses the panel on its own. */\nexport type PopoverMode = 'auto' | 'manual'\n\ninterface PopoverProps {\n /**\n * The id of the panel, which the trigger points at. One is generated when none is\n * given, so this is only needed to tie the panel to something outside the\n * component.\n */\n id?: string\n /**\n * Where the panel is placed relative to its trigger. The browser flips it to the\n * opposite side by itself when there is not enough room.\n */\n placement?: PopoverPlacement\n /**\n * How the panel closes. `auto` lets the browser dismiss it on a click outside or on\n * Escape, and stack it with other panels; `manual` leaves everything to the\n * consumer, which is what a panel with its own focus and dismissal rules needs.\n */\n mode?: PopoverMode\n /**\n * The name of an anchor the consumer has set on its own control, written as a CSS\n * dashed identifier such as `--tooltip-anchor`. Supplying it replaces the internal\n * wrapper, which is the required route as soon as the trigger is a text input,\n * where the browser's own `popovertarget` attribute is not allowed.\n */\n anchor?: string\n /**\n * Strips the panel of the design system's surface: no background, no border, no\n * shadow and no rounded corners. It is what a panel whose content brings its own\n * asks for, as VDatePicker does.\n */\n bare?: boolean\n /**\n * Stops the panel being narrower than whatever it is anchored to. It is a FLOOR, so a\n * panel with a width of its own still grows past it rather than being clamped to the\n * trigger, which is what a list of long labels under a short field wants.\n */\n matchTrigger?: boolean\n}\n\nconst props = withDefaults(defineProps<PopoverProps>(), {\n id: undefined,\n placement: 'bottom-start',\n mode: 'auto',\n anchor: undefined,\n bare: false,\n matchTrigger: false,\n})\n\n/**\n * Whether the panel is showing. It starts closed and is BIDIRECTIONAL, fed from the DOM: in\n * `auto` mode the browser's own light dismiss writes back to it. Setting it opens and closes\n * the panel; a consumer needing the change to be synchronous uses the exposed `show`/`close`\n * instead, which is what VTooltip and the pickers do.\n */\nconst open = defineModel<boolean>('open', { default: false })\n\nconst slots = defineSlots<{\n /**\n * The element that opens the panel. Bind the `triggerProps` it receives onto a\n * button of your own: that is what wires the two together.\n */\n trigger?(props: { triggerProps: PopoverTriggerProps }): unknown\n /** What the panel contains. */\n default(): unknown\n}>()\n\n/*\n * Everything the consumer passes — the ARIA role, the aria-*, the data-*, class,\n * style and the listeners — goes on the PANEL rather than on the anchoring wrapper,\n * because the panel is what the consumer is really describing and styling.\n *\n * This is a deliberate departure from the design system's usual wrapper pattern,\n * which keeps class and style on the root; `useRootAttrs` therefore does not apply\n * here.\n */\ndefineOptions({ inheritAttrs: false })\n\nconst panelEl = ref<HTMLElement | null>(null)\nconst generatedId = useId()\nconst panelId = computed(() => props.id ?? generatedId)\n\n// The open state and the guards that keep the calls idempotent live in usePopover.\n// The invariant to respect here: `shown` is fed by the panel's own events and never\n// assigned by hand, so the browser stays the source of truth — it can close the panel\n// without asking us.\nconst { shown, syncShown, show, hide } = usePopover(panelEl)\n\n// TRAP — a function read by the template, never a `computed`: `slots` is not reactive, so a\n// computed would keep its first answer while a slot behind a `v-if` comes and goes.\nfunction hasTrigger() {\n return slots.trigger !== undefined\n}\n\nconst triggerProps = computed<PopoverTriggerProps>(() => ({\n popovertarget: panelId.value,\n 'aria-expanded': open.value,\n 'aria-controls': panelId.value,\n}))\n\n// Both anchoring modes are served by one CSS declaration: the panel reads this variable,\n// which names either the consumer's anchor or the wrapper's own.\n//\n// TRAP — it is set inline in BOTH modes, never left to a fallback. A custom property\n// INHERITS: left unset in the `#trigger` mode, the panel read whatever an ancestor had\n// set — a VPopover placed in VDateInput's `#footer` inherited the date field's anchor and\n// opened under the field instead of under its own trigger, with no error anywhere.\nconst panelStyle = computed(() => ({\n '--popover-anchor-name': props.anchor ?? '--popover-anchor',\n}))\n\nfunction onToggle(event: Event) {\n syncShown(event)\n open.value = shown.value\n}\n\nusePopoverModel(open, () => shown.value, show, hide)\n\n// The imperative counterpart of `v-model:open`, for a consumer whose opening must be\n// SYNCHRONOUS: the model would insert a tick, which is exactly what VTooltip's delay and\n// the pickers' focus frame cannot afford.\ndefineExpose({\n /** Opens the panel at once, without waiting for the model to come round. */\n show,\n /** Closes it at once. Safe to call on a panel that is already closed. */\n close: hide,\n /** The panel element itself, which is the popover. */\n el: panelEl,\n})\n</script>\n\n<template>\n <span class=\"v-popover\" :data-trigger=\"hasTrigger() ? '' : undefined\">\n <slot name=\"trigger\" :trigger-props=\"triggerProps\" />\n <div\n :id=\"panelId\"\n ref=\"panelEl\"\n :popover=\"mode\"\n class=\"v-overlay v-popover-panel v-floating\"\n :class=\"{ 'v-panel': !bare }\"\n :data-placement=\"placement\"\n :data-match-trigger=\"matchTrigger ? '' : undefined\"\n :style=\"panelStyle\"\n v-bind=\"$attrs\"\n @beforetoggle=\"syncShown\"\n @toggle=\"onToggle\"\n >\n <slot />\n </div>\n </span>\n</template>\n\n<style>\n@layer vectis.components {\n /*\n * Without a trigger the wrapper has nothing to wrap, so `display: contents` takes\n * it out of the layout entirely — the panel is positioned against the viewport and\n * does not depend on it. Left as an empty inline-block it would still create a line\n * box, and therefore add height, inside every component using this route.\n */\n .v-popover {\n display: contents;\n }\n\n /*\n * With a trigger the wrapper must generate a box, since that box is the ANCHOR —\n * which is why `display: contents` stops at the rule above. It is FLEX rather than\n * inline-block, the `.v-badge-host` and `.v-tooltip` form: both are inline-level\n * atomic boxes, so text flows around them identically, but an inline-block opens an\n * inline formatting context for the trigger, which then lands on a line box and adds\n * the strut's descender to the wrapper. The wrapper being the anchor, that offsets\n * the panel as well as the row.\n *\n * `align-items: center` covers the rest: 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 */\n .v-popover[data-trigger] {\n display: inline-flex;\n align-items: center;\n anchor-name: --popover-anchor;\n /* Confining the name to this subtree is indispensable, not tidy: an open panel\n moves to the top layer, where anchor resolution treats it as coming after the\n whole document. Without the confinement every panel on the page would attach\n itself to the LAST wrapper carrying this name. */\n anchor-scope: --popover-anchor;\n }\n\n /*\n * The panel gets this one declaration and no dimension whatsoever. Widths, heights\n * and overflow differ from one consumer to the next, and since each component ships\n * its own stylesheet, a declaration here would sit at equal specificity with theirs\n * — leaving the winner to whichever order the consumer's bundler happens to\n * produce.\n */\n .v-popover-panel {\n position-anchor: var(--popover-anchor-name);\n }\n\n /*\n * A `bare` panel is bare of the BROWSER's decoration too. The UA sheet gives every\n * `[popover]` a border, a padding, a Canvas background and `overflow: auto`, and `.v-panel`\n * is what normally overrides them: without it they came back, a solid line and a white box\n * around content promised \"no background, no border\".\n *\n * TRAP — `:where()` keeps this at (0,1,0). A bare consumer restyles its panel through the\n * `.v-popover-panel.v-x-panel` compound (VTooltip), at (0,2,0), which must win; a (0,2,0)\n * rule here would tie with it across two sheets.\n */\n .v-popover-panel:where(:not(.v-panel)) {\n border: none;\n padding: 0;\n background: none;\n color: inherit;\n overflow: visible;\n }\n\n /*\n * The one dimension this sheet does carry, and it is a FLOOR rather than a size: a panel\n * may not come out narrower than what it is anchored to. It is written here rather than in\n * each consumer because `anchor-size()` resolves against `position-anchor`, which is the\n * declaration above — the same reason the anchoring itself lives here.\n *\n * A minimum beats a maximum, so a consumer's own `max-inline-size` does not clamp it: a\n * trigger wider than the ceiling widens the panel past it, which is intended. VMenu carries\n * the twin of this rule, being deliberately outside VPopover, and answers to the same word.\n */\n .v-popover-panel[data-match-trigger] {\n min-inline-size: anchor-size(width);\n }\n}\n</style>\n"],"mappings":""}
|
|
1
|
+
{"version":3,"file":"VPopover.js","names":[],"sources":["../../../src/components/VPopover/VPopover.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @core\n/**\n * Bridge native popover state to v-model while CSS anchors the panel. Roles, keyboard handling\n * and dismissal policy belong to the consuming widget.\n */\nimport { computed, ref, useId } from 'vue'\n\nimport { usePopover, usePopoverModel } from '../../composables/usePopover'\n\nexport type PopoverPlacement =\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/**\n * What the trigger has to carry for the browser to open the panel and for assistive technology\n * to know what it controls.\n */\nexport type PopoverTriggerProps = {\n popovertarget: string\n 'aria-expanded': boolean\n 'aria-controls': string\n}\n\n/** Whether the browser dismisses the panel on its own. */\nexport type PopoverMode = 'auto' | 'manual'\n\ninterface PopoverProps {\n /**\n * The id of the panel, which the trigger points at. One is generated when none is\n * given, so this is only needed to tie the panel to something outside the\n * component.\n */\n id?: string\n /**\n * Where the panel is placed relative to its trigger. The browser flips it to the\n * opposite side by itself when there is not enough room.\n */\n placement?: PopoverPlacement\n /** How the panel closes. */\n mode?: PopoverMode\n /**\n * The name of an anchor the consumer has set on its own control, written as a CSS\n * dashed identifier such as `--tooltip-anchor`. Supplying it replaces the internal\n * wrapper, which is the required route as soon as the trigger is a text input,\n * where the browser's own `popovertarget` attribute is not allowed.\n */\n anchor?: string\n /**\n * Strips the panel of the design system's surface: no background, no border, no\n * shadow and no rounded corners. It is what a panel whose content brings its own\n * asks for, as VDatePicker does.\n */\n bare?: boolean\n /** Stops the panel being narrower than whatever it is anchored to. */\n matchTrigger?: boolean\n}\n\nconst props = withDefaults(defineProps<PopoverProps>(), {\n id: undefined,\n placement: 'bottom-start',\n mode: 'auto',\n anchor: undefined,\n bare: false,\n matchTrigger: false,\n})\n\n/**\n * Whether the panel is showing. Setting it opens and closes the panel; a consumer needing the\n * change to be synchronous uses the exposed `show`/ `close` instead, which VTooltip and the\n * pickers do.\n */\nconst open = defineModel<boolean>('open', { default: false })\n\ndefineSlots<{\n /**\n * The element that opens the panel. Bind the `triggerProps` it receives onto a\n * button of your own: that is what wires the two together.\n */\n trigger?(props: { triggerProps: PopoverTriggerProps }): unknown\n /** What the panel contains. */\n default(): unknown\n}>()\n\n/*\n * Everything the consumer passes; the ARIA role, the aria-*, the data-*, class, style and the\n * listeners; goes on the PANEL rather than on the anchoring wrapper, because the panel is what\n * the consumer is really describing and styling. This is a deliberate departure from the design\n * system's usual wrapper pattern, which keeps class and style on the root; `useRootAttrs`\n * therefore does not apply here.\n */\ndefineOptions({ inheritAttrs: false })\n\nconst panelEl = ref<HTMLElement | null>(null)\nconst generatedId = useId()\nconst panelId = computed(() => props.id ?? generatedId)\n\n// The invariant to respect here: `shown` is fed by the panel's own events and never assigned by\n// hand, so the browser stays the source of truth; it can close the panel without asking us.\nconst { shown, syncShown, show, hide } = usePopover(panelEl)\n\nconst triggerProps = computed<PopoverTriggerProps>(() => ({\n popovertarget: panelId.value,\n 'aria-expanded': open.value,\n 'aria-controls': panelId.value,\n}))\n\n// Set the anchor explicitly in both trigger modes; an unset custom property would inherit an\n// ancestor popover's anchor.\nconst panelStyle = computed(() => ({\n '--popover-anchor-name': props.anchor ?? '--popover-anchor',\n}))\n\nfunction onToggle(event: Event) {\n syncShown(event)\n open.value = shown.value\n}\n\nusePopoverModel(open, () => shown.value, show, hide)\n\n// The imperative counterpart of `v-model:open`, for a consumer whose opening must be\n// SYNCHRONOUS: the model would insert a tick, which is exactly what VTooltip's delay and\n// the pickers' focus frame cannot afford.\ndefineExpose({\n /** Opens the panel at once, without waiting for the model to come round. */\n show,\n /** Closes it at once. Safe to call on a panel that is already closed. */\n close: hide,\n /** The panel element itself, which is the popover. */\n el: panelEl,\n})\n</script>\n\n<template>\n <span class=\"v-popover\" :data-trigger=\"$slots.trigger ? '' : undefined\">\n <slot name=\"trigger\" :trigger-props=\"triggerProps\" />\n <div\n :id=\"panelId\"\n ref=\"panelEl\"\n :popover=\"mode\"\n class=\"v-overlay v-popover-panel v-floating\"\n :class=\"{ 'v-panel': !bare }\"\n :data-placement=\"placement\"\n :data-match-trigger=\"matchTrigger ? '' : undefined\"\n :style=\"panelStyle\"\n v-bind=\"$attrs\"\n @beforetoggle=\"syncShown\"\n @toggle=\"onToggle\"\n >\n <slot />\n </div>\n </span>\n</template>\n\n<style>\n@layer vectis.components {\n /*\n * Without a trigger the wrapper has nothing to wrap, so `display: contents` takes it out of\n * the layout entirely; the panel is positioned against the viewport and does not depend on\n * it. Left as an empty inline-block it would still create a line box, and therefore add\n * height, inside every component using this route.\n */\n .v-popover {\n display: contents;\n }\n\n /*\n * With a trigger the wrapper must generate a box, since that box is the ANCHOR; which is why\n * `display: contents` stops at the rule above. It is FLEX rather than inline-block, the\n * `.v-badge-host` and `.v-tooltip` form: both are inline-level atomic boxes, so text flows\n * around them identically, but an inline-block opens an inline formatting context for the\n * trigger, which then lands on a line box and adds the strut's descender to the wrapper.\n */\n .v-popover[data-trigger] {\n display: inline-flex;\n align-items: center;\n anchor-name: --popover-anchor;\n /*\n * Without the confinement every panel on the page would attach itself to the last wrapper\n * carrying this name.\n */\n anchor-scope: --popover-anchor;\n }\n\n /*\n * Widths, heights and overflow differ from one consumer to the next, and since each component\n * ships its own stylesheet, a declaration here would sit at equal specificity with theirs;\n * leaving the winner to whichever order the consumer's bundler happens to produce.\n */\n .v-popover-panel {\n position-anchor: var(--popover-anchor-name);\n }\n\n /*\n * `:where()` keeps this at (0,1,0). A bare consumer restyles its panel through the\n * `.v-popover-panel.v-x-panel` compound (VTooltip), at (0,2,0), which must win; a (0,2,0)\n * rule here would tie with it across two sheets.\n */\n .v-popover-panel:where(:not(.v-panel)) {\n border: none;\n padding: 0;\n background: none;\n color: inherit;\n overflow: visible;\n }\n\n /*\n * It is written here rather than in each consumer because `anchor-size()` resolves against\n * `position-anchor`, which is the declaration above; the same reason the anchoring itself\n * lives here. A minimum beats a maximum, so a consumer's own `max-inline-size` does not clamp\n * it: a trigger wider than the ceiling widens the panel past it, which is intended.\n */\n .v-popover-panel[data-match-trigger] {\n min-inline-size: anchor-size(width);\n }\n}\n</style>\n"],"mappings":""}
|