@adyen/bento-mcp 0.1.2 → 0.1.3
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/CHANGELOG.md +5 -0
- package/dist/assets/components/anchor-scroller/anchor-scroller.vue +1 -1
- package/dist/assets/components/checkbox/checkbox.vue +1 -1
- package/dist/assets/components/checkbox/components/checkbox-group/checkbox-group.vue +1 -1
- package/dist/assets/components/data-grid/components/data-grid-row/data-grid-row.vue +1 -1
- package/dist/assets/components/data-grid/components/data-grid-tag-with-text-cell/data-grid-tag-with-text-cell.vue +1 -1
- package/dist/assets/components/data-grid/data-grid.vue +1 -1
- package/dist/assets/components/date-picker/date-picker.vue +1 -1
- package/dist/assets/components/date-range-picker/components/date-range-picker-calendar/date-range-picker-calendar.vue +1 -1
- package/dist/assets/components/date-range-picker/date-range-picker.vue +1 -1
- package/dist/assets/components/dropdown/components/dropdown-base-textbox/dropdown-base-textbox.vue +1 -1
- package/dist/assets/components/dropdown/dropdown.vue +1 -1
- package/dist/assets/components/file-uploader/components/file-uploader-restrictions/file-uploader-restrictions.vue +1 -1
- package/dist/assets/components/file-uploader/file-uploader.docs.mdx +9 -0
- package/dist/assets/components/file-uploader/file-uploader.stories.ts +1 -1
- package/dist/assets/components/file-uploader/file-uploader.vue +1 -1
- package/dist/assets/components/filter-bar/components/all-filters-modal/all-filters-modal.vue +1 -1
- package/dist/assets/components/filter-bar/components/date-range-filter/date-range-filter.vue +1 -1
- package/dist/assets/components/filter-bar/filter-bar.vue +1 -1
- package/dist/assets/components/form-layout/components/form-layout-title/form-layout-title.vue +1 -1
- package/dist/assets/components/form-layout/form-layout.docs.mdx +4 -0
- package/dist/assets/components/input-field/input-field.vue +1 -1
- package/dist/assets/components/input-field-phone-number/input-field-phone-number.vue +1 -1
- package/dist/assets/components/internal/controls-group/controls-group.vue +1 -1
- package/dist/assets/components/internal/field-label/field-label.vue +1 -1
- package/dist/assets/components/modal-fullscreen/components/modal-fullscreen-page.vue +1 -1
- package/dist/assets/components/modal-fullscreen/modal-fullscreen.vue +1 -1
- package/dist/assets/components/pagination/components/pagination-context/pagination-context.vue +1 -1
- package/dist/assets/components/pagination/components/pagination-results-per-page/pagination-results-per-page.vue +1 -1
- package/dist/assets/components/pagination/pagination.vue +1 -1
- package/dist/assets/components/radio-group/radio-group.vue +1 -1
- package/dist/assets/components/rich-text-editor/rich-text-editor.vue +1 -1
- package/dist/assets/components/selection-card/components/selection-card-group/selection-card-group.vue +1 -1
- package/dist/assets/components/textarea/textarea.vue +1 -1
- package/dist/main.js +145 -136
- package/package.json +1 -1
package/dist/assets/components/filter-bar/components/all-filters-modal/all-filters-modal.vue
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div class="b-all-filters-modal"> <filter-bar-button :is-filter-open="isModalOpen" :applied-filters-count="numberOfActiveFilters" no-applied-state-styles @click="onClickAllFiltersModal" > {{ t('allFilters') }} <template #iconLeft> <sliders-icon aria-hidden="true" /> </template> </filter-bar-button> <bento-modal class="'b-all-filters-modal__modal'" :is-open="isModalOpen" :with-column-layout="true" size="large" :actions="modalActions" :is-dismissible="false" hide-close-button @close-modal="cancel" > <template #default> {{ t('allFilters') }} </template> <template #content-col-1> <div ref="allFiltersModalFiltersSection" class="b-all-filters-modal__filters" @keydown.right="onKeydownRight" > <bento-search-bar class="b-all-filters-modal__search-bar" :value="searchTerm" :input-field-aria-label="t('searchBar').toString()" @input="searchTerm = $event" @clear="onSearchbarClear" /> <div v-if="isEmptyResultForSearchFilter" class="b-all-filters-modal__empty-search-message"> <bento-typography el="span"> {{ t('noFiltersMatchThisSearch') }}</bento-typography> </div> <div v-if="activeFilters.length" class="b-all-filters-modal__filters-section"> <bento-typography :id="`active-filters-${allFiltersModalUniqueId}`" class="b-all-filters-modal__filters-section-title" el="span" variant="caption" > {{ t('activeFilters') }} </bento-typography> <ul class="b-all-filters-modal__filters-list" :aria-labelledby="`active-filters-${allFiltersModalUniqueId}`" > <li v-for="item in activeFilters" :key="`${item.label}-${item.field}`"> <all-filters-modal-button :ref="el => (allFiltersModalFilterButtons[item.label] = el)" :label="item.label" :applied-filters-count="getFilterCount(item)" :is-filter-open="selectedFilter.label === item.label" :disabled="item.disabled" @click="selectFilter(item.label, item.field)" /> </li> </ul> </div> <div v-if="otherFilters.length" class="b-all-filters-modal__filters-section"> <bento-typography v-if="activeFilters.length" :id="`other-filters-${allFiltersModalUniqueId}`" class="b-all-filters-modal__filters-section-title" el="span" variant="caption" > {{ t('otherFilters') }} </bento-typography> <ul class="b-all-filters-modal__filters-list" :aria-labelledby="`other-filters-${allFiltersModalUniqueId}`" > <li v-for="item in otherFilters" :key="`${item.label}-${item.field}`"> <all-filters-modal-button :ref="el => (allFiltersModalFilterButtons[item.label] = el)" :label="item.label" :disabled="item.disabled" :applied-filters-count="getFilterCount(item)" :is-filter-open="selectedFilter.label === item.label" @click="selectFilter(item.label, item.field)" /> </li> </ul> </div> </div> </template> <template #content-col-2> <div class="b-all-filters-modal__selected-filter-content" @keydown.left="onKeydownLeft"> <div v-if="shouldShowFilterTitle" class="b-all-filters-modal__selected-filter-text"> <bento-typography el="div" variant="title" medium> {{ selectedFilter.label }} </bento-typography> <bento-typography v-if="selectedFilter.description" el="div" variant="body" wide> {{ selectedFilter.description }} </bento-typography> </div> <div class="b-all-filters-modal__selected-filter-component"> <component :is="selectedFilter.type" ref="selectedFilterRef" :key="selectedFilter.field" :description="selectedFilter.description" :field="selectedFilter.field" :label="selectedFilter.label" :options="selectedFilter.options" :value="selectedFilter.value" :default-value="selectedFilter.defaultValue" :only-slot="true" :disabled="selectedFilter.disabled" @input="updateFilter" /> </div> <div class="b-all-filters-modal__clear-button"> <bento-button variant="secondary" :disabled="isClearButtonDisabled" @click="clearFilter">{{ t('clear') }}</bento-button> </div> </div> </template> </bento-modal> </div> </template> <script lang="ts"> import { computed, defineComponent, onBeforeUpdate, onMounted, type PropType, ref, toRef, watch } from 'vue'; import BentoModal from '@/components/modal/modal.vue'; import { BentoButton } from '@/components/button'; import { type BentoFilterBarModel, BentoFilterItemType, type BentoFilterModel, type BentoSelectFilterOptions, } from '@/components/filter-bar/filter-bar.types'; import { BentoBooleanFilter, BentoCheckboxGroupFilter, BentoDateFilter, BentoDateRangeFilter, BentoInputFilter, BentoInputWithDropdownFilter, BentoRadioGroupFilter, BentoRangeFilter, BentoSelectAvatarFilter, BentoSelectCountryFilter, BentoSelectCurrencyFilter, BentoSelectFilter, BentoSelectPaymentMethodFilter, BentoSelectTagFilter, BentoToggleFilter, } from '../index'; import { type BentoDateRangePickerValue } from '@/components/date-range-picker'; import BentoSearchBar from '@/components/search-bar/search-bar.vue'; import { BentoTypography } from '@/components/typography'; import { useSearchBarFilter } from '@/components/search-bar'; import { generateUid } from '@/core/utils/ts'; import { getNextFocusableElement } from '@/utils/ts/focus-trap.utils'; import { FilterBarButton } from '../filter-bar-button'; import { AllFiltersModalEvent } from './all-filters-modal.types'; import { useI18n } from '@/utils/ts/i18n'; import AllFiltersModalButton from './components/all-filters-modal-button/all-filters-modal-button.vue'; import SlidersIcon from '@adyen/ui-assets-icons-16/vue/sliders-1'; import messages from './messages.json'; import { type BentoCheckboxGroupFilterOptions } from '../checkbox-group-filter'; type MessageSchema = (typeof messages)['en-US']; /** * A modal that acts as sort of filter manager for the BentoFilterBar. * * @example * import { BentoAllFiltersModal } from '@adyen/bento-vue2'; * * export default { * components: { BentoAllFiltersModal }, * template: ` * <bento-all-filters-modal> * ...Example code of the component * </bento-all-filters-modal> * ` * } */ export default defineComponent({ i18n: { messages }, name: 'all-filters-modal', components: { AllFiltersModalButton, BentoBooleanFilter, BentoButton, BentoCheckboxGroupFilter, BentoDateFilter, BentoDateRangeFilter, BentoInputFilter, BentoInputWithDropdownFilter, BentoModal, BentoRadioGroupFilter, BentoRangeFilter, BentoSearchBar, BentoSelectAvatarFilter, BentoSelectCurrencyFilter, BentoSelectCountryFilter, BentoSelectFilter, BentoSelectPaymentMethodFilter, BentoSelectTagFilter, BentoToggleFilter, BentoTypography, FilterBarButton, SlidersIcon, }, props: { /** * Array of filters based on {@see FilterBarModel}. * Each one of the elements in the array correspond to a filter that will * be created and rendered in the AllFiltersModal. * * Can be used with v-model or independently with the @input event. * * @example * [{ * field: 'company', * label: 'Company', * value: '200', * type: FilterType.TEXT, * }, { * field: 'name', * label: 'Name', * value: null, * type: FilterType.TEXT, * }] */ value: { type: Array as PropType<BentoFilterBarModel>, default: () => [], }, }, emits: [AllFiltersModalEvent.INPUT], setup(props, { emit }) { const filtersInModal = ref(props.value); const allFiltersModalUniqueId = generateUid('all-filters-modal'); const allFiltersModalFiltersSection = ref([]); const isModalOpen = ref(false); const selectedFilterIndex = ref(0); const selectedFilterRef = ref(null); const searchTerm = ref<string>(''); const { t, n } = useI18n<{ message: MessageSchema }>({ messages }); const filteredItems = useSearchBarFilter<BentoFilterModel>(searchTerm, toRef(filtersInModal, 'value')); const allFiltersModalFilterButtons = ref({}); onBeforeUpdate(() => { allFiltersModalFilterButtons.value = []; }); watch( () => props.value, () => { /** * Values need to be preserve when the modal is opened given that * when the "select" filter is enabled with external filtering inside the modal * the "searchTerm" gets reset and doing so resets all the unsaved values of the array */ if (isModalOpen.value) { filtersInModal.value = props.value.map((filter, index) => { return { ...filter, value: filtersInModal.value[index].value }; }); } else { filtersInModal.value = props.value; } } ); const sortAlphabetically = (array: BentoFilterBarModel) => array.sort((a, b) => (a.label > b.label ? 1 : b.label > a.label ? -1 : 0)); const activeFilters = computed(() => sortAlphabetically( filteredItems.value.filter(el => Array.isArray(el.value) ? el.value?.length && el.value?.find(item => item !== '') : el.value ) ) ); const otherFilters = computed(() => sortAlphabetically( filteredItems.value.filter(el => Array.isArray(el.value) ? el.value?.length === 0 || el.value?.every(item => item === '') : !el.value ) ) ); const isEmptyResultForSearchFilter = computed(() => searchTerm.value && filteredItems.value?.length === 0); const selectedFilter = computed(() => filtersInModal.value[selectedFilterIndex.value]); const selectOpenedFilter = () => { const openedFilter = activeFilters.value.length ? activeFilters.value[0] : otherFilters.value[0]; const openedFilterIndex = filtersInModal.value.findIndex( el => el.label === openedFilter.label && el.field === openedFilter.field ); selectedFilterIndex.value = openedFilterIndex; }; onMounted(() => { selectOpenedFilter(); }); const computeCounts = (filterValue: BentoFilterBarModel) => filterValue.map(filter => { switch (filter.type) { case BentoFilterItemType.SELECT: case BentoFilterItemType.SELECT_AVATAR: case BentoFilterItemType.SELECT_COUNTRY: case BentoFilterItemType.SELECT_CURRENCY: case BentoFilterItemType.SELECT_PAYMENT_METHOD: case BentoFilterItemType.SELECT_TAG: switch ((filter.options as BentoSelectFilterOptions)?.multiple) { case true: return { label: filter.label, field: filter.field, count: (filter.value as Array<string | number>)?.length ? (filter.value as Array<string | number>)?.length : null, }; default: return { label: filter.label, field: filter.field, count: filter.value == null ? null : 1, }; } case BentoFilterItemType.DATE_RANGE: return { label: filter.label, field: filter.field, count: (filter.value as BentoDateRangePickerValue)?.startDate != null && (filter.value as BentoDateRangePickerValue)?.endDate != null ? 1 : null, }; case BentoFilterItemType.RANGE: return { label: filter.label, field: filter.field, count: (filter.value as Array<string>)?.find(el => el !== '') ? 1 : null, }; case BentoFilterItemType.CHECKBOX_GROUP: return { label: filter.label, field: filter.field, count: (filter.value as Array<string | number>)?.length ? (filter.value as Array<string | number>)?.length : null, }; case BentoFilterItemType.TOGGLE: return { label: filter.label, field: filter.field, count: filter.value ? 1 : null, }; case BentoFilterItemType.INPUT: case BentoFilterItemType.BOOLEAN: case BentoFilterItemType.DATE: case BentoFilterItemType.RADIO_GROUP: default: return { label: filter.label, field: filter.field, count: filter.value != null ? 1 : null, }; } }); const hasValueSelected = computed(() => Array.isArray(selectedFilter.value.value) ? selectedFilter.value?.value.length && selectedFilter.value.value.find(item => item !== '') : !!filtersInModal.value[selectedFilterIndex.value].value ); /** * Hides filter title if the selected filter is Toggle. * The Toggle filter shows a checkbox next to the "title". */ const shouldShowFilterTitle = computed(() => { return selectedFilter.value.type !== BentoFilterItemType.TOGGLE; }); const isClearButtonDisabled = computed(() => !hasValueSelected.value || selectedFilter.value.disabled); const numberOfActiveFilters = computed( () => computeCounts(props.value)?.filter(({ count }) => !!count).length ); const onClickAllFiltersModal = () => { if (!isModalOpen.value) { isModalOpen.value = true; } else { isModalOpen.value = false; } }; const getFilterCount = (filter: BentoFilterModel) => { const filterCount = computeCounts(filtersInModal.value).find(el => el.field === filter.field).count; const isBentoSelectFilter = typeof filter.type === 'string' && // Type guard to avoid casting filter.type.includes('BentoSelect') && (filter.options as BentoSelectFilterOptions)?.multiple && filterCount === (filter.options as BentoSelectFilterOptions)?.listboxItems?.length; const isBentoCheckboxGroupFilter = typeof filter.type === 'string' && filter.type === BentoFilterItemType.CHECKBOX_GROUP && filterCount === (filter.options as BentoCheckboxGroupFilterOptions)?.checkboxItems?.length; if (isBentoSelectFilter || isBentoCheckboxGroupFilter) { return t('all'); } return filterCount === null ? null : n(filterCount); }; const selectFilter = (filterLabel: string, filterField: string) => { selectedFilterIndex.value = filtersInModal.value.findIndex( el => el.label === filterLabel && el.field === filterField ); }; const onSearchbarClear = () => { searchTerm.value = ''; }; const cancel = () => { isModalOpen.value = false; filtersInModal.value = props.value; onSearchbarClear(); selectOpenedFilter(); // TODO: scroll filters section to top }; const updateFilter = (updatedFilter: BentoFilterModel) => { const updatedFilters = filtersInModal.value.map(({ value, ...otherFields }, index) => { if (index === selectedFilterIndex.value) { return { value: updatedFilter.value, ...otherFields }; } return { value, ...otherFields }; }); filtersInModal.value = updatedFilters; }; const clearFilter = () => { updateFilter({ field: selectedFilter.value.field, label: selectedFilter.value.label, type: selectedFilter.value.type, value: undefined, }); }; const clearAll = () => { filtersInModal.value = props.value.map(({ value, ...otherFields }) => ({ ...otherFields, value: undefined, })); }; const applyAll = () => { emit(AllFiltersModalEvent.INPUT, filtersInModal.value); isModalOpen.value = false; }; const isApplyAllButtonDisabled = computed( () => JSON.stringify(filtersInModal.value) === JSON.stringify(props.value) ); const modalActions = computed(() => [ { title: t('applyAll'), disabled: isApplyAllButtonDisabled.value, event: applyAll, }, { title: t('cancel'), event: cancel, }, { title: t('clearAll'), disabled: !numberOfActiveFilters.value, event: clearAll, }, ]); const onKeydownRight = () => { const focusableEl = getNextFocusableElement(1, selectedFilterRef.value.$el); focusableEl.focus(); }; const onKeydownLeft = () => { allFiltersModalFilterButtons.value[selectedFilter.value.label].$el.focus(); }; return { // Values allFiltersModalUniqueId, allFiltersModalFiltersSection, allFiltersModalFilterButtons, numberOfActiveFilters, isEmptyResultForSearchFilter, isModalOpen, isClearButtonDisabled, activeFilters, otherFilters, modalActions, selectedFilter, selectedFilterRef, searchTerm, shouldShowFilterTitle, // Methods onClickAllFiltersModal, getFilterCount, cancel, selectFilter, clearFilter, updateFilter, onSearchbarClear, onKeydownRight, onKeydownLeft, // Translations t, }; }, }); </script> <style lang="scss" scoped src="./all-filters-modal.scss" />
|
|
1
|
+
<template> <div class="b-all-filters-modal"> <filter-bar-button :is-filter-open="isModalOpen" :applied-filters-count="numberOfActiveFilters" no-applied-state-styles @click="onClickAllFiltersModal" > {{ t('allFilters') }} <template #iconLeft> <sliders-icon aria-hidden="true" /> </template> </filter-bar-button> <bento-modal class="'b-all-filters-modal__modal'" :is-open="isModalOpen" :with-column-layout="true" size="large" :actions="modalActions" :is-dismissible="false" hide-close-button @close-modal="cancel" > <template #default> {{ t('allFilters') }} </template> <template #content-col-1> <div ref="allFiltersModalFiltersSection" class="b-all-filters-modal__filters" @keydown.right="onKeydownRight" > <bento-search-bar class="b-all-filters-modal__search-bar" :value="searchTerm" :input-field-aria-label="t('searchBar').toString()" @input="searchTerm = $event" @clear="onSearchbarClear" /> <div v-if="isEmptyResultForSearchFilter" class="b-all-filters-modal__empty-search-message"> <bento-typography el="span"> {{ t('noFiltersMatchThisSearch') }}</bento-typography> </div> <div v-if="activeFilters.length" class="b-all-filters-modal__filters-section"> <bento-typography :id="`active-filters-${allFiltersModalUniqueId}`" class="b-all-filters-modal__filters-section-title" el="span" variant="caption" > {{ t('activeFilters') }} </bento-typography> <ul class="b-all-filters-modal__filters-list" :aria-labelledby="`active-filters-${allFiltersModalUniqueId}`" > <li v-for="item in activeFilters" :key="`${item.label}-${item.field}`"> <all-filters-modal-button :ref="el => (allFiltersModalFilterButtons[item.label] = el)" :label="item.label" :applied-filters-count="getFilterCount(item)" :is-filter-open="selectedFilter.label === item.label" :disabled="item.disabled" @click="selectFilter(item.label, item.field)" /> </li> </ul> </div> <div v-if="otherFilters.length" class="b-all-filters-modal__filters-section"> <bento-typography v-if="activeFilters.length" :id="`other-filters-${allFiltersModalUniqueId}`" class="b-all-filters-modal__filters-section-title" el="span" variant="caption" > {{ t('otherFilters') }} </bento-typography> <ul class="b-all-filters-modal__filters-list" :aria-labelledby="`other-filters-${allFiltersModalUniqueId}`" > <li v-for="item in otherFilters" :key="`${item.label}-${item.field}`"> <all-filters-modal-button :ref="el => (allFiltersModalFilterButtons[item.label] = el)" :label="item.label" :disabled="item.disabled" :applied-filters-count="getFilterCount(item)" :is-filter-open="selectedFilter.label === item.label" @click="selectFilter(item.label, item.field)" /> </li> </ul> </div> </div> </template> <template #content-col-2> <div class="b-all-filters-modal__selected-filter-content" @keydown.left="onKeydownLeft"> <div v-if="shouldShowFilterTitle" class="b-all-filters-modal__selected-filter-text"> <bento-typography :id="selectedFilter.labelId" el="div" variant="title" medium> {{ selectedFilter.label }} </bento-typography> <bento-typography v-if="selectedFilter.description" el="div" variant="body" wide> {{ selectedFilter.description }} </bento-typography> </div> <div class="b-all-filters-modal__selected-filter-component"> <component :is="selectedFilter.type" ref="selectedFilterRef" :key="selectedFilter.field" :description="selectedFilter.description" :field="selectedFilter.field" :label="selectedFilter.label" :options="selectedFilter.options" :value="selectedFilter.value" :default-value="selectedFilter.defaultValue" :only-slot="true" :disabled="selectedFilter.disabled" :aria-labelledby="selectedFilter.labelId" @input="updateFilter" /> </div> <div class="b-all-filters-modal__clear-button"> <bento-button variant="secondary" :disabled="isClearButtonDisabled" @click="clearFilter">{{ t('clear') }}</bento-button> </div> </div> </template> </bento-modal> </div> </template> <script lang="ts"> import { computed, defineComponent, onBeforeUpdate, onMounted, type PropType, ref, toRef, watch } from 'vue'; import BentoModal from '@/components/modal/modal.vue'; import { BentoButton } from '@/components/button'; import { type BentoFilterBarModel, BentoFilterItemType, type BentoFilterModel, type BentoSelectFilterOptions, } from '@/components/filter-bar/filter-bar.types'; import { BentoBooleanFilter, BentoCheckboxGroupFilter, BentoDateFilter, BentoDateRangeFilter, BentoInputFilter, BentoInputWithDropdownFilter, BentoRadioGroupFilter, BentoRangeFilter, BentoSelectAvatarFilter, BentoSelectCountryFilter, BentoSelectCurrencyFilter, BentoSelectFilter, BentoSelectPaymentMethodFilter, BentoSelectTagFilter, BentoToggleFilter, } from '../index'; import { type BentoDateRangePickerValue } from '@/components/date-range-picker'; import BentoSearchBar from '@/components/search-bar/search-bar.vue'; import { BentoTypography } from '@/components/typography'; import { useSearchBarFilter } from '@/components/search-bar'; import { generateUid } from '@/core/utils/ts'; import { getNextFocusableElement } from '@/utils/ts/focus-trap.utils'; import { FilterBarButton } from '../filter-bar-button'; import { AllFiltersModalEvent } from './all-filters-modal.types'; import { useI18n } from '@/utils/ts/i18n'; import AllFiltersModalButton from './components/all-filters-modal-button/all-filters-modal-button.vue'; import SlidersIcon from '@adyen/ui-assets-icons-16/vue/sliders-1'; import messages from './messages.json'; import { type BentoCheckboxGroupFilterOptions } from '../checkbox-group-filter'; type MessageSchema = (typeof messages)['en-US']; /** * A modal that acts as sort of filter manager for the BentoFilterBar. * * @example * import { BentoAllFiltersModal } from '@adyen/bento-vue2'; * * export default { * components: { BentoAllFiltersModal }, * template: ` * <bento-all-filters-modal> * ...Example code of the component * </bento-all-filters-modal> * ` * } */ export default defineComponent({ i18n: { messages }, name: 'all-filters-modal', components: { AllFiltersModalButton, BentoBooleanFilter, BentoButton, BentoCheckboxGroupFilter, BentoDateFilter, BentoDateRangeFilter, BentoInputFilter, BentoInputWithDropdownFilter, BentoModal, BentoRadioGroupFilter, BentoRangeFilter, BentoSearchBar, BentoSelectAvatarFilter, BentoSelectCurrencyFilter, BentoSelectCountryFilter, BentoSelectFilter, BentoSelectPaymentMethodFilter, BentoSelectTagFilter, BentoToggleFilter, BentoTypography, FilterBarButton, SlidersIcon, }, props: { /** * Array of filters based on {@see FilterBarModel}. * Each one of the elements in the array correspond to a filter that will * be created and rendered in the AllFiltersModal. * * Can be used with v-model or independently with the @input event. * * @example * [{ * field: 'company', * label: 'Company', * value: '200', * type: FilterType.TEXT, * }, { * field: 'name', * label: 'Name', * value: null, * type: FilterType.TEXT, * }] */ value: { type: Array as PropType<BentoFilterBarModel>, default: () => [], }, }, emits: [AllFiltersModalEvent.INPUT], setup(props, { emit }) { const filtersInModal = ref(props.value); const allFiltersModalUniqueId = generateUid('all-filters-modal'); const allFiltersModalFiltersSection = ref([]); const isModalOpen = ref(false); const selectedFilterIndex = ref(0); const selectedFilterRef = ref(null); const searchTerm = ref<string>(''); const { t, n } = useI18n<{ message: MessageSchema }>({ messages }); const filteredItems = useSearchBarFilter<BentoFilterModel>(searchTerm, toRef(filtersInModal, 'value')); const allFiltersModalFilterButtons = ref({}); onBeforeUpdate(() => { allFiltersModalFilterButtons.value = []; }); watch( () => props.value, () => { /** * Values need to be preserve when the modal is opened given that * when the "select" filter is enabled with external filtering inside the modal * the "searchTerm" gets reset and doing so resets all the unsaved values of the array */ if (isModalOpen.value) { filtersInModal.value = props.value.map((filter, index) => { return { ...filter, value: filtersInModal.value[index].value }; }); } else { filtersInModal.value = props.value; } } ); const sortAlphabetically = (array: BentoFilterBarModel) => array.sort((a, b) => (a.label > b.label ? 1 : b.label > a.label ? -1 : 0)); const activeFilters = computed(() => sortAlphabetically( filteredItems.value.filter(el => Array.isArray(el.value) ? el.value?.length && el.value?.find(item => item !== '') : el.value ) ) ); const otherFilters = computed(() => sortAlphabetically( filteredItems.value.filter(el => Array.isArray(el.value) ? el.value?.length === 0 || el.value?.every(item => item === '') : !el.value ) ) ); const isEmptyResultForSearchFilter = computed(() => searchTerm.value && filteredItems.value?.length === 0); const selectedFilter = computed(() => ({ ...filtersInModal.value[selectedFilterIndex.value], labelId: generateUid(filtersInModal.value[selectedFilterIndex.value]?.field), })); const selectOpenedFilter = () => { const openedFilter = activeFilters.value.length ? activeFilters.value[0] : otherFilters.value[0]; const openedFilterIndex = filtersInModal.value.findIndex( el => el.label === openedFilter.label && el.field === openedFilter.field ); selectedFilterIndex.value = openedFilterIndex; }; onMounted(() => { selectOpenedFilter(); }); const computeCounts = (filterValue: BentoFilterBarModel) => filterValue.map(filter => { switch (filter.type) { case BentoFilterItemType.SELECT: case BentoFilterItemType.SELECT_AVATAR: case BentoFilterItemType.SELECT_COUNTRY: case BentoFilterItemType.SELECT_CURRENCY: case BentoFilterItemType.SELECT_PAYMENT_METHOD: case BentoFilterItemType.SELECT_TAG: switch ((filter.options as BentoSelectFilterOptions)?.multiple) { case true: return { label: filter.label, field: filter.field, count: (filter.value as Array<string | number>)?.length ? (filter.value as Array<string | number>)?.length : null, }; default: return { label: filter.label, field: filter.field, count: filter.value == null ? null : 1, }; } case BentoFilterItemType.DATE_RANGE: return { label: filter.label, field: filter.field, count: (filter.value as BentoDateRangePickerValue)?.startDate != null && (filter.value as BentoDateRangePickerValue)?.endDate != null ? 1 : null, }; case BentoFilterItemType.RANGE: return { label: filter.label, field: filter.field, count: (filter.value as Array<string>)?.find(el => el !== '') ? 1 : null, }; case BentoFilterItemType.CHECKBOX_GROUP: return { label: filter.label, field: filter.field, count: (filter.value as Array<string | number>)?.length ? (filter.value as Array<string | number>)?.length : null, }; case BentoFilterItemType.TOGGLE: return { label: filter.label, field: filter.field, count: filter.value ? 1 : null, }; case BentoFilterItemType.INPUT: case BentoFilterItemType.BOOLEAN: case BentoFilterItemType.DATE: case BentoFilterItemType.RADIO_GROUP: default: return { label: filter.label, field: filter.field, count: filter.value != null ? 1 : null, }; } }); const hasValueSelected = computed(() => Array.isArray(selectedFilter.value.value) ? selectedFilter.value?.value.length && selectedFilter.value.value.find(item => item !== '') : !!filtersInModal.value[selectedFilterIndex.value].value ); /** * Hides filter title if the selected filter is Toggle. * The Toggle filter shows a checkbox next to the "title". */ const shouldShowFilterTitle = computed(() => { return selectedFilter.value.type !== BentoFilterItemType.TOGGLE; }); const isClearButtonDisabled = computed(() => !hasValueSelected.value || selectedFilter.value.disabled); const numberOfActiveFilters = computed( () => computeCounts(props.value)?.filter(({ count }) => !!count).length ); const onClickAllFiltersModal = () => { if (!isModalOpen.value) { isModalOpen.value = true; } else { isModalOpen.value = false; } }; const getFilterCount = (filter: BentoFilterModel) => { const filterCount = computeCounts(filtersInModal.value).find(el => el.field === filter.field).count; const isBentoSelectFilter = typeof filter.type === 'string' && // Type guard to avoid casting filter.type.includes('BentoSelect') && (filter.options as BentoSelectFilterOptions)?.multiple && filterCount === (filter.options as BentoSelectFilterOptions)?.listboxItems?.length; const isBentoCheckboxGroupFilter = typeof filter.type === 'string' && filter.type === BentoFilterItemType.CHECKBOX_GROUP && filterCount === (filter.options as BentoCheckboxGroupFilterOptions)?.checkboxItems?.length; if (isBentoSelectFilter || isBentoCheckboxGroupFilter) { return t('all'); } return filterCount === null ? null : n(filterCount); }; const selectFilter = (filterLabel: string, filterField: string) => { selectedFilterIndex.value = filtersInModal.value.findIndex( el => el.label === filterLabel && el.field === filterField ); }; const onSearchbarClear = () => { searchTerm.value = ''; }; const cancel = () => { isModalOpen.value = false; filtersInModal.value = props.value; onSearchbarClear(); selectOpenedFilter(); // TODO: scroll filters section to top }; const updateFilter = (updatedFilter: BentoFilterModel) => { const updatedFilters = filtersInModal.value.map(({ value, ...otherFields }, index) => { if (index === selectedFilterIndex.value) { return { value: updatedFilter.value, ...otherFields }; } return { value, ...otherFields }; }); filtersInModal.value = updatedFilters; }; const clearFilter = () => { updateFilter({ field: selectedFilter.value.field, label: selectedFilter.value.label, type: selectedFilter.value.type, value: undefined, }); }; const clearAll = () => { filtersInModal.value = props.value.map(({ value, ...otherFields }) => ({ ...otherFields, value: undefined, })); }; const applyAll = () => { emit(AllFiltersModalEvent.INPUT, filtersInModal.value); isModalOpen.value = false; }; const isApplyAllButtonDisabled = computed( () => JSON.stringify(filtersInModal.value) === JSON.stringify(props.value) ); const modalActions = computed(() => [ { title: t('applyAll'), disabled: isApplyAllButtonDisabled.value, event: applyAll, }, { title: t('cancel'), event: cancel, }, { title: t('clearAll'), disabled: !numberOfActiveFilters.value, event: clearAll, }, ]); const onKeydownRight = () => { const focusableEl = getNextFocusableElement(1, selectedFilterRef.value.$el); focusableEl.focus(); }; const onKeydownLeft = () => { allFiltersModalFilterButtons.value[selectedFilter.value.label].$el.focus(); }; return { // Values allFiltersModalUniqueId, allFiltersModalFiltersSection, allFiltersModalFilterButtons, numberOfActiveFilters, isEmptyResultForSearchFilter, isModalOpen, isClearButtonDisabled, activeFilters, otherFilters, modalActions, selectedFilter, selectedFilterRef, searchTerm, shouldShowFilterTitle, // Methods onClickAllFiltersModal, getFilterCount, cancel, selectFilter, clearFilter, updateFilter, onSearchbarClear, onKeydownRight, onKeydownLeft, // Translations t, }; }, }); </script> <style lang="scss" scoped src="./all-filters-modal.scss" />
|
package/dist/assets/components/filter-bar/components/date-range-filter/date-range-filter.vue
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <bento-base-filter ref="baseFilter" class="b-base-filter-date-range-filter" :button-label="buttonLabel" :button-secondary-label="buttonSecondaryLabel" :controlled="!onlySlot" :disabled="disabled" :disable-apply-button="isApplyButtonDisabled" :disable-secondary-button="isSecondaryButtonDisabled" :default-value="defaultValue" :field="field" :label="label" :only-slot="onlySlot" :tooltip-position="options?.tooltipPosition" :tooltip-text="options?.tooltipText" fit-content @open="openFilter" @clear="clearFilter" @reset="resetFilter" @cancel="onCancel" > <bento-date-range-picker v-if="onlySlot" :allow-time-input="options?.allowTimeInput" :model-value="internalFilterValue" :first-day-of-week="options?.firstDayOfWeek" :is-date-disabled="options?.isDateDisabled" :number-of-months="options?.numberOfMonths" :quick-select-ranges="options?.quickSelectRanges" :variant="options?.variant" :disabled="disabled" :granularities="options?.granularities" :max-range="options?.maxRange" :min="options?.min" :max="options?.max" :date-form-data="formData" :show-end-date-on-open="options?.showEndDateOnOpen" @update:model-value="onDateInputSelected" /> <template v-else> <div class="b-date-range-filter"> <date-range-picker-calendar :allow-time-input="options?.allowTimeInput" :first-day-of-week="options?.firstDayOfWeek" :is-date-disabled="options?.isDateDisabled" :granularities="options?.granularities" :max-range="options?.maxRange" :min="options?.min" :max="options?.max" :number-of-months="options?.numberOfMonths" :quick-select-ranges="adjustedQuickSelectRanges" :value="internalFilterValue" :date-form-data="formData" :show-end-date-on-open="options?.showEndDateOnOpen" :variant="options?.variant" @input="onDateSelected" @custom-range="onRangeSelectorInput" @error="onDatePickerRangeError" @form-date="onDatePickerRangeFormDateInputUpdate" @start-date-selected="isSelectingDate = true" > <template #title> <bento-typography strongest variant="body" el="span"> {{ label }} </bento-typography> </template> <template #actions> <bento-divider class="b-date-range-filter__divider" /> <bento-button-actions class="b-date-range-filter__actions" :actions="actions" layout="space-between" /> </template> </date-range-picker-calendar> </div> </template> </bento-base-filter> </template> <script lang="ts"> import { computed, defineComponent, type PropType, reactive, ref, toRef, watch } from 'vue'; import { BentoBaseFilter, useBaseFilter } from '../base-filter'; import { type BentoDateRangeFilterOptions, type BentoDateRangeFilterValue, BentoFilterEvent, } from '../../filter-bar.types'; import { DateRangePickerCalendar } from '@/components/date-range-picker/components/date-range-picker-calendar'; import { BentoDateRangePicker, type BentoDateRangePickerValue } from '@/components/date-range-picker'; import { BentoDivider } from '@/components/divider'; import { BentoTypography } from '@/components/typography'; import { BentoButtonActions, type BentoButtonActionsList } from '@/components/button/components/button-actions'; import { useI18n } from '@/utils/ts/i18n'; import { getDateLocale } from '@/utils/ts/get-date-locale/get-date-locale'; import { isSameDay } from 'date-fns/isSameDay'; import { format } from 'date-fns/format'; import { isEqual } from 'date-fns/isEqual'; import { isToday } from 'date-fns/isToday'; import { dateToInputDateString, dateToTimeInputString } from '@/utils/ts/format-date'; import messages from './messages.json'; import { type DateRangePickerCalendarFormData, type DateRangePickerCalendarRangeSelectorItem, type DateRangePickerCalendarRangeSelectorItems, } from '@/components/date-range-picker/components/date-range-picker-calendar/date-range-picker-calendar.types'; type MessageSchema = (typeof messages)['en-US']; /** * The Date filter displays a calendar that allows * to select a single date. * * @example * import { BentoDateRangeFilter } from '@adyen/bento-vue2'; * * export default { * components: { BentoDateRangeFilter }, * template: ` * <bento-date-range-filter * :field="filter.field" * :label="filter.label" * :options="filter.options" * :value="filter.value" * @update="updateFilterHandler" * > * ` * } */ export default defineComponent({ i18n: { messages }, name: 'bento-date-range-filter', components: { BentoBaseFilter, BentoButtonActions, BentoDateRangePicker, BentoTypography, BentoDivider, DateRangePickerCalendar, }, props: { /** * Disables the filter from being interacted with. */ disabled: { type: Boolean, default: false }, /** * Default value of the filter to reset to. */ defaultValue: { type: Object as PropType<BentoDateRangeFilterValue>, default: null }, /** * Filter name/key referencing the actual property name in the data set. * Should be unique per filter bar. */ field: { type: String, required: true }, /** * Label text to be displayed */ label: { type: String, required: true }, /** * Renders the base filter's slot content only - leaving out the button and the popover. * Used for the allFiltersModal component. */ onlySlot: { type: Boolean, default: false }, /** * List of options that are needed to setup the filter * Available options: * - allowTimeInput: Allows user to enter time values in the form * - firstDayOfWeek: Allows user to set the first day of the week. 0 is sunday, 1 is monday, 6 is saturday * - isDateDisabled: Indicate if a date should be disabled or not * - maxRange: Set a maximum number of dates to be selectable by the range * - numberOfMonths: Number of months rendered on pane * - quickSelectRanges: An array that sets the custom range dropdown items. @see BentoDateRangePicker * - showEndDateOnOpen: Upon opening date range picker, end date's month will be pre-selected. * - granularities: A list of available granularities. * - variant: The type of calendar to display. Defaults to showing days. * - tooltipPosition: The position of the tooltip attached to the filter button * - tooltipText: The content of the tooltip attached to the filter button */ options: { type: Object as PropType<BentoDateRangeFilterOptions>, default: undefined, }, /** * Current value of the filter */ value: { type: Object as PropType<BentoDateRangeFilterValue>, default: () => ({ startDate: undefined, endDate: undefined }), }, }, emits: [BentoFilterEvent.INPUT, BentoFilterEvent.UPDATE], setup(props, { emit }) { const { locale, t } = useI18n<{ message: MessageSchema }>({ messages }); const locales = getDateLocale(); const { internalFilterValue, isSecondaryButtonDisabled, isApplyButtonDisabled } = useBaseFilter( toRef(props, 'value'), toRef(props, 'defaultValue') ); const isSelectingDate = ref(false); const adjustedQuickSelectRanges = computed<Array<DateRangePickerCalendarRangeSelectorItem>>(() => { return ( props?.options?.quickSelectRanges && props.options.quickSelectRanges.map((range: DateRangePickerCalendarRangeSelectorItem) => { const timeDiff = !range.data.endDate ? new Date(Date.now()).getTime() - range.data.startDate.getTime() : undefined; return { ...range, data: { ...range.data, // Adding timeDifference to calculate an up-to-date endDate on range selection timeDifference: timeDiff, }, }; }) ); }); const baseFilter = ref(null); const isDateIncorrect = ref(false); const formData = reactive({ startDate: dateToInputDateString(props.value?.startDate), endDate: dateToInputDateString(props.value?.endDate), startTime: dateToTimeInputString(props.value?.startDate), endTime: dateToTimeInputString(props.value?.endDate), }); watch( () => props.value, () => { formData.startDate = dateToInputDateString(props.value?.startDate); formData.endDate = dateToInputDateString(props.value?.endDate); formData.startTime = dateToTimeInputString(props.value?.startDate); formData.endTime = dateToTimeInputString(props.value?.endDate); } ); function isQuickSelectAnArray( quickSelectRanges: BentoDateRangeFilterOptions['quickSelectRanges'] ): quickSelectRanges is DateRangePickerCalendarRangeSelectorItems { return (quickSelectRanges as DateRangePickerCalendarRangeSelectorItems)?.length !== undefined; } const calculateSecondaryLabel = (currentValue: BentoDateRangeFilterValue) => { if (!currentValue || (!currentValue?.startDate && !currentValue.endDate)) { return null; } // If quick select range selected then display its label if ( isQuickSelectAnArray(props.options?.quickSelectRanges) && props.options?.quickSelectRanges?.length > 0 ) { const customRangeSelected = props.options?.quickSelectRanges?.find(({ value, data }) => { if (currentValue) { if (currentValue.range) { return value === currentValue.range; } if (!currentValue.startDate || !currentValue.endDate) { return false; } if (!data?.endDate) { // Autoselecting the correct value for open-ended ranges return ( isEqual(data?.startDate, currentValue.startDate) && isToday(currentValue.endDate) ); } // If no range selected but start and end dates correspond to one of the preselected ranges, select it return ( isEqual(data?.startDate, currentValue.startDate) && isEqual(data?.endDate, currentValue.endDate) ); } return false; }); if (customRangeSelected) { return customRangeSelected.label; } } if (!currentValue.startDate || !currentValue.endDate) { return null; } return isSameDay(currentValue.startDate, currentValue.endDate) && props?.options?.allowTimeInput ? `${format(currentValue.startDate, 'dd MMM, yyyy HH:mm:ss', { locale: locales[locale.value?.substring(0, 2)] || locales.enUS, })} - ${format(currentValue.endDate, 'HH:mm:ss', { locale: locales[locale.value?.substring(0, 2)] || locales.enUS, })}` : `${format(currentValue.startDate, 'dd MMM, yyyy', { locale: locales[locale.value?.substring(0, 2)] || locales.enUS, })} - ${format(currentValue.endDate, 'dd MMM, yyyy', { locale: locales[locale.value?.substring(0, 2)] || locales.enUS, })}`; }; const buttonSecondaryLabel = ref(calculateSecondaryLabel(props.value)); const buttonLabel = computed(() => props.label); const updateFilter = () => { baseFilter.value.closePopover(); // Omit the onlySlot prop as it is not needed const { onlySlot, ...cleanedProps } = props; emit(BentoFilterEvent.UPDATE, { ...cleanedProps, value: internalFilterValue.value }); }; const clearFilter = () => { internalFilterValue.value = undefined; updateFilter(); }; const resetFilter = () => { internalFilterValue.value = props.defaultValue; updateFilter(); }; const openFilter = () => { internalFilterValue.value = props.value; }; // If we click outside or press escape this should cancel and reset the form inputs // set all internal states back to their initial values const onCancel = () => { internalFilterValue.value = props.value; isSelectingDate.value = false; formData.startDate = dateToInputDateString(props.value?.startDate); formData.endDate = dateToInputDateString(props.value?.endDate); formData.startTime = dateToTimeInputString(props.value?.startDate); formData.endTime = dateToTimeInputString(props.value?.endDate); }; const onDateSelected = (selectedDate: BentoDateRangePickerValue) => { isSelectingDate.value = false; internalFilterValue.value = selectedDate; formData.startDate = dateToInputDateString(selectedDate.startDate); formData.endDate = dateToInputDateString(selectedDate.endDate); emit(BentoFilterEvent.INPUT, { ...props, value: internalFilterValue.value }); }; const onRangeSelectorInput = (quickSelectRanges?: { startDate: Date; endDate: Date }) => { if (quickSelectRanges?.startDate && quickSelectRanges?.endDate) { internalFilterValue.value = quickSelectRanges; formData.startDate = dateToInputDateString(quickSelectRanges.startDate); formData.endDate = dateToInputDateString(quickSelectRanges.endDate); emit(BentoFilterEvent.INPUT, { ...props, value: internalFilterValue.value }); } }; // Need to update the internal value and emit the date when using the date-picker input const onDateInputSelected = (selectedDate: BentoDateRangePickerValue) => { onDateSelected(selectedDate); emit(BentoFilterEvent.INPUT, { ...props, value: internalFilterValue.value }); }; const actions = computed( () => [ { title: t('apply') as string, event: updateFilter, disabled: isSelectingDate.value || isDateIncorrect.value || isApplyButtonDisabled.value, }, { title: (props.defaultValue ? t('reset') : t('clear')) as string, event: (props.defaultValue ? resetFilter : clearFilter) as () => void, disabled: isDateIncorrect.value && isSecondaryButtonDisabled.value, }, ] as BentoButtonActionsList ); const onDatePickerRangeError = () => { isDateIncorrect.value = true; internalFilterValue.value = { startDate: undefined, endDate: undefined }; emit(BentoFilterEvent.INPUT, { ...props, value: internalFilterValue.value }); }; const onDatePickerRangeFormDateInputUpdate = (dateFormDate: DateRangePickerCalendarFormData) => { isDateIncorrect.value = false; if (dateFormDate.startTime) { formData.startTime = dateFormDate.startTime; } if (dateFormDate.endTime) { formData.endTime = dateFormDate.endTime; } }; watch( () => props.value, newValue => { buttonSecondaryLabel.value = calculateSecondaryLabel(newValue); }, { deep: true } ); return { // Refs baseFilter, // Values actions, formData, internalFilterValue, isApplyButtonDisabled, isSecondaryButtonDisabled, isSelectingDate, buttonLabel, buttonSecondaryLabel, adjustedQuickSelectRanges, // Events onCancel, onDateSelected, openFilter, clearFilter, resetFilter, updateFilter, onDatePickerRangeError, onDatePickerRangeFormDateInputUpdate, onDateInputSelected, onRangeSelectorInput, }; }, }); </script> <style lang="scss" scoped src="./date-range-filter.scss" />
|
|
1
|
+
<template> <bento-base-filter ref="baseFilter" class="b-base-filter-date-range-filter" :button-label="buttonLabel" :button-secondary-label="buttonSecondaryLabel" :controlled="!onlySlot" :disabled="disabled" :disable-apply-button="isApplyButtonDisabled" :disable-secondary-button="isSecondaryButtonDisabled" :default-value="defaultValue" :field="field" :label="label" :only-slot="onlySlot" :tooltip-position="options?.tooltipPosition" :tooltip-text="options?.tooltipText" fit-content @open="openFilter" @clear="clearFilter" @reset="resetFilter" @cancel="onCancel" > <bento-date-range-picker v-if="onlySlot" :allow-time-input="options?.allowTimeInput" :model-value="internalFilterValue" :first-day-of-week="options?.firstDayOfWeek" :is-date-disabled="options?.isDateDisabled" :number-of-months="options?.numberOfMonths" :quick-select-ranges="options?.quickSelectRanges" :variant="options?.variant" :disabled="disabled" :granularities="options?.granularities" :max-range="options?.maxRange" :min="options?.min" :max="options?.max" :date-form-data="formData" :show-end-date-on-open="options?.showEndDateOnOpen" :aria-labelledby="$attrs?.['aria-labelledby']" @update:model-value="onDateInputSelected" /> <template v-else> <div class="b-date-range-filter"> <date-range-picker-calendar :allow-time-input="options?.allowTimeInput" :first-day-of-week="options?.firstDayOfWeek" :is-date-disabled="options?.isDateDisabled" :granularities="options?.granularities" :max-range="options?.maxRange" :min="options?.min" :max="options?.max" :number-of-months="options?.numberOfMonths" :quick-select-ranges="adjustedQuickSelectRanges" :value="internalFilterValue" :date-form-data="formData" :show-end-date-on-open="options?.showEndDateOnOpen" :variant="options?.variant" @input="onDateSelected" @custom-range="onRangeSelectorInput" @error="onDatePickerRangeError" @form-date="onDatePickerRangeFormDateInputUpdate" @start-date-selected="isSelectingDate = true" > <template #title> <bento-typography strongest variant="body" el="span"> {{ label }} </bento-typography> </template> <template #actions> <bento-divider class="b-date-range-filter__divider" /> <bento-button-actions class="b-date-range-filter__actions" :actions="actions" layout="space-between" /> </template> </date-range-picker-calendar> </div> </template> </bento-base-filter> </template> <script lang="ts"> import { computed, defineComponent, type PropType, reactive, ref, toRef, watch } from 'vue'; import { BentoBaseFilter, useBaseFilter } from '../base-filter'; import { type BentoDateRangeFilterOptions, type BentoDateRangeFilterValue, BentoFilterEvent, } from '../../filter-bar.types'; import { DateRangePickerCalendar } from '@/components/date-range-picker/components/date-range-picker-calendar'; import { BentoDateRangePicker, type BentoDateRangePickerValue } from '@/components/date-range-picker'; import { BentoDivider } from '@/components/divider'; import { BentoTypography } from '@/components/typography'; import { BentoButtonActions, type BentoButtonActionsList } from '@/components/button/components/button-actions'; import { useI18n } from '@/utils/ts/i18n'; import { getDateLocale } from '@/utils/ts/get-date-locale/get-date-locale'; import { isSameDay } from 'date-fns/isSameDay'; import { format } from 'date-fns/format'; import { isEqual } from 'date-fns/isEqual'; import { isToday } from 'date-fns/isToday'; import { dateToTimeInputString } from '@/utils/ts/format-date'; import { useDateInputFormatter } from '@/composables/use-date-input-formatter/use-date-input-formatter'; import messages from './messages.json'; import { type DateRangePickerCalendarFormData, type DateRangePickerCalendarRangeSelectorItem, type DateRangePickerCalendarRangeSelectorItems, } from '@/components/date-range-picker/components/date-range-picker-calendar/date-range-picker-calendar.types'; type MessageSchema = (typeof messages)['en-US']; /** * The Date filter displays a calendar that allows * to select a single date. * * @example * import { BentoDateRangeFilter } from '@adyen/bento-vue2'; * * export default { * components: { BentoDateRangeFilter }, * template: ` * <bento-date-range-filter * :field="filter.field" * :label="filter.label" * :options="filter.options" * :value="filter.value" * @update="updateFilterHandler" * > * ` * } */ export default defineComponent({ i18n: { messages }, name: 'bento-date-range-filter', components: { BentoBaseFilter, BentoButtonActions, BentoDateRangePicker, BentoTypography, BentoDivider, DateRangePickerCalendar, }, props: { /** * Disables the filter from being interacted with. */ disabled: { type: Boolean, default: false }, /** * Default value of the filter to reset to. */ defaultValue: { type: Object as PropType<BentoDateRangeFilterValue>, default: null }, /** * Filter name/key referencing the actual property name in the data set. * Should be unique per filter bar. */ field: { type: String, required: true }, /** * Label text to be displayed */ label: { type: String, required: true }, /** * Renders the base filter's slot content only - leaving out the button and the popover. * Used for the allFiltersModal component. */ onlySlot: { type: Boolean, default: false }, /** * List of options that are needed to setup the filter * Available options: * - allowTimeInput: Allows user to enter time values in the form * - firstDayOfWeek: Allows user to set the first day of the week. 0 is sunday, 1 is monday, 6 is saturday * - isDateDisabled: Indicate if a date should be disabled or not * - maxRange: Set a maximum number of dates to be selectable by the range * - numberOfMonths: Number of months rendered on pane * - quickSelectRanges: An array that sets the custom range dropdown items. @see BentoDateRangePicker * - showEndDateOnOpen: Upon opening date range picker, end date's month will be pre-selected. * - granularities: A list of available granularities. * - variant: The type of calendar to display. Defaults to showing days. * - tooltipPosition: The position of the tooltip attached to the filter button * - tooltipText: The content of the tooltip attached to the filter button */ options: { type: Object as PropType<BentoDateRangeFilterOptions>, default: undefined, }, /** * Current value of the filter */ value: { type: Object as PropType<BentoDateRangeFilterValue>, default: () => ({ startDate: undefined, endDate: undefined }), }, }, emits: [BentoFilterEvent.INPUT, BentoFilterEvent.UPDATE], setup(props, { emit }) { const { locale, t } = useI18n<{ message: MessageSchema }>({ messages }); const locales = getDateLocale(); const { dateToInputDateString } = useDateInputFormatter(); const { internalFilterValue, isSecondaryButtonDisabled, isApplyButtonDisabled } = useBaseFilter( toRef(props, 'value'), toRef(props, 'defaultValue') ); const isSelectingDate = ref(false); const adjustedQuickSelectRanges = computed<Array<DateRangePickerCalendarRangeSelectorItem>>(() => { return ( props?.options?.quickSelectRanges && props.options.quickSelectRanges.map((range: DateRangePickerCalendarRangeSelectorItem) => { const timeDiff = !range.data.endDate ? new Date(Date.now()).getTime() - range.data.startDate.getTime() : undefined; return { ...range, data: { ...range.data, // Adding timeDifference to calculate an up-to-date endDate on range selection timeDifference: timeDiff, }, }; }) ); }); const baseFilter = ref(null); const isDateIncorrect = ref(false); const formData = reactive({ startDate: dateToInputDateString(props.value?.startDate), endDate: dateToInputDateString(props.value?.endDate), startTime: dateToTimeInputString(props.value?.startDate), endTime: dateToTimeInputString(props.value?.endDate), }); watch( () => props.value, () => { formData.startDate = dateToInputDateString(props.value?.startDate); formData.endDate = dateToInputDateString(props.value?.endDate); formData.startTime = dateToTimeInputString(props.value?.startDate); formData.endTime = dateToTimeInputString(props.value?.endDate); } ); function isQuickSelectAnArray( quickSelectRanges: BentoDateRangeFilterOptions['quickSelectRanges'] ): quickSelectRanges is DateRangePickerCalendarRangeSelectorItems { return (quickSelectRanges as DateRangePickerCalendarRangeSelectorItems)?.length !== undefined; } const calculateSecondaryLabel = (currentValue: BentoDateRangeFilterValue) => { if (!currentValue || (!currentValue?.startDate && !currentValue.endDate)) { return null; } // If quick select range selected then display its label if ( isQuickSelectAnArray(props.options?.quickSelectRanges) && props.options?.quickSelectRanges?.length > 0 ) { const customRangeSelected = props.options?.quickSelectRanges?.find(({ value, data }) => { if (currentValue) { if (currentValue.range) { return value === currentValue.range; } if (!currentValue.startDate || !currentValue.endDate) { return false; } if (!data?.endDate) { // Autoselecting the correct value for open-ended ranges return ( isEqual(data?.startDate, currentValue.startDate) && isToday(currentValue.endDate) ); } // If no range selected but start and end dates correspond to one of the preselected ranges, select it return ( isEqual(data?.startDate, currentValue.startDate) && isEqual(data?.endDate, currentValue.endDate) ); } return false; }); if (customRangeSelected) { return customRangeSelected.label; } } if (!currentValue.startDate || !currentValue.endDate) { return null; } return isSameDay(currentValue.startDate, currentValue.endDate) && props?.options?.allowTimeInput ? `${format(currentValue.startDate, 'dd MMM, yyyy HH:mm:ss', { locale: locales[locale.value?.substring(0, 2)] || locales.enUS, })} - ${format(currentValue.endDate, 'HH:mm:ss', { locale: locales[locale.value?.substring(0, 2)] || locales.enUS, })}` : `${format(currentValue.startDate, 'dd MMM, yyyy', { locale: locales[locale.value?.substring(0, 2)] || locales.enUS, })} - ${format(currentValue.endDate, 'dd MMM, yyyy', { locale: locales[locale.value?.substring(0, 2)] || locales.enUS, })}`; }; const buttonSecondaryLabel = ref(calculateSecondaryLabel(props.value)); const buttonLabel = computed(() => props.label); const updateFilter = () => { baseFilter.value.closePopover(); // Omit the onlySlot prop as it is not needed const { onlySlot, ...cleanedProps } = props; emit(BentoFilterEvent.UPDATE, { ...cleanedProps, value: internalFilterValue.value }); }; const clearFilter = () => { internalFilterValue.value = undefined; updateFilter(); }; const resetFilter = () => { internalFilterValue.value = props.defaultValue; updateFilter(); }; const openFilter = () => { internalFilterValue.value = props.value; }; // If we click outside or press escape this should cancel and reset the form inputs // set all internal states back to their initial values const onCancel = () => { internalFilterValue.value = props.value; isSelectingDate.value = false; formData.startDate = dateToInputDateString(props.value?.startDate); formData.endDate = dateToInputDateString(props.value?.endDate); formData.startTime = dateToTimeInputString(props.value?.startDate); formData.endTime = dateToTimeInputString(props.value?.endDate); }; const onDateSelected = (selectedDate: BentoDateRangePickerValue) => { isSelectingDate.value = false; internalFilterValue.value = selectedDate; formData.startDate = dateToInputDateString(selectedDate.startDate); formData.endDate = dateToInputDateString(selectedDate.endDate); emit(BentoFilterEvent.INPUT, { ...props, value: internalFilterValue.value }); }; const onRangeSelectorInput = (quickSelectRanges?: { startDate: Date; endDate: Date }) => { if (quickSelectRanges?.startDate && quickSelectRanges?.endDate) { internalFilterValue.value = quickSelectRanges; formData.startDate = dateToInputDateString(quickSelectRanges.startDate); formData.endDate = dateToInputDateString(quickSelectRanges.endDate); emit(BentoFilterEvent.INPUT, { ...props, value: internalFilterValue.value }); } }; // Need to update the internal value and emit the date when using the date-picker input const onDateInputSelected = (selectedDate: BentoDateRangePickerValue) => { onDateSelected(selectedDate); emit(BentoFilterEvent.INPUT, { ...props, value: internalFilterValue.value }); }; const actions = computed( () => [ { title: t('apply') as string, event: updateFilter, disabled: isSelectingDate.value || isDateIncorrect.value || isApplyButtonDisabled.value, }, { title: (props.defaultValue ? t('reset') : t('clear')) as string, event: (props.defaultValue ? resetFilter : clearFilter) as () => void, disabled: isDateIncorrect.value && isSecondaryButtonDisabled.value, }, ] as BentoButtonActionsList ); const onDatePickerRangeError = () => { isDateIncorrect.value = true; internalFilterValue.value = { startDate: undefined, endDate: undefined }; emit(BentoFilterEvent.INPUT, { ...props, value: internalFilterValue.value }); }; const onDatePickerRangeFormDateInputUpdate = (dateFormDate: DateRangePickerCalendarFormData) => { isDateIncorrect.value = false; if (dateFormDate.startTime) { formData.startTime = dateFormDate.startTime; } if (dateFormDate.endTime) { formData.endTime = dateFormDate.endTime; } }; watch( () => props.value, newValue => { buttonSecondaryLabel.value = calculateSecondaryLabel(newValue); }, { deep: true } ); return { // Refs baseFilter, // Values actions, formData, internalFilterValue, isApplyButtonDisabled, isSecondaryButtonDisabled, isSelectingDate, buttonLabel, buttonSecondaryLabel, adjustedQuickSelectRanges, // Events onCancel, onDateSelected, openFilter, clearFilter, resetFilter, updateFilter, onDatePickerRangeError, onDatePickerRangeFormDateInputUpdate, onDateInputSelected, onRangeSelectorInput, }; }, }); </script> <style lang="scss" scoped src="./date-range-filter.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div ref="filterBarRef" class="b-filter-bar"> <div v-if="!showSearchbarInsideContainer && isSearchBarEnabled" class="b-filter-bar__search-external-container"> <bento-search-bar class="b-filter-bar__search-bar" v-bind="searchBarProps" :input-field-aria-label="searchBarAriaLabel" :value="searchTerm" condensed @input="onSearchbarInput" @clear="onSearchbarClear" /> </div> <component :is="screenLayoutComponent" :class="containerConditionalClasses" condensed> <div :class="contentConditionalClasses"> <div v-if="showSearchbarInsideContainer && isSearchBarEnabled"> <bento-search-bar class="b-filter-bar__search-bar" v-bind="searchBarProps" :input-field-aria-label="searchBarAriaLabel" :value="searchTerm" condensed @input="onSearchbarInput" @clear="onSearchbarClear" /> </div> <template v-if="persistentFilters.length"> <component :is="filter.type" v-for="filter in persistentFilters" :key="filter.field" :field="filter.field" :label="filter.label" :disabled="filter?.disabled" :options="filter.options" :value="filter.value" :default-value="filter.defaultValue" @update="updateFilterHandler" /> </template> <component :is="filter.type" v-for="filter in regularFilters" :key="filter.field" :field="filter.field" :label="filter.label" :disabled="filter?.disabled" :options="filter.options" :value="filter.value" @update="updateFilterHandler" /> <!-- All filters button --> <all-filters-modal v-if="hasHiddenFilters" class="b-filter-bar__all-filters-modal" :value="valueAndFilterValuesPropsMerged" @input="updateAllFilters" /> <!-- Clear all filters button --> <filter-bar-button v-if="isClearAllVisible" class="b-filter-bar__clear-all" @click="clearAllFilters"> {{ t('clearFilters') }} <template #iconLeft> <refresh-icon aria-hidden="true" /> </template> </filter-bar-button> <!-- Reset all filters button --> <filter-bar-button v-if="isResetAllVisible" class="b-filter-bar__reset-all" @click="resetAllFilters"> {{ t('resetFilters') }} <template #iconLeft> <refresh-icon aria-hidden="true" /> </template> </filter-bar-button> </div> </component> </div> </template> <script setup lang="ts" generic=""> import { computed, ref, toRef } from 'vue'; import { FilterBarButton } from './components/filter-bar-button'; import { BentoFilterBarEvent, type BentoFilterBarModel, type BentoFilterBarProps, type BentoFilterModel, } from './filter-bar.types'; import BentoSearchBar from '@/components/search-bar/search-bar.vue'; import { BentoSearchBarEvent } from '@/components/search-bar/search-bar.types'; import { useBentoToastController } from '@/composables'; import { useI18n } from '@/utils/ts/i18n'; import { useContainerSizeOptionsLayout } from './composables/use-container-size-options-layout/use-container-size-options-layout'; import AllFiltersModal from './components/all-filters-modal/all-filters-modal.vue'; import RefreshIcon from '@adyen/ui-assets-icons-16/vue/refresh'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; // TODO: setup Vue3 generics in Bento 2.0.0 const props = withDefaults(defineProps<BentoFilterBarProps>(), { config: () => [], filterValues: () => [], containerSizeLayout: () => ({ medium: 'one-line', small: 'multi-line', }), search: undefined, searchTerm: undefined, value: () => [], showAppliedHiddenFilters: false, filterValuesObject: undefined, }); const emit = defineEmits([ BentoFilterBarEvent.INPUT, BentoFilterBarEvent.UPDATE_FILTER_VALUES_OBJECT, BentoFilterBarEvent.UPDATE_FILTER_VALUES_ARRAY, BentoSearchBarEvent.CLEAR, BentoSearchBarEvent.INPUT, BentoSearchBarEvent.UPDATE, ]); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const filterBarRef = ref<HTMLDivElement>(null); const valueAndFilterValuesPropsMerged = computed<BentoFilterBarModel>(() => (props.config?.length > 0 ? props.config : props.value)?.map(filterModel => { let filterValue = props.filterValues.find(({ field }) => field === filterModel.field)?.value; if (props.filterValuesObject && filterModel.field in props.filterValuesObject) { filterValue = props.filterValuesObject[filterModel.field]; } if (filterValue === undefined) { // If the filter is using the deprecated `config` value prop use that // But if that is also not set then check if a default value exists set it to that if it is defined. filterValue = filterModel?.value === undefined && filterModel?.defaultValue !== undefined ? filterModel?.defaultValue : filterModel?.value; } return { ...filterModel, value: filterValue !== undefined ? filterValue : filterModel?.value }; }) ); const { addToast } = useBentoToastController(); /** * Container size layout. * Allows to change the layout from multi-line to one-line for each container size type: * - large: >=700 * - medium: >=430 & <700 * - small: <430 */ const { screenLayoutComponent, showSearchbarInsideContainer, isOneLineLayout } = useContainerSizeOptionsLayout( filterBarRef, toRef(props, 'containerSizeLayout') ); /** * Classes */ /** * Handles the classes of the swappable component ('div' or 'fixed-scroller) */ const containerConditionalClasses = computed(() => ({ 'b-filter-bar__fixed-scroller': isOneLineLayout.value, 'b-filter-bar__container': !isOneLineLayout.value, // Multi-line })); /** * Handles the classes of the container inside the swappable component. */ const contentConditionalClasses = computed(() => ({ 'b-filter-bar__scroller-content': isOneLineLayout.value, 'b-filter-bar__filters': !isOneLineLayout.value, // Multi-line })); /** * Search bar pops */ const searchBarAriaLabel = computed(() => props.search?.inputFieldAriaLabel || (t('searchBar') as string)); const searchBarProps = computed(() => { if (!props.search) { return undefined; } const { debounceTime, disabled, inputFieldAriaLabel, hint, placeholder } = props.search; return { debounceTime, disabled, inputFieldAriaLabel, hint, placeholder }; }); const hasHiddenFilters = computed(() => { // Has props with visible set const hasVisibleSet = valueAndFilterValuesPropsMerged.value.some( ({ visible }) => visible !== undefined && visible !== null ); return hasVisibleSet ? valueAndFilterValuesPropsMerged.value.some(({ visible }) => !visible) : false; }); // Persistent filters i.e. filters with default value const persistentFilters = computed(() => valueAndFilterValuesPropsMerged.value.filter(({ defaultValue }) => defaultValue !== undefined) ); // Show non-persistent & visible filters const regularFilters = computed(() => { const hasVisibleSet = valueAndFilterValuesPropsMerged.value.some( ({ visible }) => visible !== undefined && visible !== null ); // If no filter has the `visible` prop set then all are visible, otherwise search for the "visible: true" elements return hasVisibleSet ? valueAndFilterValuesPropsMerged.value.filter( ({ defaultValue, visible, value }) => // eslint-disable-next-line eqeqeq (defaultValue === undefined && visible) || (props.showAppliedHiddenFilters && value != undefined) ) : valueAndFilterValuesPropsMerged.value.filter( ({ defaultValue, value }) => // eslint-disable-next-line eqeqeq defaultValue === undefined || (props.showAppliedHiddenFilters && value != undefined) ); }); // Enable search bar. // Soft equality to check for "null" and "undefined" // eslint-disable-next-line eqeqeq const isSearchBarEnabled = computed(() => props.searchTerm != undefined); const hasActiveFilters = computed(() => valueAndFilterValuesPropsMerged.value .filter(({ defaultValue }) => defaultValue === undefined || defaultValue === null) // Disabling strict equality to check for both undefined and null // eslint-disable-next-line eqeqeq .some(({ value }) => value != undefined) ); // Show the "Clear all" button (true) if at least one filter is active (contains a value) // and there are not persistent filters present. const isClearAllVisible = computed(() => persistentFilters.value.length === 0 && hasActiveFilters.value); const isResetAllVisible = computed(() => { if (persistentFilters.value.length === 0) { return false; } const areAllValuesEqualToDefaults = valueAndFilterValuesPropsMerged.value .filter(({ defaultValue }) => defaultValue !== undefined || defaultValue !== null) .every(({ value, defaultValue }) => JSON.stringify(value) === JSON.stringify(defaultValue)); return !areAllValuesEqualToDefaults || hasActiveFilters.value; }); const updateAllFilters = (newFilters: BentoFilterBarModel) => { emit(BentoFilterBarEvent.INPUT, newFilters); emit( BentoFilterBarEvent.UPDATE_FILTER_VALUES_ARRAY, newFilters ?.filter(({ value }) => value !== null || value !== undefined) .map(({ field, value }) => ({ field, value })) ); emit( BentoFilterBarEvent.UPDATE_FILTER_VALUES_OBJECT, newFilters ?.filter(({ value }) => value !== null || value !== undefined) .reduce((acc, filter) => { acc[filter.field] = filter.value; return acc; }, {}) ); }; const updateFilterHandler = (updatedFilter: BentoFilterModel) => { const filterIndex = valueAndFilterValuesPropsMerged.value.findIndex( filter => filter.field === updatedFilter.field ); const newFilters = valueAndFilterValuesPropsMerged.value.slice(); newFilters[filterIndex].value = updatedFilter.value; updateAllFilters(newFilters); }; const clearAllFilters = () => { const previousFilters = valueAndFilterValuesPropsMerged.value.slice(); const clearedFilters = valueAndFilterValuesPropsMerged.value.map(({ value, ...otherFields }) => ({ ...otherFields, value: undefined, })); addToast({ text: t('filtersHaveBeenCleared') as string, action: { handler: () => updateAllFilters(previousFilters), text: t('undo') as string, }, }); updateAllFilters(clearedFilters); }; const resetAllFilters = () => { const previousFilters = valueAndFilterValuesPropsMerged.value.slice(); const resetFilters = valueAndFilterValuesPropsMerged.value.map(({ defaultValue, ...otherFields }) => ({ ...otherFields, defaultValue, value: defaultValue, })); addToast({ text: t('filtersHaveBeenReset') as string, action: { handler: () => updateAllFilters(previousFilters), text: t('undo') as string, }, }); updateAllFilters(resetFilters); }; const onSearchbarInput = (newSearchTerm: string) => { emit(BentoSearchBarEvent.INPUT, newSearchTerm); emit(BentoSearchBarEvent.UPDATE, newSearchTerm); }; const onSearchbarClear = () => { emit(BentoSearchBarEvent.CLEAR); }; // Expose methods for BentoDataGrid to access defineExpose({ isClearAllVisible, clearAllFilters, isResetAllVisible, resetAllFilters }); </script> <script lang="ts"> import { BentoBooleanFilter, BentoCheckboxGroupFilter, BentoDateFilter, BentoDateRangeFilter, BentoInputFilter, BentoInputWithDropdownFilter, BentoRadioGroupFilter, BentoRangeFilter, BentoSelectAvatarFilter, BentoSelectCountryFilter, BentoSelectCurrencyFilter, BentoSelectFilter, BentoSelectPaymentMethodFilter, BentoSelectTagFilter, BentoToggleFilter, } from './components'; import { BentoTeleport, FixedScroller } from '@/internal'; /** * The filter bar filters item lists and tables according to various filter criteria. * * @example * import { BentoFilterBar } from '@adyen/bento-vue2' * * export default { * components: { BentoFilterBar }, * data: () => ({ * filters: [{ * field: 'name', * label: 'Name', * value: null, * options: { ... }, * type: FilterType.TEXT, * }] * }), * template: ` * <bento-filter-bar * v-model="filter" * /> * ` * } */ export default { i18n: { messages }, name: 'bento-filter-bar', components: { BentoBooleanFilter, BentoCheckboxGroupFilter, BentoDateFilter, BentoDateRangeFilter, BentoInputFilter, BentoInputWithDropdownFilter, BentoRadioGroupFilter, BentoRangeFilter, BentoSelectAvatarFilter, BentoSelectCountryFilter, BentoSelectCurrencyFilter, BentoSelectFilter, BentoSelectPaymentMethodFilter, BentoSelectTagFilter, BentoTeleport, BentoToggleFilter, FixedScroller, }, }; </script> <style lang="scss" scoped src="./filter-bar.scss" />
|
|
1
|
+
<template> <div ref="filterBarRef" class="b-filter-bar"> <div v-if="!showSearchbarInsideContainer && isSearchBarEnabled" class="b-filter-bar__search-external-container"> <bento-search-bar class="b-filter-bar__search-bar" v-bind="searchBarProps" :input-field-aria-label="searchBarAriaLabel" :value="searchTerm" condensed @input="onSearchbarInput" @clear="onSearchbarClear" /> </div> <component :is="screenLayoutComponent" :class="containerConditionalClasses" condensed> <div :class="contentConditionalClasses"> <div v-if="showSearchbarInsideContainer && isSearchBarEnabled"> <bento-search-bar class="b-filter-bar__search-bar" v-bind="searchBarProps" :input-field-aria-label="searchBarAriaLabel" :value="searchTerm" condensed @input="onSearchbarInput" @clear="onSearchbarClear" /> </div> <template v-if="persistentFilters.length"> <component :is="filter.type" v-for="filter in persistentFilters" :key="filter.field" :field="filter.field" :label="filter.label" :disabled="filter?.disabled" :options="filter.options" :value="filter.value" :default-value="filter.defaultValue" @update="updateFilterHandler" /> </template> <component :is="filter.type" v-for="filter in regularFilters" :key="filter.field" :field="filter.field" :label="filter.label" :disabled="filter?.disabled" :options="filter.options" :value="filter.value" @update="updateFilterHandler" /> <!-- All filters button --> <all-filters-modal v-if="hasHiddenFilters" class="b-filter-bar__all-filters-modal" :value="valueAndFilterValuesPropsMerged" @input="updateAllFilters" /> <!-- Clear all filters button --> <filter-bar-button v-if="isClearAllVisible" class="b-filter-bar__clear-all" @click="clearAllFilters"> {{ t('clearFilters') }} <template #iconLeft> <refresh-icon aria-hidden="true" /> </template> </filter-bar-button> <!-- Reset all filters button --> <filter-bar-button v-if="isResetAllVisible" class="b-filter-bar__reset-all" @click="resetAllFilters"> {{ t('resetFilters') }} <template #iconLeft> <refresh-icon aria-hidden="true" /> </template> </filter-bar-button> </div> </component> </div> </template> <script setup lang="ts"> import { computed, ref, toRef } from 'vue'; import { FilterBarButton } from './components/filter-bar-button'; import { BentoFilterBarEvent, type BentoFilterBarModel, type BentoFilterBarProps, type BentoFilterModel, } from './filter-bar.types'; import BentoSearchBar from '@/components/search-bar/search-bar.vue'; import { BentoSearchBarEvent } from '@/components/search-bar/search-bar.types'; import { useBentoToastController } from '@/composables'; import { useI18n } from '@/utils/ts/i18n'; import { useContainerSizeOptionsLayout } from './composables/use-container-size-options-layout/use-container-size-options-layout'; import AllFiltersModal from './components/all-filters-modal/all-filters-modal.vue'; import RefreshIcon from '@adyen/ui-assets-icons-16/vue/refresh'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; // TODO: setup Vue3 generics in Bento 2.0.0 const props = withDefaults(defineProps<BentoFilterBarProps>(), { config: () => [], filterValues: () => [], containerSizeLayout: () => ({ medium: 'one-line', small: 'multi-line', }), search: undefined, searchTerm: undefined, value: () => [], showAppliedHiddenFilters: false, filterValuesObject: undefined, }); const emit = defineEmits([ BentoFilterBarEvent.INPUT, BentoFilterBarEvent.UPDATE_FILTER_VALUES_OBJECT, BentoFilterBarEvent.UPDATE_FILTER_VALUES_ARRAY, BentoSearchBarEvent.CLEAR, BentoSearchBarEvent.INPUT, BentoSearchBarEvent.UPDATE, ]); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const filterBarRef = ref<HTMLDivElement>(null); const valueAndFilterValuesPropsMerged = computed<BentoFilterBarModel>(() => (props.config?.length > 0 ? props.config : props.value)?.map(filterModel => { let filterValue = props.filterValues.find(({ field }) => field === filterModel.field)?.value; if (props.filterValuesObject && filterModel.field in props.filterValuesObject) { filterValue = props.filterValuesObject[filterModel.field]; } if (filterValue === undefined) { // If the filter is using the deprecated `config` value prop use that // But if that is also not set then check if a default value exists set it to that if it is defined. filterValue = filterModel?.value === undefined && filterModel?.defaultValue !== undefined ? filterModel?.defaultValue : filterModel?.value; } return { ...filterModel, value: filterValue !== undefined ? filterValue : filterModel?.value }; }) ); const { addToast } = useBentoToastController(); /** * Container size layout. * Allows to change the layout from multi-line to one-line for each container size type: * - large: >=700 * - medium: >=430 & <700 * - small: <430 */ const { screenLayoutComponent, showSearchbarInsideContainer, isOneLineLayout } = useContainerSizeOptionsLayout( filterBarRef, toRef(props, 'containerSizeLayout') ); /** * Classes */ /** * Handles the classes of the swappable component ('div' or 'fixed-scroller) */ const containerConditionalClasses = computed(() => ({ 'b-filter-bar__fixed-scroller': isOneLineLayout.value, 'b-filter-bar__container': !isOneLineLayout.value, // Multi-line })); /** * Handles the classes of the container inside the swappable component. */ const contentConditionalClasses = computed(() => ({ 'b-filter-bar__scroller-content': isOneLineLayout.value, 'b-filter-bar__filters': !isOneLineLayout.value, // Multi-line })); /** * Search bar pops */ const searchBarAriaLabel = computed(() => props.search?.inputFieldAriaLabel || (t('searchBar') as string)); const searchBarProps = computed(() => { if (!props.search) { return undefined; } const { debounceTime, disabled, inputFieldAriaLabel, hint, placeholder } = props.search; return { debounceTime, disabled, inputFieldAriaLabel, hint, placeholder }; }); const hasHiddenFilters = computed(() => { // Has props with visible set const hasVisibleSet = valueAndFilterValuesPropsMerged.value.some( ({ visible }) => visible !== undefined && visible !== null ); return hasVisibleSet ? valueAndFilterValuesPropsMerged.value.some(({ visible }) => !visible) : false; }); // Persistent filters i.e. filters with default value const persistentFilters = computed(() => valueAndFilterValuesPropsMerged.value.filter(({ defaultValue }) => defaultValue !== undefined) ); // Show non-persistent & visible filters const regularFilters = computed(() => { const hasVisibleSet = valueAndFilterValuesPropsMerged.value.some( ({ visible }) => visible !== undefined && visible !== null ); // If no filter has the `visible` prop set then all are visible, otherwise search for the "visible: true" elements return hasVisibleSet ? valueAndFilterValuesPropsMerged.value.filter( ({ defaultValue, visible, value }) => // eslint-disable-next-line eqeqeq (defaultValue === undefined && visible) || (props.showAppliedHiddenFilters && value != undefined) ) : valueAndFilterValuesPropsMerged.value.filter( ({ defaultValue, value }) => // eslint-disable-next-line eqeqeq defaultValue === undefined || (props.showAppliedHiddenFilters && value != undefined) ); }); // Enable search bar. // Soft equality to check for "null" and "undefined" // eslint-disable-next-line eqeqeq const isSearchBarEnabled = computed(() => props.searchTerm != undefined); const hasActiveFilters = computed(() => valueAndFilterValuesPropsMerged.value .filter(({ defaultValue }) => defaultValue === undefined || defaultValue === null) // Disabling strict equality to check for both undefined and null // eslint-disable-next-line eqeqeq .some(({ value }) => value != undefined) ); // Show the "Clear all" button (true) if at least one filter is active (contains a value) // and there are not persistent filters present. const isClearAllVisible = computed(() => persistentFilters.value.length === 0 && hasActiveFilters.value); const isResetAllVisible = computed(() => { if (persistentFilters.value.length === 0) { return false; } const areAllValuesEqualToDefaults = valueAndFilterValuesPropsMerged.value .filter(({ defaultValue }) => defaultValue !== undefined || defaultValue !== null) .every(({ value, defaultValue }) => JSON.stringify(value) === JSON.stringify(defaultValue)); return !areAllValuesEqualToDefaults || hasActiveFilters.value; }); const updateAllFilters = (newFilters: BentoFilterBarModel) => { emit(BentoFilterBarEvent.INPUT, newFilters); emit( BentoFilterBarEvent.UPDATE_FILTER_VALUES_ARRAY, newFilters ?.filter(({ value }) => value !== null || value !== undefined) .map(({ field, value }) => ({ field, value })) ); emit( BentoFilterBarEvent.UPDATE_FILTER_VALUES_OBJECT, newFilters ?.filter(({ value }) => value !== null || value !== undefined) .reduce((acc, filter) => { acc[filter.field] = filter.value; return acc; }, {}) ); }; const updateFilterHandler = (updatedFilter: BentoFilterModel) => { const filterIndex = valueAndFilterValuesPropsMerged.value.findIndex( filter => filter.field === updatedFilter.field ); const newFilters = valueAndFilterValuesPropsMerged.value.slice(); newFilters[filterIndex].value = updatedFilter.value; updateAllFilters(newFilters); }; const clearAllFilters = () => { const previousFilters = valueAndFilterValuesPropsMerged.value.slice(); const clearedFilters = valueAndFilterValuesPropsMerged.value.map(({ value, ...otherFields }) => ({ ...otherFields, value: undefined, })); addToast({ text: t('filtersHaveBeenCleared') as string, action: { handler: () => updateAllFilters(previousFilters), text: t('undo') as string, }, }); updateAllFilters(clearedFilters); }; const resetAllFilters = () => { const previousFilters = valueAndFilterValuesPropsMerged.value.slice(); const resetFilters = valueAndFilterValuesPropsMerged.value.map(({ defaultValue, ...otherFields }) => ({ ...otherFields, defaultValue, value: defaultValue, })); addToast({ text: t('filtersHaveBeenReset') as string, action: { handler: () => updateAllFilters(previousFilters), text: t('undo') as string, }, }); updateAllFilters(resetFilters); }; const onSearchbarInput = (newSearchTerm: string) => { emit(BentoSearchBarEvent.INPUT, newSearchTerm); emit(BentoSearchBarEvent.UPDATE, newSearchTerm); }; const onSearchbarClear = () => { emit(BentoSearchBarEvent.CLEAR); }; // Expose methods for BentoDataGrid to access defineExpose({ isClearAllVisible, clearAllFilters, isResetAllVisible, resetAllFilters }); </script> <script lang="ts"> import { BentoBooleanFilter, BentoCheckboxGroupFilter, BentoDateFilter, BentoDateRangeFilter, BentoInputFilter, BentoInputWithDropdownFilter, BentoRadioGroupFilter, BentoRangeFilter, BentoSelectAvatarFilter, BentoSelectCountryFilter, BentoSelectCurrencyFilter, BentoSelectFilter, BentoSelectPaymentMethodFilter, BentoSelectTagFilter, BentoToggleFilter, } from './components'; import { BentoTeleport, FixedScroller } from '@/internal'; /** * The filter bar filters item lists and tables according to various filter criteria. * * @example * import { BentoFilterBar } from '@adyen/bento-vue2' * * export default { * components: { BentoFilterBar }, * data: () => ({ * filters: [{ * field: 'name', * label: 'Name', * value: null, * options: { ... }, * type: FilterType.TEXT, * }] * }), * template: ` * <bento-filter-bar * v-model="filter" * /> * ` * } */ export default { i18n: { messages }, name: 'bento-filter-bar', components: { BentoBooleanFilter, BentoCheckboxGroupFilter, BentoDateFilter, BentoDateRangeFilter, BentoInputFilter, BentoInputWithDropdownFilter, BentoRadioGroupFilter, BentoRangeFilter, BentoSelectAvatarFilter, BentoSelectCountryFilter, BentoSelectCurrencyFilter, BentoSelectFilter, BentoSelectPaymentMethodFilter, BentoSelectTagFilter, BentoTeleport, BentoToggleFilter, FixedScroller, }, }; </script> <style lang="scss" scoped src="./filter-bar.scss" />
|
package/dist/assets/components/form-layout/components/form-layout-title/form-layout-title.vue
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div class="b-form-layout-title" :class="conditionalClasses"> <bento-typography :el="el" variant="title" :medium="isSection"> <slot /> </bento-typography> <bento-typography v-if="hasSlot('description')" class="b-form-layout-title__description" el="span" wide> <slot name="description" /> </bento-typography> </div> </template> <script setup lang="ts"> import { computed, useSlots } from 'vue'; import { BentoTypography, BentoTypographyElement } from '@/components/typography'; import { useHasSlot } from '@/composables'; const slots = useSlots(); const props = defineProps({ /** * Defines if the Form Layout title is * a "section" or a "subsection". * Default is "section" (false). Turn into "true" for "subsection" */ subsection: { type: Boolean, default: false, }, /** * Sets the HTML DOM element. * @values h1, h2, h3, h4, h5, h6, div, p, span */ el: { type: BentoTypography.props.el.type, required: true, validator: (value: BentoTypographyElement) => Object.values(BentoTypographyElement).includes(value), }, }); // Uses "medium" typography if it is a section title const isSection = computed(() => !props.subsection); const hasSlot = useHasSlot(slots); const conditionalClasses = computed(() => ({ 'b-form-layout-title--subsection': props.subsection, })); </script> <script lang="ts"> /** * If you’re building a lengthy form, organize related * fields into sections and, if needed, subsections. * * A form can have a "Section Title" and multiple "Subsection titles". * * One of the values provided in @see BentoTypographyElement must always be provided as a string * to define correct HTML sementic from the application's context * * @example * import { BentoFormLayoutTitle } from '@adyen/bento-vue2'; * * export default { * components: { BentoFormLayoutTitle }, * template: ` * <bento-form-layout-title subsection el="h2"> * Title * <template #description>Description text</template> * </bento-form-layout-title> * ` * } */ export default {}; </script> <style lang="scss" scoped src="./form-layout-title.scss" />
|
|
1
|
+
<template> <div class="b-form-layout-title" :class="conditionalClasses"> <bento-typography :el="el" variant="title" :medium="isSection"> <slot /> </bento-typography> <bento-typography v-if="hasSlot('description') || allFieldsRequired" class="b-form-layout-title__description" el="span" wide > <slot name="description" /> <template v-if="allFieldsRequired"> {{ hasSlot('description') ? ` ${t('requiredFields')}` : t('requiredFields') }} </template> </bento-typography> </div> </template> <script setup lang="ts"> import { computed, useSlots } from 'vue'; import { BentoTypography, BentoTypographyElement } from '@/components/typography'; import { useHasSlot } from '@/composables'; import { useI18n } from '@/utils/ts/i18n'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const slots = useSlots(); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const props = defineProps({ /** * Defines if the Form Layout title is * a "section" or a "subsection". * Default is "section" (false). Turn into "true" for "subsection" */ subsection: { type: Boolean, default: false, }, /** * Sets the HTML DOM element. * @values h1, h2, h3, h4, h5, h6, div, p, span */ el: { type: BentoTypography.props.el.type, required: true, validator: (value: BentoTypographyElement) => Object.values(BentoTypographyElement).includes(value), }, /** * Defines if the Form Layout title displays * a message indicating that all fields are required. */ allFieldsRequired: { type: Boolean, default: false, }, }); // Uses "medium" typography if it is a section title const isSection = computed(() => !props.subsection); const hasSlot = useHasSlot(slots); const conditionalClasses = computed(() => ({ 'b-form-layout-title--subsection': props.subsection, })); </script> <script lang="ts"> /** * If you’re building a lengthy form, organize related * fields into sections and, if needed, subsections. * * A form can have a "Section Title" and multiple "Subsection titles". * * One of the values provided in @see BentoTypographyElement must always be provided as a string * to define correct HTML sementic from the application's context * * @example * import { BentoFormLayoutTitle } from '@adyen/bento-vue2'; * * export default { * components: { BentoFormLayoutTitle }, * template: ` * <bento-form-layout-title subsection el="h2"> * Title * <template #description>Description text</template> * </bento-form-layout-title> * ` * } */ export default { name: 'bento-form-layout-title', i18n: { messages }, }; </script> <style lang="scss" scoped src="./form-layout-title.scss" />
|
|
@@ -51,6 +51,10 @@ directly, as spacing here is optimized for larger forms/configuration flows.
|
|
|
51
51
|
If you're building a lengthy form, organize related fields into sections and, if needed, subsections. This adds
|
|
52
52
|
additional context and makes filling it out easier.
|
|
53
53
|
|
|
54
|
+
If all fields in a section or subsection are required, use the `allFieldsRequired` prop on the `BentoFormLayoutTitle`
|
|
55
|
+
component to display a localized "All fields are required." instruction. If only some fields are required, mark those
|
|
56
|
+
individual components as `required` instead.
|
|
57
|
+
|
|
54
58
|
An element (`el`) of type string corresponding to one of the values contained in `BentoTypographyElement` must always be
|
|
55
59
|
provided to define correct HTML sementic from the application's context.
|
|
56
60
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <bento-typography class="b-input-field" data-testid="input-text-container" :class="inputFieldConditionalClasses" el="div" variant="body" > <field-label v-if="hasSlot('default') || label" :for="inputFieldId" data-testid="input-field-label" :tooltip-text="tooltipText" :optional="optional" :required="required" :label="label" @click="focusInput" > <slot></slot> </field-label> <div class="b-input-field__input-box" data-testid="input-box" :aria-disabled="disabled" @click="focusInput"> <!-- Optional Icon for Default text variant --> <span v-if="isDefaultVariant && (hasSlot('defaultIconBefore') || hasSlot('iconBefore'))" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__icon-before" > <slot v-if="hasSlot('iconBefore')" name="iconBefore" /> <slot v-if="hasSlot('defaultIconBefore') && !hasSlot('iconBefore')" name="defaultIconBefore" /> </span> <!-- Mandatory Icon for Payment Method variant --> <span v-if="isPaymentMethodVariant && hasSlot('paymentMethod')" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__payment-method" > <slot name="paymentMethod" /> </span> <!-- Dropdown at start --> <div v-if="shouldDisplayDropdownAtStart" :id="dropdownId" class="b-input-field__dropdown b-input-field__dropdown--start" @click.stop > <bento-dropdown v-if="dropdown" ref="inputDropdownElement" v-bind="inputFieldDropdownProps" :disabled="disabled" :readonly="isDropdownReadOnly" @update:model-value="onDropdownInput" /> </div> <!-- Static Value at start --> <bento-typography v-if="shouldDisplayStaticValueAtStart" :id="staticValueId" class="b-input-field__static-value b-input-field__static-value--start" el="span" variant="body" > <slot name="staticValue" /> </bento-typography> <div class="b-input-field__input-container"> <input :id="inputFieldId" v-bind="attrs" ref="inputFieldElement" class="b-input-field__input" :aria-label="computedAriaLabel" :aria-describedby="computedAriaDescribedBy" :aria-owns="computedAriaOwns" :aria-invalid="shouldShowError" :placeholder="placeholder" :required="required" :type="type" :value="modelValue ?? value" :disabled="disabled" :readonly="isReadOnly" @input="onInput" @change="emit('change')" @focus="onFocus" @blur="onBlur" @keydown="emit('keydown', $event)" @keyup="emit('keyup', $event)" @keydown.esc="emit('escape-pressed')" @click="emit('click', $event)" /> <!-- Hint at the end --> <bento-typography v-if="hint && isFocused && (modelValue || value)" el="span" class="b-input-field__hint" > {{ hint }} </bento-typography> </div> <span v-if="hasSlot('iconAfter')" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__icon-after" data-testid="input-text__icon-after" > <slot name="iconAfter" /> </span> <!-- Static Value at the end --> <bento-typography v-if="shouldDisplayStaticValueAtEnd" :id="staticValueId" class="b-input-field__static-value b-input-field__static-value--end" el="span" variant="body" > <slot name="staticValue" /> </bento-typography> <!-- Dropdown at end --> <div v-if="shouldDisplayDropdownAtEnd" :id="dropdownId" data-testid="input-field-dropdown-container-end" class="b-input-field__dropdown b-input-field__dropdown--end" @click.stop > <bento-dropdown v-if="dropdown" :id="dropdownId" ref="inputDropdownElement" v-bind="inputFieldDropdownProps" :disabled="disabled" :readonly="isDropdownReadOnly" @update:model-value="onDropdownInput" /> </div> </div> <error-message v-if="shouldShowError && !!errorMessage" :id="errorId" :error-message="errorMessage" class="b-input-field__error-message" /> <span v-if="hasSlot('description') || description" :id="descriptionId" class="b-input-field__description" @click="focusInput" > <bento-typography el="span" variant="body"> <slot id="description" name="description"> {{ description }} </slot> </bento-typography> </span> </bento-typography> </template> <script setup lang="ts"> import { computed, inject, provide, type Ref, ref, toRef, toRefs, useAttrs, useSlots } from 'vue'; // Components import { BentoDropdown } from '@/components/dropdown'; import { BentoTypography } from '@/components/typography'; import { ErrorMessage, FieldLabel } from '@/internal'; // Utils import { deprecate } from '@/utils/ts/deprecate'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; import { generateUid } from '@/core/utils/ts'; import { getSlotText } from '@/utils/ts/get-slot-text'; import { useAriaLabel } from '@/utils/ts/aria-label'; import { useI18n } from '@/utils/ts/i18n'; import { printDevelopmentWarning } from '@/utils/ts/print-development-warning'; import { INPUT_FIELD_COMPONENT_INJECTION_KEY, INPUT_FIELD_HINT_INJECTION_KEY, INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, } from '@/components/input-field/input-field.keys'; import { type BentoListboxSelectedValue } from '@/types/listbox'; // Composables import { useFormLayoutFieldLoading, useHasSlot } from '@/composables'; // Types import { type BentoInputDropdownProps, BentoInputFieldElementPosition, type BentoInputFieldProps, BentoInputFieldStateClass, BentoInputFieldType, type BentoInputFieldValue, BentoInputFieldVariant, } from './input-field.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const attrs = useAttrs(); const slots = useSlots(); const props = withDefaults(defineProps<BentoInputFieldProps>(), { ariaHidden: true, ariaLabel: undefined, condensed: false, description: '', disabled: false, dropdown: undefined, dropdownPosition: BentoInputFieldElementPosition.START, error: false, errorMessage: null, label: '', modelValue: undefined, optional: false, placeholder: '', required: false, readonly: false, slashedZero: false, staticValue: '', staticValuePosition: BentoInputFieldElementPosition.START, tooltipText: null, type: BentoInputFieldType.TEXT, value: undefined, variant: BentoInputFieldVariant.DEFAULT, }); const emit = defineEmits<{ /** * Emitted when the element has lost focus */ (e: 'blur'): void; /** * Emitted when the input is clicked */ (e: 'click', event: MouseEvent); /** * Emitted when the user modifies the element's value. Unlike the 'input' event, the change event is not necessarily fired for each alteration to an element's value */ (e: 'change'): void; /** * Emitted when the Input Field's internal dropdown changes it's value */ (e: 'dropdown-input', selectedValue: BentoListboxSelectedValue): void; /** * Emitted when the "ESC" key is pressed */ (e: 'escape-pressed'): void; /** * Emitted when the element has received focus */ (e: 'focus'): void; /** * Emitted when a key is pressed down */ (e: 'keydown', event: KeyboardEvent): void; /** * Emitted when a key is pressed and lifted up */ (e: 'keyup', event: KeyboardEvent): void; /** * Emitted when the component's model changes * @deprecated since version 2.0. Use `v-model` or `update:model-value` instead. */ (e: 'input', inputValue: string): void; /** * Emitted when the Input Field's internal dropdown changes it's value. Contains every dropdown prop passed with the updated selected value as 'value' */ (e: 'update:dropdown', updatedDropdownProps: BentoInputDropdownProps): void; /** * Emitted when the component's model changes */ (e: 'update:model-value', inputValue: BentoInputFieldValue): void; }>(); const { emitValue } = useFormFieldEmits<BentoInputFieldValue>(emit); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const hasSlot = useHasSlot(slots); const inputFieldElement = ref(null); const inputDropdownElement = ref(null); const inputFieldId = generateUid('input'); const dropdownId = generateUid('input-dropdown'); const staticValueId = generateUid('input-static-value'); const descriptionId = generateUid('input-description'); const errorId = generateUid('input-error'); const isFocused = ref(false); // Provide INPUT_FIELD_COMPONENT_INJECTION_KEY as true, to set the input only dropdown size variant in the BentoDropdown comp provide(INPUT_FIELD_COMPONENT_INJECTION_KEY, true); // Renders the input with a hint if a string is provided const hint = inject<Ref<string | undefined>>(INPUT_FIELD_HINT_INJECTION_KEY, ref(undefined)); const onFocus = () => { isFocused.value = true; emit('focus'); }; const onBlur = () => { isFocused.value = false; emit('blur'); }; const { isReadOnly } = useFormLayoutFieldLoading(toRef(props, 'readonly')); const isDropdownReadOnly = computed<BentoInputDropdownProps['readonly']>( () => isReadOnly.value || props.dropdown?.readonly ); const shouldShowStaticValue = computed( () => props.variant === BentoInputFieldVariant.STATIC_VALUE && !!slots.staticValue ); const isDefaultVariant = computed(() => props.variant === BentoInputFieldVariant.DEFAULT); const isPaymentMethodVariant = computed(() => props.variant === BentoInputFieldVariant.PAYMENT_METHOD); const shouldDisplayStaticValueAtStart = computed( () => shouldShowStaticValue.value && props.staticValuePosition === BentoInputFieldElementPosition.START ); const shouldDisplayStaticValueAtEnd = computed( () => shouldShowStaticValue.value && props.staticValuePosition === BentoInputFieldElementPosition.END ); const shouldShowDropdown = computed(() => props.variant === BentoInputFieldVariant.DROPDOWN && !!props.dropdown); const shouldDisplayDropdownAtStart = computed( () => shouldShowDropdown.value && props.dropdownPosition === BentoInputFieldElementPosition.START ); const shouldDisplayDropdownAtEnd = computed( () => shouldShowDropdown.value && props.dropdownPosition === BentoInputFieldElementPosition.END ); const shouldShowError = computed( () => !props.disabled && !isReadOnly.value && (!!props.errorMessage || props.error) ); const computedLabel = computed(() => (getSlotText(slots)('default') as string) || props.label); const computedDescription = computed(() => (getSlotText(slots)('description') as string) || props.description); const computedAriaOwns = computed(() => { if (shouldShowDropdown.value === true && shouldShowStaticValue.value === true) { return `${staticValueId} ${dropdownId}`; } if (shouldShowDropdown.value === true) { return `${dropdownId}`; } if (shouldShowStaticValue.value === true) { return `${staticValueId}`; } return null; }); // vue-i18n issue with t() used in script - direct usage in template works // requires either createI18n({ legacy: false }) or to be wrapped in computed() const computedAriaLabelFallbackMessage = computed(() => t('ariaLabelFallback')); const computedAriaLabelDropdowFallback = computed(() => t('ariaLabelDropdownFallback', { inputLabel: computedAriaLabel.value }) ); const ariaLabelAttribute = ref(attrs['aria-label']); const { ariaLabel } = toRefs(props); const computedAriaLabel = useAriaLabel({ ariaLabel: ariaLabelAttribute.value as string, // TODO: take this line away when the aria-label props is removed label: ariaLabel, defaultFallback: computedLabel.value || computedAriaLabelFallbackMessage, }); const computedAriaDescribedBy = computed( () => `${computedDescription.value ? descriptionId : ''} ${ shouldShowError.value && !!props.errorMessage ? errorId : '' }` ); const inputFieldConditionalClasses = computed(() => ({ [`b-input-field--${BentoInputFieldStateClass.CONDENSED}`]: props.condensed, [`b-input-field--${BentoInputFieldStateClass.DISABLED}`]: props.disabled, [`b-input-field--${BentoInputFieldStateClass.READONLY}`]: isReadOnly.value, [`b-input-field--${BentoInputFieldStateClass.ERROR}`]: shouldShowError.value, [`b-input-field--slashed-zero`]: props.slashedZero, })); const inputFieldDropdownProps = computed(() => ({ 'aria-label': computedAriaLabelDropdowFallback.value, ...props.dropdown, })); const onInput = (event: Event) => { const inputValue = (event.target as HTMLInputElement).value; if (!props.disabled && !isReadOnly.value) { emitValue(inputValue); } }; const onDropdownInput = (selectedValue: BentoListboxSelectedValue) => { emit('dropdown-input', selectedValue); emit('update:dropdown', { ...props?.dropdown, value: selectedValue }); }; const focusInput = () => { inputFieldElement.value.focus(); }; const focus = () => { if (props.variant === 'dropdown') { inputDropdownElement.value.focus(); } else { focusInput(); } }; // Expose method so that other components can focus on it in Vue3 defineExpose({ focusInput, focus, }); const inputFieldParentComponent = inject(INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, ''); if (!Object.values(BentoInputFieldType).includes(props.type as BentoInputFieldType) && !inputFieldParentComponent) { if (props.type === 'tel') { printDevelopmentWarning( `"bento-input-field" dropdown does not support the type '${props.type}', use "bento-input-field-phone-number" instead.` ); } else if (props.type === 'password') { printDevelopmentWarning( `"bento-input-field" dropdown does not support the type '${props.type}', use "bento-input-field-password" instead.` ); } else { printDevelopmentWarning(`"bento-input-field" dropdown does not support the type '${props.type}'`); } } /** * Deprecations */ if (props.error) { deprecate( 'BentoInputField "error" property', `Use the BentoInputField "errorMessage" property instead to indicate that there is a modifier error. Having the field as "undefined" will hide/remove the error. <bento-dropdown errorMessage="Error message text" />`, '2.0.0' ); } if (hasSlot('defaultIconBefore')) { deprecate( 'BentoInputField "defaultIconBefore" slot', `Use the BentoInputField "iconBefore" slot instead to add an icon at the left side of the input field <bento-input-field> \t<template #iconBefore> \t\t<my-icon-goes-here /> \t</template> </bento-input-field>`, '2.0.0' ); } if (props.value) { deprecate( 'BentoInputField "value" property', `The use of "value" prop in "BentoInputField" is no longer supported. Use the "v-model" or "model-value" property instead.`, '2.0.0' ); } </script> <script lang="ts"> /** * Input field component is used to create interactive controls for web-based * forms in order to accept data from the user. * * @example * import { BentoInputField } from '@adyen/bento-vue2' * * const inputValue = ref(''); * * export default { * components: { BentoInputField }, * tempate: ` * <bento-input-field * :model-value="inputValue" * :@update:model-value="newValue => inputValue.value = newValue" * > * } */ export default { name: 'bento-input-field', model: { prop: 'modelValue' }, i18n: { messages }, }; </script> <style lang="scss" scoped src="./input-field.scss" />
|
|
1
|
+
<template> <bento-typography class="b-input-field" data-testid="input-text-container" :class="inputFieldConditionalClasses" el="div" variant="body" > <field-label v-if="hasSlot('default') || label" :for="inputFieldId" data-testid="input-field-label" :tooltip-text="tooltipText" :optional="optional" :required="required" :label="label" @click="focusInput" > <slot></slot> </field-label> <div class="b-input-field__input-box" data-testid="input-box" :aria-disabled="disabled" @click="focusInput"> <!-- Optional Icon for Default text variant --> <span v-if="isDefaultVariant && (hasSlot('defaultIconBefore') || hasSlot('iconBefore'))" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__icon-before" > <slot v-if="hasSlot('iconBefore')" name="iconBefore" /> <slot v-if="hasSlot('defaultIconBefore') && !hasSlot('iconBefore')" name="defaultIconBefore" /> </span> <!-- Mandatory Icon for Payment Method variant --> <span v-if="isPaymentMethodVariant && hasSlot('paymentMethod')" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__payment-method" > <slot name="paymentMethod" /> </span> <!-- Dropdown at start --> <div v-if="shouldDisplayDropdownAtStart" :id="dropdownId" class="b-input-field__dropdown b-input-field__dropdown--start" @click.stop > <bento-dropdown v-if="dropdown" ref="inputDropdownElement" v-bind="inputFieldDropdownProps" :disabled="disabled" :readonly="isDropdownReadOnly" @update:model-value="onDropdownInput" /> </div> <!-- Static Value at start --> <bento-typography v-if="shouldDisplayStaticValueAtStart" :id="staticValueId" class="b-input-field__static-value b-input-field__static-value--start" el="span" variant="body" > <slot name="staticValue" /> </bento-typography> <div class="b-input-field__input-container"> <input :id="inputFieldId" v-bind="attrs" ref="inputFieldElement" class="b-input-field__input" :aria-label="computedAriaLabel" :aria-describedby="computedAriaDescribedBy" :aria-owns="computedAriaOwns" :aria-invalid="shouldShowError" :placeholder="placeholder" :required="required" :aria-required="required" :type="type" :value="modelValue ?? value" :disabled="disabled" :readonly="isReadOnly" @input="onInput" @change="emit('change')" @focus="onFocus" @blur="onBlur" @keydown="emit('keydown', $event)" @keyup="emit('keyup', $event)" @keydown.esc="emit('escape-pressed')" @click="emit('click', $event)" /> <!-- Hint at the end --> <bento-typography v-if="hint && isFocused && (modelValue || value)" el="span" class="b-input-field__hint" > {{ hint }} </bento-typography> </div> <span v-if="hasSlot('iconAfter')" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__icon-after" data-testid="input-text__icon-after" > <slot name="iconAfter" /> </span> <!-- Static Value at the end --> <bento-typography v-if="shouldDisplayStaticValueAtEnd" :id="staticValueId" class="b-input-field__static-value b-input-field__static-value--end" el="span" variant="body" > <slot name="staticValue" /> </bento-typography> <!-- Dropdown at end --> <div v-if="shouldDisplayDropdownAtEnd" :id="dropdownId" data-testid="input-field-dropdown-container-end" class="b-input-field__dropdown b-input-field__dropdown--end" @click.stop > <bento-dropdown v-if="dropdown" :id="dropdownId" ref="inputDropdownElement" v-bind="inputFieldDropdownProps" :disabled="disabled" :readonly="isDropdownReadOnly" @update:model-value="onDropdownInput" /> </div> </div> <error-message v-if="shouldShowError && !!errorMessage" :id="errorId" :error-message="errorMessage" class="b-input-field__error-message" /> <span v-if="hasSlot('description') || description" :id="descriptionId" class="b-input-field__description" @click="focusInput" > <bento-typography el="span" variant="body"> <slot id="description" name="description"> {{ description }} </slot> </bento-typography> </span> </bento-typography> </template> <script setup lang="ts"> import { computed, inject, provide, type Ref, ref, toRef, toRefs, useAttrs, useSlots } from 'vue'; // Components import { BentoDropdown } from '@/components/dropdown'; import { BentoTypography } from '@/components/typography'; import { ErrorMessage, FieldLabel } from '@/internal'; // Utils import { deprecate } from '@/utils/ts/deprecate'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; import { generateUid } from '@/core/utils/ts'; import { getSlotText } from '@/utils/ts/get-slot-text'; import { useAriaLabel } from '@/utils/ts/aria-label'; import { useI18n } from '@/utils/ts/i18n'; import { printDevelopmentWarning } from '@/utils/ts/print-development-warning'; import { INPUT_FIELD_COMPONENT_INJECTION_KEY, INPUT_FIELD_HINT_INJECTION_KEY, INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, } from '@/components/input-field/input-field.keys'; import { type BentoListboxSelectedValue } from '@/types/listbox'; // Composables import { useFormLayoutFieldLoading, useHasSlot } from '@/composables'; // Types import { type BentoInputDropdownProps, BentoInputFieldElementPosition, type BentoInputFieldProps, BentoInputFieldStateClass, BentoInputFieldType, type BentoInputFieldValue, BentoInputFieldVariant, } from './input-field.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const attrs = useAttrs(); const slots = useSlots(); const props = withDefaults(defineProps<BentoInputFieldProps>(), { ariaHidden: true, ariaLabel: undefined, condensed: false, description: '', disabled: false, dropdown: undefined, dropdownPosition: BentoInputFieldElementPosition.START, error: false, errorMessage: null, label: '', modelValue: undefined, optional: false, placeholder: '', required: false, readonly: false, slashedZero: false, staticValue: '', staticValuePosition: BentoInputFieldElementPosition.START, tooltipText: null, type: BentoInputFieldType.TEXT, value: undefined, variant: BentoInputFieldVariant.DEFAULT, }); const emit = defineEmits<{ /** * Emitted when the element has lost focus */ (e: 'blur'): void; /** * Emitted when the input is clicked */ (e: 'click', event: MouseEvent); /** * Emitted when the user modifies the element's value. Unlike the 'input' event, the change event is not necessarily fired for each alteration to an element's value */ (e: 'change'): void; /** * Emitted when the Input Field's internal dropdown changes it's value */ (e: 'dropdown-input', selectedValue: BentoListboxSelectedValue): void; /** * Emitted when the "ESC" key is pressed */ (e: 'escape-pressed'): void; /** * Emitted when the element has received focus */ (e: 'focus'): void; /** * Emitted when a key is pressed down */ (e: 'keydown', event: KeyboardEvent): void; /** * Emitted when a key is pressed and lifted up */ (e: 'keyup', event: KeyboardEvent): void; /** * Emitted when the component's model changes * @deprecated since version 2.0. Use `v-model` or `update:model-value` instead. */ (e: 'input', inputValue: string): void; /** * Emitted when the Input Field's internal dropdown changes it's value. Contains every dropdown prop passed with the updated selected value as 'value' */ (e: 'update:dropdown', updatedDropdownProps: BentoInputDropdownProps): void; /** * Emitted when the component's model changes */ (e: 'update:model-value', inputValue: BentoInputFieldValue): void; }>(); const { emitValue } = useFormFieldEmits<BentoInputFieldValue>(emit); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const hasSlot = useHasSlot(slots); const inputFieldElement = ref(null); const inputDropdownElement = ref(null); const inputFieldId = generateUid('input'); const dropdownId = generateUid('input-dropdown'); const staticValueId = generateUid('input-static-value'); const descriptionId = generateUid('input-description'); const errorId = generateUid('input-error'); const isFocused = ref(false); // Provide INPUT_FIELD_COMPONENT_INJECTION_KEY as true, to set the input only dropdown size variant in the BentoDropdown comp provide(INPUT_FIELD_COMPONENT_INJECTION_KEY, true); // Renders the input with a hint if a string is provided const hint = inject<Ref<string | undefined>>(INPUT_FIELD_HINT_INJECTION_KEY, ref(undefined)); const onFocus = () => { isFocused.value = true; emit('focus'); }; const onBlur = () => { isFocused.value = false; emit('blur'); }; const { isReadOnly } = useFormLayoutFieldLoading(toRef(props, 'readonly')); const isDropdownReadOnly = computed<BentoInputDropdownProps['readonly']>( () => isReadOnly.value || props.dropdown?.readonly ); const shouldShowStaticValue = computed( () => props.variant === BentoInputFieldVariant.STATIC_VALUE && !!slots.staticValue ); const isDefaultVariant = computed(() => props.variant === BentoInputFieldVariant.DEFAULT); const isPaymentMethodVariant = computed(() => props.variant === BentoInputFieldVariant.PAYMENT_METHOD); const shouldDisplayStaticValueAtStart = computed( () => shouldShowStaticValue.value && props.staticValuePosition === BentoInputFieldElementPosition.START ); const shouldDisplayStaticValueAtEnd = computed( () => shouldShowStaticValue.value && props.staticValuePosition === BentoInputFieldElementPosition.END ); const shouldShowDropdown = computed(() => props.variant === BentoInputFieldVariant.DROPDOWN && !!props.dropdown); const shouldDisplayDropdownAtStart = computed( () => shouldShowDropdown.value && props.dropdownPosition === BentoInputFieldElementPosition.START ); const shouldDisplayDropdownAtEnd = computed( () => shouldShowDropdown.value && props.dropdownPosition === BentoInputFieldElementPosition.END ); const shouldShowError = computed( () => !props.disabled && !isReadOnly.value && (!!props.errorMessage || props.error) ); const computedLabel = computed(() => (getSlotText(slots)('default') as string) || props.label); const computedDescription = computed(() => (getSlotText(slots)('description') as string) || props.description); const computedAriaOwns = computed(() => { if (shouldShowDropdown.value === true && shouldShowStaticValue.value === true) { return `${staticValueId} ${dropdownId}`; } if (shouldShowDropdown.value === true) { return `${dropdownId}`; } if (shouldShowStaticValue.value === true) { return `${staticValueId}`; } return null; }); // vue-i18n issue with t() used in script - direct usage in template works // requires either createI18n({ legacy: false }) or to be wrapped in computed() const computedAriaLabelFallbackMessage = computed(() => t('ariaLabelFallback')); const computedAriaLabelDropdowFallback = computed(() => t('ariaLabelDropdownFallback', { inputLabel: computedAriaLabel.value }) ); const ariaLabelAttribute = ref(attrs['aria-label']); const { ariaLabel } = toRefs(props); const computedAriaLabel = useAriaLabel({ ariaLabel: ariaLabelAttribute.value as string, // TODO: take this line away when the aria-label props is removed label: ariaLabel, defaultFallback: computedLabel.value || computedAriaLabelFallbackMessage, }); const computedAriaDescribedBy = computed( () => `${computedDescription.value ? descriptionId : ''} ${ shouldShowError.value && !!props.errorMessage ? errorId : '' }` ); const inputFieldConditionalClasses = computed(() => ({ [`b-input-field--${BentoInputFieldStateClass.CONDENSED}`]: props.condensed, [`b-input-field--${BentoInputFieldStateClass.DISABLED}`]: props.disabled, [`b-input-field--${BentoInputFieldStateClass.READONLY}`]: isReadOnly.value, [`b-input-field--${BentoInputFieldStateClass.ERROR}`]: shouldShowError.value, [`b-input-field--slashed-zero`]: props.slashedZero, })); const inputFieldDropdownProps = computed(() => ({ 'aria-label': computedAriaLabelDropdowFallback.value, ...props.dropdown, })); const onInput = (event: Event) => { const inputValue = (event.target as HTMLInputElement).value; if (!props.disabled && !isReadOnly.value) { emitValue(inputValue); } }; const onDropdownInput = (selectedValue: BentoListboxSelectedValue) => { emit('dropdown-input', selectedValue); emit('update:dropdown', { ...props?.dropdown, value: selectedValue }); }; const focusInput = () => { inputFieldElement.value.focus(); }; const focus = () => { if (props.variant === 'dropdown') { inputDropdownElement.value.focus(); } else { focusInput(); } }; // Expose method so that other components can focus on it in Vue3 defineExpose({ focusInput, focus, }); const inputFieldParentComponent = inject(INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, ''); if (!Object.values(BentoInputFieldType).includes(props.type as BentoInputFieldType) && !inputFieldParentComponent) { if (props.type === 'tel') { printDevelopmentWarning( `"bento-input-field" dropdown does not support the type '${props.type}', use "bento-input-field-phone-number" instead.` ); } else if (props.type === 'password') { printDevelopmentWarning( `"bento-input-field" dropdown does not support the type '${props.type}', use "bento-input-field-password" instead.` ); } else { printDevelopmentWarning(`"bento-input-field" dropdown does not support the type '${props.type}'`); } } /** * Deprecations */ if (props.error) { deprecate( 'BentoInputField "error" property', `Use the BentoInputField "errorMessage" property instead to indicate that there is a modifier error. Having the field as "undefined" will hide/remove the error. <bento-dropdown errorMessage="Error message text" />`, '2.0.0' ); } if (hasSlot('defaultIconBefore')) { deprecate( 'BentoInputField "defaultIconBefore" slot', `Use the BentoInputField "iconBefore" slot instead to add an icon at the left side of the input field <bento-input-field> \t<template #iconBefore> \t\t<my-icon-goes-here /> \t</template> </bento-input-field>`, '2.0.0' ); } if (props.value) { deprecate( 'BentoInputField "value" property', `The use of "value" prop in "BentoInputField" is no longer supported. Use the "v-model" or "model-value" property instead.`, '2.0.0' ); } </script> <script lang="ts"> /** * Input field component is used to create interactive controls for web-based * forms in order to accept data from the user. * * @example * import { BentoInputField } from '@adyen/bento-vue2' * * const inputValue = ref(''); * * export default { * components: { BentoInputField }, * tempate: ` * <bento-input-field * :model-value="inputValue" * :@update:model-value="newValue => inputValue.value = newValue" * > * } */ export default { name: 'bento-input-field', model: { prop: 'modelValue' }, i18n: { messages }, }; </script> <style lang="scss" scoped src="./input-field.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <bento-input-field v-bind="computedProps" ref="inputField" variant="dropdown" :dropdown="dropdown" class="b-input-field-phone-number" :model-value="displayValue" :placeholder="computedPlaceholder" type="tel" @blur="onBlur" @update:model-value="onUpdateModelValue" @keydown="onKeydown" @dropdown-input="onCountryChange" > <template v-if="label">{{ label }}</template> <template v-else><slot /></template> <template #description> <slot name="description">{{ description }}</slot> </template> </bento-input-field> </template> <script setup lang="ts"> import { computed, nextTick, provide, ref, watch } from 'vue'; import { type BentoInputDropdownProps, BentoInputField } from '@/components/input-field'; import { AsYouType, type CountryCode } from 'libphonenumber-js'; import { useI18n } from '@/utils/ts/i18n'; import { appendRegionCode, BentoInputFieldPhoneNumberCountries, getCountriesDropdownItems, getFormattedPhoneCode, getPhoneNumberPlaceholder, handleInputCursor, hasDefaultRegionCode, parsePhoneAndCountry, preventRegionCodeRepetition, stripRegionCode, } from './utils'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; import { deprecate } from '@/utils/ts/deprecate'; import type { BentoInputFieldPhoneNumberProps } from './input-field-phone-number.types'; import messages from './messages.json'; import { INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY } from '@/components/input-field/input-field.keys'; type MessageSchema = (typeof messages)['en-US']; const { t, locale } = useI18n<{ message: MessageSchema }>({ messages }); const props = withDefaults(defineProps<BentoInputFieldPhoneNumberProps>(), { countries: () => BentoInputFieldPhoneNumberCountries, description: undefined, dynamicFiltering: true, }); /** * Avoid passing `value` to `bento-input-field`. * This component uses `displayValue` instead. */ const computedProps = computed(() => { const { value, ...allProps } = props; return allProps; }); const emit = defineEmits<{ /** * Emitted when the element has lost focus */ (e: 'blur'): void; /** * Emitted when the value changes. The value is the unformatted phone number in E.164 format. * @deprecated from version 2.0.0. Use `v-model` or `update:model-value` instead. */ (e: 'input', value: string): void; /** * Emitted when the value changes. The value is the unformatted phone number in E.164 format. */ (e: 'update:model-value', value: string): void; /** * Emitted when the selected country is updated. */ (e: 'update:selected-country', country: CountryCode): void; /** * Emitted when the phone number's validity changes. * The payload is a boolean indicating whether the number is valid. */ (e: 'input:valid', value: boolean): void; }>(); const inputField = ref(null); const displayValue = ref(''); const internalValue = ref(''); const insertedChar = ref(''); const { emitValue } = useFormFieldEmits<string>(emit); provide(INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, 'bento-input-field-phone-number'); const computedPlaceholder = computed(() => { if (props.placeholder) { return props.placeholder; } if (props.selectedCountry) { return getPhoneNumberPlaceholder(props.selectedCountry); } return ''; }); const isDropdownReadonly = computed(() => props.dropdown?.readonly); const countryItems = computed(() => getCountriesDropdownItems(props.countries, locale.value)); const dropdown = computed<BentoInputDropdownProps>(() => ({ items: countryItems.value, modelValue: props.selectedCountry, readonly: isDropdownReadonly.value, dynamicFiltering: props.dynamicFiltering, 'aria-label': t('countrySelectorDropdown'), })); const onBlur = () => emit('blur'); const onUpdateModelValue = async (value: string) => { const inputEl = inputField.value.$refs.inputFieldElement; let formatter: AsYouType; // Check if the input value starts with '+' or '00', indicating it includes a country code. if (value.startsWith('+') || value.startsWith('00')) { // Reset displayValue to ensure changes are always reflected. // For example, if you paste '+1 264 123 4567' multiple times, the display value should always be '1234567'. // Without this, the displayValue will be '1234567' the first time but subsequently stay as '+1 264 123 4567'. displayValue.value = ''; // Normalize the value by replacing '00' with '+'. const normalizedValue = value.startsWith('00') ? `+${value.slice(2)}` : value; // Create a new AsYouType formatter to format the number as it's typed. formatter = new AsYouType(); formatter.input(normalizedValue); // Get the country code from the formatted number. const country = formatter.getCountry(); // Check if the extracted country code is valid and included in the allowed countries. const isValidCountry = country && props.countries.includes(country); // If the dropdown is readonly, determine whether to strip the region code based on the selected country. if (isDropdownReadonly.value) { displayValue.value = props.selectedCountry === country ? stripRegionCode(formatter.getNationalNumber(), country) : value; } else { // Emit an event to update the selected country, if valid. Otherwise, set it to null. emit('update:selected-country', isValidCountry ? country : null); // Strip the region code from the formatted number if the country is valid. displayValue.value = isValidCountry ? stripRegionCode(formatter.getNationalNumber(), country) : value; } const numberValue = formatter.getNumberValue() ?? ''; const formattedPhoneCode = getFormattedPhoneCode(country); const shouldClearInput = isValidCountry && numberValue.startsWith(formattedPhoneCode) && numberValue.length <= formattedPhoneCode.length; internalValue.value = shouldClearInput ? '' : numberValue; emitValue(internalValue.value); } else { // If the input doesn't start with a country code, format the number based on the selected country. formatter = new AsYouType(props.selectedCountry); const formatted = formatter.input(appendRegionCode(value, props.selectedCountry)); // Prevent repetition of the region code and get the unformatted number. const unformattedNumber = preventRegionCodeRepetition(value, formatter.getNumberValue(), props.selectedCountry) ?? ''; // Emit the unformatted number value and update the display value with the stripped region code. internalValue.value = unformattedNumber; emitValue(internalValue.value); displayValue.value = stripRegionCode(formatted, props.selectedCountry); } await nextTick(); // Re-parse the number to ensure correct formatting, e.g., adding a leading '0' for NL numbers if valid and `hasDefaultRegionCode` returns false. if (formatter.isValid() && props.selectedCountry && !hasDefaultRegionCode(props.selectedCountry)) { const { displayNumber, isValid } = parsePhoneAndCountry( displayValue.value?.toString(), props.selectedCountry, props.countries ); displayValue.value = displayNumber; emitValidation(isValid); } else { emitValidation(formatter.isValid()); } handleInputCursor(inputEl, displayValue.value.toString(), insertedChar.value); }; const emitValidation = (isValid: boolean) => { // Numbers shorter than 5 digits are never valid phone numbers. // `libphonenumber-js` rejects numbers under 5 digits (`TOO_SHORT`). // We don’t call that check directly here, but use the same value for consistency. const MIN_PHONE_NUMBER_LENGTH_FOR_VALIDATION = 5; const numberLength = displayValue.value?.toString().replace(/\D/g, '')?.length ?? 0; const isValidNumberLength = numberLength >= MIN_PHONE_NUMBER_LENGTH_FOR_VALIDATION; emit('input:valid', isValidNumberLength && isValid); }; const onKeydown = event => { if (/^[a-zA-Z]$/.test(event.key) && !event.metaKey && !event.ctrlKey) { event.preventDefault(); return; } insertedChar.value = event.key; }; const onCountryChange = (country: CountryCode) => { emit('update:selected-country', country); const isUnformatted = displayValue.value?.toString().startsWith('+'); if (hasDefaultRegionCode(country) && !isUnformatted) { let formatter = new AsYouType(country); const formatted = formatter.input(appendRegionCode(displayValue.value?.toString(), country)); const unformattedNumber = preventRegionCodeRepetition(displayValue.value?.toString(), formatter.getNumberValue(), country) ?? ''; internalValue.value = unformattedNumber; emitValue(internalValue.value); displayValue.value = stripRegionCode(formatted, country); emitValidation(formatter.isValid()); } else { const { displayNumber, unformattedNumber, isValid } = parsePhoneAndCountry( displayValue.value?.toString(), country, props.countries ); internalValue.value = unformattedNumber ?? ''; emitValue(internalValue.value); displayValue.value = displayNumber; emitValidation(isValid); } }; watch( () => [props.value, props.modelValue], () => { const newValue = props.modelValue ?? props.value; if (newValue === internalValue.value) { return; } const { displayNumber, country, isValid } = parsePhoneAndCountry( newValue?.toString(), props.selectedCountry, props.countries ); if (country && country !== props.selectedCountry && !isDropdownReadonly.value) { emit('update:selected-country', country); } displayValue.value = stripRegionCode(displayNumber, country); emitValidation(isValid); }, { immediate: true } ); if (props.value) { deprecate( 'BentoInputFieldPhoneNumber "value" property', `The use of "value" prop in "BentoInputFieldPhoneNumber" is no longer supported. Use the "v-model" or "model-value" property instead.`, '2.0.0' ); } </script> <script lang="ts"> /** * Input field phone number can be used to display phone numbers that come with a country code. * * @example * import { BentoInputFieldPhoneNumber } from '@adyen/bento-vue2'; * * export default { * components: { BentoInputFieldPhoneNumber }, * template: ` * <bento-input-field-phone-number * label="Phone number" * :countries="['NL', 'BE']" * model-value="0612345678" * selected-country="NL" * /> * ` * } */ export default { i18n: { messages }, model: { prop: 'modelValue' }, }; </script> <style lang="scss" scoped src="./input-field-phone-number.scss" />
|
|
1
|
+
<template> <bento-input-field v-bind="computedProps" ref="inputField" variant="dropdown" :dropdown="dropdown" class="b-input-field-phone-number" :model-value="displayValue" :placeholder="computedPlaceholder" type="tel" @blur="onBlur" @update:model-value="onUpdateModelValue" @keydown="onKeydown" @dropdown-input="onCountryChange" > <template v-if="label">{{ label }}</template> <template v-else><slot /></template> <template #description> <slot name="description">{{ description }}</slot> </template> </bento-input-field> </template> <script setup lang="ts"> import { computed, nextTick, provide, ref, watch } from 'vue'; import { type BentoInputDropdownProps, BentoInputField } from '@/components/input-field'; import { AsYouType, type CountryCode } from 'libphonenumber-js'; import { useI18n } from '@/utils/ts/i18n'; import { appendRegionCode, BentoInputFieldPhoneNumberCountries, getCountriesDropdownItems, getFormattedPhoneCode, getPhoneNumberPlaceholder, handleInputCursor, hasDefaultRegionCode, parsePhoneAndCountry, preventRegionCodeRepetition, stripRegionCode, } from './utils'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; import { deprecate } from '@/utils/ts/deprecate'; import type { BentoInputFieldPhoneNumberProps } from './input-field-phone-number.types'; import messages from './messages.json'; import { INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY } from '@/components/input-field/input-field.keys'; type MessageSchema = (typeof messages)['en-US']; const { t, locale } = useI18n<{ message: MessageSchema }>({ messages }); const props = withDefaults(defineProps<BentoInputFieldPhoneNumberProps>(), { countries: () => BentoInputFieldPhoneNumberCountries, description: undefined, dynamicFiltering: true, }); /** * Avoid passing `value` to `bento-input-field`. * This component uses `displayValue` instead. */ const computedProps = computed(() => { const { value, ...allProps } = props; return allProps; }); const emit = defineEmits<{ /** * Emitted when the element has lost focus */ (e: 'blur'): void; /** * Emitted when the value changes. The value is the unformatted phone number in E.164 format. * @deprecated from version 2.0.0. Use `v-model` or `update:model-value` instead. */ (e: 'input', value: string): void; /** * Emitted when the value changes. The value is the unformatted phone number in E.164 format. */ (e: 'update:model-value', value: string): void; /** * Emitted when the selected country is updated. */ (e: 'update:selected-country', country: CountryCode): void; /** * Emitted when the phone number's validity changes. * The payload is a boolean indicating whether the number is valid. */ (e: 'input:valid', value: boolean): void; }>(); const inputField = ref(null); const displayValue = ref(''); const internalValue = ref(''); const insertedChar = ref(''); const { emitValue } = useFormFieldEmits<string>(emit); provide(INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, 'bento-input-field-phone-number'); const computedPlaceholder = computed(() => { if (props.placeholder) { return props.placeholder; } if (props.selectedCountry) { return getPhoneNumberPlaceholder(props.selectedCountry); } return ''; }); const isDropdownReadonly = computed(() => props.dropdown?.readonly); const countryItems = computed(() => getCountriesDropdownItems(props.countries, locale.value)); const dropdown = computed<BentoInputDropdownProps>(() => ({ items: countryItems.value, modelValue: props.selectedCountry, readonly: isDropdownReadonly.value, dynamicFiltering: props.dynamicFiltering, 'aria-label': t('countrySelectorDropdown'), })); const onBlur = () => emit('blur'); const onUpdateModelValue = async (value: string) => { const inputEl = inputField.value.$refs.inputFieldElement; let formatter: AsYouType; // Check if the input value starts with '+' or '00', indicating it includes a country code. if (value.startsWith('+') || value.startsWith('00')) { // Reset displayValue to ensure changes are always reflected. // For example, if you paste '+1 264 123 4567' multiple times, the display value should always be '1234567'. // Without this, the displayValue will be '1234567' the first time but subsequently stay as '+1 264 123 4567'. displayValue.value = ''; // Normalize the value by replacing '00' with '+'. const normalizedValue = value.startsWith('00') ? `+${value.slice(2)}` : value; // Create a new AsYouType formatter to format the number as it's typed. formatter = new AsYouType(); formatter.input(normalizedValue); // Get the country code from the formatted number. const country = formatter.getCountry(); // Check if the extracted country code is valid and included in the allowed countries. const isValidCountry = country && props.countries.includes(country); // If the dropdown is readonly, determine whether to strip the region code based on the selected country. if (isDropdownReadonly.value) { displayValue.value = props.selectedCountry === country ? stripRegionCode(formatter.getNationalNumber(), country) : value; } else { // Emit an event to update the selected country, if valid. Otherwise, set it to null. emit('update:selected-country', isValidCountry ? country : null); // Strip the region code from the formatted number if the country is valid. displayValue.value = isValidCountry ? stripRegionCode(formatter.getNationalNumber(), country) : value; } const numberValue = formatter.getNumberValue() ?? ''; const formattedPhoneCode = getFormattedPhoneCode(country); const shouldClearInput = isValidCountry && numberValue.startsWith(formattedPhoneCode) && numberValue.length <= formattedPhoneCode.length; internalValue.value = shouldClearInput ? '' : numberValue; emitValue(internalValue.value); } else { // If the input doesn't start with a country code, format the number based on the selected country. formatter = new AsYouType(props.selectedCountry); const formatted = formatter.input(appendRegionCode(value, props.selectedCountry)); // Prevent repetition of the region code and get the unformatted number. const numberValue = formatter.getNumberValue(); const unformattedNumber = numberValue ? preventRegionCodeRepetition(value, numberValue, props.selectedCountry) : value; // Emit raw input when parsing fails i.e. number do not have country code // Emit the unformatted number value and update the display value with the stripped region code. internalValue.value = unformattedNumber; emitValue(internalValue.value); displayValue.value = stripRegionCode(formatted, props.selectedCountry); } await nextTick(); // Re-parse the number to ensure correct formatting, e.g., adding a leading '0' for NL numbers if valid and `hasDefaultRegionCode` returns false. if (formatter.isValid() && props.selectedCountry && !hasDefaultRegionCode(props.selectedCountry)) { const { displayNumber, isValid } = parsePhoneAndCountry( displayValue.value?.toString(), props.selectedCountry, props.countries ); displayValue.value = displayNumber; emitValidation(isValid); } else { emitValidation(formatter.isValid()); } handleInputCursor(inputEl, displayValue.value.toString(), insertedChar.value); }; const emitValidation = (isValid: boolean) => { // Numbers shorter than 5 digits are never valid phone numbers. // `libphonenumber-js` rejects numbers under 5 digits (`TOO_SHORT`). // We don’t call that check directly here, but use the same value for consistency. const MIN_PHONE_NUMBER_LENGTH_FOR_VALIDATION = 5; const numberLength = displayValue.value?.toString().replace(/\D/g, '')?.length ?? 0; const isValidNumberLength = numberLength >= MIN_PHONE_NUMBER_LENGTH_FOR_VALIDATION; emit('input:valid', isValidNumberLength && isValid); }; const onKeydown = event => { if (/^[a-zA-Z]$/.test(event.key) && !event.metaKey && !event.ctrlKey) { event.preventDefault(); return; } insertedChar.value = event.key; }; const onCountryChange = (country: CountryCode) => { emit('update:selected-country', country); const isUnformatted = displayValue.value?.toString().startsWith('+'); if (hasDefaultRegionCode(country) && !isUnformatted) { let formatter = new AsYouType(country); const formatted = formatter.input(appendRegionCode(displayValue.value?.toString(), country)); const unformattedNumber = preventRegionCodeRepetition(displayValue.value?.toString(), formatter.getNumberValue(), country) ?? ''; internalValue.value = unformattedNumber; emitValue(internalValue.value); displayValue.value = stripRegionCode(formatted, country); emitValidation(formatter.isValid()); } else { const { displayNumber, unformattedNumber, isValid } = parsePhoneAndCountry( displayValue.value?.toString(), country, props.countries ); internalValue.value = unformattedNumber ?? ''; emitValue(internalValue.value); displayValue.value = displayNumber; emitValidation(isValid); } }; watch( () => [props.value, props.modelValue], () => { const newValue = props.modelValue ?? props.value; if (newValue === internalValue.value) { return; } const { displayNumber, country, isValid } = parsePhoneAndCountry( newValue?.toString(), props.selectedCountry, props.countries ); if (country && country !== props.selectedCountry && !isDropdownReadonly.value) { emit('update:selected-country', country); } displayValue.value = stripRegionCode(displayNumber, country); emitValidation(isValid); }, { immediate: true } ); if (props.value) { deprecate( 'BentoInputFieldPhoneNumber "value" property', `The use of "value" prop in "BentoInputFieldPhoneNumber" is no longer supported. Use the "v-model" or "model-value" property instead.`, '2.0.0' ); } </script> <script lang="ts"> /** * Input field phone number can be used to display phone numbers that come with a country code. * * @example * import { BentoInputFieldPhoneNumber } from '@adyen/bento-vue2'; * * export default { * components: { BentoInputFieldPhoneNumber }, * template: ` * <bento-input-field-phone-number * label="Phone number" * :countries="['NL', 'BE']" * model-value="0612345678" * selected-country="NL" * /> * ` * } */ export default { i18n: { messages }, model: { prop: 'modelValue' }, }; </script> <style lang="scss" scoped src="./input-field-phone-number.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <fieldset class="b-controls-group" :class="conditionalClasses" :form="form" :disabled="disabled"
|
|
1
|
+
<template> <fieldset class="b-controls-group" v-bind="attrs" :class="conditionalClasses" :form="form" :disabled="disabled"> <legend class="b-controls-group__label-container" :class="legendConditionalClasses"> <field-label v-if="label" class="b-controls-group__label" :label="label" :tooltip-text="tooltipText" :optional="optional" :required="required" /> <bento-typography v-if="description || hasSlot('description')" class="b-controls-group__description" el="span" > <slot name="description"> {{ description }} </slot> </bento-typography> </legend> <slot /> </fieldset> </template> <script setup lang="ts"> import { computed, useAttrs, useSlots } from 'vue'; import { BentoTypography } from '@/components/typography'; import { FieldLabel } from '@/components/internal/field-label'; import { deprecate } from '@/utils/ts/deprecate'; import { useHasSlot } from '@/composables'; const attrs = useAttrs(); const slots = useSlots(); const hasSlot = useHasSlot(slots); const props = defineProps({ /** * Descriptive supporting text visbile underneath the label. */ description: { type: String, default: null }, /** * The name of the component to cite when throwing a depreacte error. */ componentName: { type: String, required: true }, /** * Sets all child controls of the <fieldset> as disabled. */ disabled: { type: Boolean, default: false }, /** * The value of the id attribute of a <form> element the <fieldset> is to part of, even if it is not inside the form itself. */ form: { type: String, default: null }, /** * Shows/hides the group label. */ hideLabel: { type: Boolean, default: null }, /** * The name associated with the group. Required for accessibility purposes. * The label is shown when `hideLabel` is set to `true` or if `description` is passed */ label: { type: String, required: true }, /** * Indicates the field is optional. */ optional: { type: Boolean, default: false }, /** * Indicates the field is required. */ required: { type: Boolean, default: false }, /** * Tooltip message. */ tooltipText: { type: String, default: null }, }); const conditionalClasses = computed(() => ({ 'b-controls-group--disabled': props.disabled, })); const legendConditionalClasses = computed(() => { const isLabelAndDescriptionPresent = props.label && (props.description || hasSlot('description')); const shouldShowLabel = isLabelAndDescriptionPresent || props.hideLabel === false; return { 'b-controls-group__label-container--hidden': !shouldShowLabel, }; }); // eslint-disable-next-line eqeqeq -- Needs to show deprecate message when hideLabel is not set and no description available const hasNoPropsDefined = !(props.description || hasSlot('description')) && props.hideLabel == undefined; if (hasNoPropsDefined) { deprecate( `${props.componentName}: The property \`label\` will always be rendered by default.`, `After version 2.0.0, the label will be shown by default. To continue hiding the label set \`:hide-label="true"\`. Up until version 2.0.0, to display the label set \`hide-label="false"\`. \`label\` will always be rendered when \`description\` is passed alongside with \`label\``, `2.0.0` ); } </script> <script lang="ts"> /** * A controls group components should be used whenever it is necessary to group a set of controls (i.e. radio buttons or checkboxes) * This component conforms to the official WAI-ARIA guidelines for grouping controls: https://www.w3.org/WAI/tutorials/forms/grouping/ * * NOTE: This component should not be used by itself but more implemented by other exported components * @example * import { ControlsGroup, BentoCheckbox, BentoRadioButton } from '@adyen/bento-vue2/'; * * export default { * components: { ControlsGroup, BentoRadioButton }, * template: ` * <controls-group * label="Your group label" * description="Description" * > * <bento-radio-button * disabled * value="inputRadio" * > * Label text * </bento-radio-button> * <bento-radio-button * value="inputRadio" * > * Another label text * </bento-radio-button> * </controls-group> * ` * }; */ export default {}; </script> <style lang="scss" scoped src="./controls-group.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <label class="b-field-label" :class="conditionalClasses"> <bento-typography el="span" stronger> <template v-if="label">{{ label }}</template> <template v-else><slot></slot></template> </bento-typography> <bento-typography v-if="!!tooltipText" el="span" class="b-field-label__tooltip-wrapper"> <bento-info-icon :tooltip-text="tooltipText" class="b-field-label__tooltip" /> </bento-typography> <bento-typography v-if="required" class="b-field-label__requirement" el="span"
|
|
1
|
+
<template> <label class="b-field-label" :class="conditionalClasses"> <bento-typography el="span" stronger> <template v-if="label">{{ label }}</template> <template v-else><slot></slot></template> </bento-typography> <bento-typography v-if="!!tooltipText" el="span" class="b-field-label__tooltip-wrapper"> <bento-info-icon :tooltip-text="tooltipText" class="b-field-label__tooltip" /> </bento-typography> <bento-typography v-if="required" class="b-field-label__requirement" el="span">*</bento-typography> <bento-typography v-else-if="optional" class="b-field-label__requirement" el="span" >({{ t('optional') }})</bento-typography > </label> </template> <script setup lang="ts"> import { BentoInfoIcon } from '@/components/info-icon'; import { BentoTypography } from '@/components/typography'; import { useI18n } from '@/utils/ts/i18n'; import messages from './messages.json'; import { computed } from 'vue'; type MessageSchema = (typeof messages)['en-US']; const props = defineProps({ /** * Renders the dropdown with a condensed style. */ condensed: { type: Boolean, default: false }, /** * Field label */ label: { type: String, default: null, }, /** * Sets the field as optional */ optional: { type: Boolean, default: false, }, /** * Sets the field as required */ required: { type: Boolean, default: false, }, /** * Adds a info icon next to the label to display the passed tooltip message. */ tooltipText: { type: String, default: null, }, }); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const conditionalClasses = computed(() => ({ 'b-field-label--condensed': props.condensed })); </script> <script lang="ts"> /** * Internal component to handle the labels for all field components * @example * import { FieldLabel } from '@/components/internal/field-label'; * * export default { * components: { FieldLabel }, * template: ` * <field-label label="Label" tooltip-text="Tooltip info" required /> * ` * } */ export default { i18n: { messages }, }; </script> <style lang="scss" scoped src="./field-label.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <dialog-page class="b-modal-fullscreen-page" :page-id="pageId" :previous-page="previousPage" always-show-border> <template #header> <div class="b-modal-fullscreen-page__header"> <div class="b-modal-fullscreen-page__header-section b-modal-fullscreen-page__header-section--title"> <bento-typography v-if="title" el="h2" variant="title" class="b-modal-fullscreen-page__title" :title="title" >{{ title }}</bento-typography > </div> <div v-if="hasSlot('header')" class="b-modal-fullscreen-page__header-section b-modal-fullscreen-page__header-section--slot" > <slot name="header" /> </div> <div class="b-modal-fullscreen-page__header-section b-modal-fullscreen-page__header-section--actions"> <bento-typography v-if="contextualLabel" class="b-modal-fullscreen-page__contextual-label" el="span" :title="contextualLabel" >{{ contextualLabel }}</bento-typography > <bento-button-actions v-if="actions" class="b-modal-fullscreen-page__actions" :actions="actions" /> <div class="b-modal-fullscreen-page__separator"></div> </div> </div> </template> <template #content> <
|
|
1
|
+
<template> <dialog-page class="b-modal-fullscreen-page" :page-id="pageId" :previous-page="previousPage" always-show-border> <template #header> <div class="b-modal-fullscreen-page__header"> <div class="b-modal-fullscreen-page__header-section b-modal-fullscreen-page__header-section--title"> <bento-typography v-if="title" :id="computedTitleId" el="h2" variant="title" class="b-modal-fullscreen-page__title" :title="title" >{{ title }}</bento-typography > </div> <div v-if="hasSlot('header')" class="b-modal-fullscreen-page__header-section b-modal-fullscreen-page__header-section--slot" > <slot name="header" /> </div> <div class="b-modal-fullscreen-page__header-section b-modal-fullscreen-page__header-section--actions"> <bento-typography v-if="contextualLabel" class="b-modal-fullscreen-page__contextual-label" el="span" :title="contextualLabel" >{{ contextualLabel }}</bento-typography > <bento-button-actions v-if="actions" class="b-modal-fullscreen-page__actions" :actions="actions" /> <div class="b-modal-fullscreen-page__separator"></div> <div class="b-modal-fullscreen-page__close-button"> <bento-button variant="tertiary" :aria-label="t('close')" @click="handleClose"> <template #iconLeft><cross-icon :aria-hidden="true" /></template> </bento-button> </div> </div> </div> </template> <template #content> <slot /> </template> </dialog-page> </template> <script setup lang="ts"> import { inject, onMounted, type PropType, useSlots } from 'vue'; import { BentoButton, BentoButtonActions, type BentoButtonActionsList } from '@/components/button'; import { BentoTypography } from '@/components/typography'; import { DialogPage } from '@/internal'; import { useHasSlot } from '@/composables'; import { useI18n } from '@/utils/ts/i18n'; import { MODAL_FULLSCREEN_CLOSE_INJECTION_KEY, MODAL_FULLSCREEN_TITLE_ID_INJECTION_KEY, } from '@/components/modal-fullscreen/modal-fullscreen.keys'; import CrossIcon from '@adyen/ui-assets-icons-16/vue/cross'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const { t } = useI18n<{ message: MessageSchema }>({ messages }); const props = defineProps({ /** * List of actions that will be used to render the buttons. * First element in the list will be rendered as "primary". * All elements after the first will be rendered as "secondary". * * Each object in the list should have a `title`, an `event` and * (optional) a Vue `icon` from the library `@adyen/ui-assets-icons-16`. * * @see BentoButton for a list of all the other props supported by each button action. */ actions: { type: Array as PropType<BentoButtonActionsList>, default: null, }, /** * Label shown next to the modal actions */ contextualLabel: { type: String, default: null, }, /** * Identifier of the page, used with activePage prop to handle sidepanel navigation */ pageId: { type: String, default: undefined, }, /** * ID of the page for the back button to navigate to. Only use it to overwrite the default back behavior * Used when the back button should not take the user to the natural previous page * Set to `null` to hide back button in the page */ previousPage: { type: String, default: undefined, }, /** * Title of the modal */ title: { type: String, default: null, }, /** * ID to be applied to the title to be used with aria-labelledby */ titleId: { type: String, default: undefined, }, }); const emit = defineEmits<{ /** * Emits event when close button is clicked */ (e: 'close'); }>(); const slots = useSlots(); const hasSlot = useHasSlot(slots); const injectedTitleId = inject(MODAL_FULLSCREEN_TITLE_ID_INJECTION_KEY, undefined); const computedTitleId = props.titleId ?? injectedTitleId; const closeModal = inject(MODAL_FULLSCREEN_CLOSE_INJECTION_KEY, undefined); const handleClose = () => { emit('close'); closeModal?.(); }; onMounted(() => { if (props.actions && props.actions.length > 2) { throw new Error('Not more than 2 action buttons may be provided in Bento Modal Fullscreen'); } }); </script> <script lang="ts"> /** * ModalFullscreenPage is used to render different pages of ModalFullscreen component * * @example * import { BentoModalFullscreen, BentoModalFullscreenPage } from '@adyen/bento-vue2'; * * export default { * components: { BentoModalFullscreen, BentoModalFullscreenPage }, * template: ` * <bento-modal-fullscreen active-page='page1' is-open='true'> * <bento-modal-fullscreen-page * title='Page 1' * pageId='page1' * contextual-label='Move to next page' * :actions="[ * { * title: 'Next page', * event: () => { * activePage.value = 'page2'; * }, * }]"> * Page 1 info * </bento-modal-fullscreen-page> * <bento-modal-fullscreen-page * title='Page 2' * pageId='page2' * :actions="[ * { * title: 'Save', * event: () => {}, * }]"> * Page 2 info * </bento-modal-fullscreen-page> * </bento-modal-fullscreen> * ` * } */ export default { i18n: { messages }, }; </script> <style lang="scss" scoped src="./modal-fullscreen-page.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <dialog :id="modalFullscreenId" ref="modal" class="b-modal-fullscreen" :class="conditionalClasses" data-dialog-element="modal-fullscreen" @animationend="closeModalAfterAnimation" @keydown.esc.prevent="closeModal" > <bento-focus-trap v-if="internalIsOpen" class="b-modal-fullscreen__wrapper"> <template v-if="props.activePage"> <slot /> </template> <bento-modal-fullscreen-page v-else :actions="actions" :contextual-label="contextualLabel" :title="title"> <template v-if="hasSlot('default')" #default> <slot name="default"></slot> </template> <template v-if="hasSlot('header')" #header> <slot name="header"></slot> </template> </bento-modal-fullscreen-page>
|
|
1
|
+
<template> <dialog :id="modalFullscreenId" ref="modal" class="b-modal-fullscreen" :class="conditionalClasses" data-dialog-element="modal-fullscreen" :aria-labelledby="titleId" @animationend="closeModalAfterAnimation" @keydown.esc.prevent="closeModal" > <bento-focus-trap v-if="internalIsOpen" class="b-modal-fullscreen__wrapper"> <template v-if="props.activePage"> <slot /> </template> <bento-modal-fullscreen-page v-else :actions="actions" :contextual-label="contextualLabel" :title="title" :title-id="titleId" @close="closeModal" > <template v-if="hasSlot('default')" #default> <slot name="default"></slot> </template> <template v-if="hasSlot('header')" #header> <slot name="header"></slot> </template> </bento-modal-fullscreen-page> </bento-focus-trap> <!-- Enable the toast inside the fullscreen-modal --> <template v-if="isOpen"> <bento-toast /> </template> </dialog> </template> <script setup lang="ts"> import { computed, onMounted, onUnmounted, type PropType, provide, ref, toRefs, useSlots, watch } from 'vue'; import { isVue2 } from 'vue-demi'; import { BentoToast } from '@/components/toast'; import { type BentoButtonActionsList } from '@/components/button'; import { BentoFocusTrap } from '@/components/focus-trap'; import BentoModalFullscreenPage from './components/modal-fullscreen-page.vue'; import { generateUid } from '@/core/utils/ts'; import { useHasSlot } from '@/composables'; import { useListeners } from '@/composables/use-listeners'; import { MODAL_FULLSCREEN_CLOSE_INJECTION_KEY, MODAL_FULLSCREEN_DIALOG_ID_INJECTION_KEY, MODAL_FULLSCREEN_TITLE_ID_INJECTION_KEY, } from '@/components/modal-fullscreen/modal-fullscreen.keys'; import { BentoDialogPageAnimation, type BentoDialogPageConfigData, type BentoDialogPageRegisterPage, DIALOG_PAGE_CONFIG_INJECTION_KEY, DIALOG_PAGE_REGISTER_PAGE_INJECTION_KEY, } from '@/internal'; const props = defineProps({ /** * List of actions that will be used to render the buttons. * First element in the list will be rendered as "primary". * All elements after the first will be rendered as "secondary". * * Each object in the list should have a `title`, an `event` and * (optional) a Vue `icon` from the library `@adyen/ui-assets-icons-16`. * * @see BentoButton for a list of all the other props supported by each button action. */ actions: { type: Array as PropType<BentoButtonActionsList>, default: null, }, /** * The id of the current active page * Should be the same as the pageId of the BentoModalFullscreenPage to be displayed */ activePage: { type: String, default: null, }, /** * Label shown next to the modal actions */ contextualLabel: { type: String, default: null, }, /** * Controls the modal's state from the outside. Set it to true to open the modal */ isOpen: { type: Boolean, default: null, }, /** * Title of the modal */ title: { type: String, default: null, }, }); const emit = defineEmits<{ /** * Handles closing the modal. The behavior changes depending on whether a `before-close` event listener is present. * * - **When listening for the `before-close` event:** * The `before-close` event is emitted immediately, skipping the animation. This allows for logic to run before the modal closes. * The `isOpen` prop must then be set to `false` to trigger the closing animation. * * - **When not listening for the `before-close` event:** * The closing animation plays, then `update:is-open` is emitted with `false`. * This is the default behavior, compatible with `v-model` or `.sync` on the `isOpen` prop. */ (e: 'before-close'): void; /** * Emits update event when page is changed internally */ (e: 'update:active-page', page: string): void; /** * Emits 'update:is-open' event when the modal is closed */ (e: 'update:is-open', isOpen: boolean): void; }>(); // Refs const { activePage, isOpen } = toRefs(props); const modal = ref(null); const isClosing = ref(false); const modalFullscreenId = generateUid('modal-fullscreen'); const titleId = generateUid('modal-fullscreen-title'); const internalIsOpen = ref(false); const isMounted = ref(false); const listeners = useListeners(); const slots = useSlots(); const hasSlot = useHasSlot(slots); // Animation styling const conditionalClasses = computed(() => ({ 'b-modal-fullscreen--hide': isClosing.value, })); // Handle closing and closing animation for dialog element const closeModalAfterAnimation = () => { if (isClosing.value && modal.value) { modal.value.close(); if (previousPages.value[0]) { emit('update:active-page', previousPages.value[0]); } previousPages.value = []; isClosing.value = false; internalIsOpen.value = false; emit('update:is-open', false); } }; /** * Emits the `beforeClose` event, if it is being listened to, * before closing the modal and omits the closing animation. * The animation will be triggered by the change of the `isOpen` prop instead. * * If no listener for the `before-close`, then the animation will be triggered, * which will close the modal and emit the `update:is-open` event instead, after it was closed. */ function closeModal() { /** * Vue3 appends the prefix "on" and changes to camelCase */ const eventName = isVue2 ? 'before-close' : 'onBeforeClose'; if (listeners[eventName]) { emit('before-close'); return; } startCloseModalAnimation(); } const startCloseModalAnimation = () => { if (!modal.value || !modal.value.open) { return; } isClosing.value = true; }; onMounted(() => { isMounted.value = true; }); onUnmounted(() => { isMounted.value = false; }); // Watching isOpen prop to toggle open/close the dialog element watch( [() => isOpen.value, modal], ([newIsOpen, newModalValue]) => { if (!isMounted.value || !newModalValue) { return; } if (newIsOpen) { internalIsOpen.value = true; if (!newModalValue.open) { newModalValue.showModal(); } if (!previousPages.value.length) { previousPages.value.push(activePage.value); } } else if (newModalValue.open) { startCloseModalAnimation(); } }, { immediate: true, flush: 'post' } ); // lets children components know they are inside a Modal (dialog element) provide(MODAL_FULLSCREEN_DIALOG_ID_INJECTION_KEY, modalFullscreenId); provide(MODAL_FULLSCREEN_TITLE_ID_INJECTION_KEY, titleId); provide(MODAL_FULLSCREEN_CLOSE_INJECTION_KEY, closeModal); // Pages const animation = ref(BentoDialogPageAnimation.TRANSITION_LEVEL_DEEPER); const pages = ref<Array<string>>([]); const previousPages = ref([]); const updatePage = page => emit('update:active-page', page); provide<BentoDialogPageConfigData>(DIALOG_PAGE_CONFIG_INJECTION_KEY, { activePage, animation, isOpen: internalIsOpen, pages, previousPages, updatePage, }); // unregister page if removed / destroyed const unregisterPage = (pageIdToRemove: string) => () => { pages.value = pages.value?.filter(pageId => pageId !== pageIdToRemove); }; // Provide data and function to register page to subcomponent const registerPage: BentoDialogPageRegisterPage = pageId => { if (pageId) { pages.value.push(pageId); } return { unregisterPage: unregisterPage(pageId) }; }; provide(DIALOG_PAGE_REGISTER_PAGE_INJECTION_KEY, registerPage); </script> <script lang="ts"> /** * Fullscreen modal takes full available space (no content underneath is visible) and focuses the user's attention exclusively on one complex task or piece of complex information. * * @example * import { BentoModalFullscreen } from '@adyen/bento-vue2'; * * export default { * components: { BentoModalFullscreen }, * template: ` * <bento-modal-fullscreen * :is-open="isOpen" * @update:is-open="isOpen = $event" * title='Fullscreen modal title' * contextual-label='Contextual label' * :actions="[{ title: 'Action', event: () => {}]" * > * <template #header>{{ header slot }}</template> * {{ content }} * </bento-modal-fullscreen> * ` * } */ export default {}; </script> <style lang="scss" scoped src="./modal-fullscreen.scss" />
|
package/dist/assets/components/pagination/components/pagination-context/pagination-context.vue
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div class="b-pagination-context"> <bento-typography v-if="totalPages > 1" el="div"> <component :is="i18nComponent" v-bind="getI18nProps('pageNumber')" tag="div"> <template #page> <bento-dropdown :id="dropdownId" :aria-label="t('pageNumber', { page: n(page) })" class="b-pagination-context__dropdown" condensed :items="listOfAvailablePages" :model-value="page" :virtual-scroll="virtualScroll" @update:model-value="onPageSelectChange" /> </template> </component> </bento-typography> <bento-typography v-else-if="
|
|
1
|
+
<template> <div class="b-pagination-context"> <bento-typography v-if="totalPages > 1 && !hidePageSelection" el="div"> <component :is="i18nComponent" v-bind="getI18nProps('pageNumber')" tag="div"> <template #page> <bento-dropdown :id="dropdownId" :aria-label="t('pageNumber', { page: n(page) })" class="b-pagination-context__dropdown" condensed :items="listOfAvailablePages" :model-value="page" :virtual-scroll="virtualScroll" @update:model-value="onPageSelectChange" /> </template> </component> </bento-typography> <bento-typography v-else-if="showPageNumberOfTotal" el="span"> {{ t('pageNumberOfTotal', { page: n(page), totalPages: n(totalPages) }) }} </bento-typography> <bento-typography v-else el="span"> {{ t('pageNumber', { page: n(page) }) }} </bento-typography> </div> </template> <script setup lang="ts"> import { computed } from 'vue'; import { BentoTypography } from '@/components/typography'; import { BentoDropdown } from '@/components/dropdown'; import { type BentoListboxOptionItem } from '@/types/listbox'; import { generateUid } from '@/core/utils/ts'; import messages from './messages.json'; import { getI18nComponent, getI18nProps, useI18n } from '@/utils/ts/i18n'; type MessageSchema = (typeof messages)['en-US']; const props = withDefaults( defineProps<{ /** * Hides the page selection dropdown and displays a static "Page X of Y" text instead. * Useful for very large datasets where rendering all page options would cause performance issues. */ hidePageSelection?: boolean; /** * The current page number of the pager. */ page?: number; /** * The total number of items the pager is paging through. */ totalPages?: number; /** * Enables virtual scrolling on all dropdowns if set to true. */ virtualScroll?: boolean; }>(), { hidePageSelection: false, page: 1, totalPages: null, virtualScroll: false, } ); const emit = defineEmits<{ (e: 'page-selected', value: string | number); }>(); const { t, n } = useI18n<{ message: MessageSchema }>({ messages }); const dropdownId = generateUid('bento-pagination-context-current-page'); const onPageSelectChange = (selectedPage: string | number) => emit('page-selected', selectedPage); const i18nComponent = getI18nComponent(); // Generate a list of pages from 1 to "totalPages" const transformIntoOption = (value: number): BentoListboxOptionItem => ({ label: n(value), value, }); const transformToPageNumber = (_, index) => index + 1; const listOfAvailablePages = computed(() => Array.from(new Array(props.totalPages), transformToPageNumber).map(transformIntoOption) ); const showPageNumberOfTotal = computed( () => props.totalPages === 1 || (props.hidePageSelection && props.page <= props.totalPages) ); </script> <script lang="ts"> export default { i18n: { messages }, }; </script> <style lang="scss" scoped src="./pagination-context.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div class="b-pagination-results-per-page"> <bento-typography el="div"> <component :is="i18nComponent" v-bind="getI18nProps(translationPathKey)" class="b-pagination-results-per-page__dropdown-container" tag="div" :plural="totalCount" > <template #itemCount> <bento-dropdown :id="dropdownId" :aria-label="t(translationPathKey, { itemCount: itemsPerPage, totalCount })" class="b-pagination-results-per-page__dropdown" condensed :items="predefinedDropdownOptions" :model-value="itemsPerPage" @update:model-value="onSelectChange" /> </template> <template v-if="!!totalCount" #totalCount>{{ totalCount }}</template> </component> </bento-typography> </div> </template> <script setup lang="ts"> import { computed } from 'vue'; import { BentoTypography } from '@/components/typography'; import { BentoDropdown } from '@/components/dropdown'; import { generateUid } from '@/core/utils/ts'; import { getI18nComponent, getI18nProps, useI18n } from '@/utils/ts/i18n'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const { t, n } = useI18n<{ message: MessageSchema }>({ messages }); const props = withDefaults( defineProps<{ /** * The amount of items the pager is paging through. */ itemsPerPage?: number; /** * The total number of items the pager is paging through. */ totalCount?: number; }>(), { itemsPerPage: 50, totalCount: null, } ); const emit = defineEmits<{ (e: 'select', value: number); }>(); const
|
|
1
|
+
<template> <div class="b-pagination-results-per-page"> <bento-typography el="div"> <component :is="i18nComponent" v-bind="getI18nProps(translationPathKey)" class="b-pagination-results-per-page__dropdown-container" tag="div" :plural="totalCount" > <template v-if="hidePageSizeSelection" #itemCount>{{ itemsPerPage }}</template> <template v-else #itemCount> <bento-dropdown :id="dropdownId" :aria-label="t(translationPathKey, { itemCount: itemsPerPage, totalCount })" class="b-pagination-results-per-page__dropdown" condensed :items="predefinedDropdownOptions" :model-value="itemsPerPage" @update:model-value="onSelectChange" /> </template> <template v-if="!!totalCount" #totalCount>{{ totalCount }}</template> </component> </bento-typography> </div> </template> <script setup lang="ts"> import { computed, watch } from 'vue'; import { BentoTypography } from '@/components/typography'; import { BentoDropdown } from '@/components/dropdown'; import { generateUid } from '@/core/utils/ts'; import { getI18nComponent, getI18nProps, useI18n } from '@/utils/ts/i18n'; import { printDevelopmentWarning } from '@/utils/ts/print-development-warning'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const { t, n } = useI18n<{ message: MessageSchema }>({ messages }); const props = withDefaults( defineProps<{ /** * Determines whether to hide the dropdown that allows users to change the number of results per page. * When set to `true`, the current `itemsPerPage` value is displayed as plain static text. */ hidePageSizeSelection?: boolean; /** * The amount of items the pager is paging through. */ itemsPerPage?: number; /** * The total number of items the pager is paging through. */ totalCount?: number; /** * The predefined options for the results per page dropdown. */ pageSizeItems?: Array<number>; }>(), { hidePageSizeSelection: false, itemsPerPage: 50, totalCount: null, pageSizeItems: () => [10, 20, 50, 75, 100], } ); const emit = defineEmits<{ (e: 'select', value: number); }>(); const isValid = computed( () => Array.isArray(props.pageSizeItems) && props.pageSizeItems.every(option => Number.isInteger(option)) ); watch( isValid, valid => { if (!valid) { printDevelopmentWarning('BentoPagination: pageSizeItems must be an array of integers.'); } }, { immediate: true } ); const dropdownId = generateUid('b-pagination-context-number-elements'); const predefinedDropdownOptions = computed(() => props.pageSizeItems.map(option => ({ label: n(option), value: option, })) ); const i18nComponent = getI18nComponent(); const onSelectChange = (selectedNumberOfResults: number) => { emit('select', selectedNumberOfResults); }; const translationPathKey = computed(() => (props.totalCount ? 'showingItemsWithTotal' : 'showingItems')); </script> <script lang="ts"> export default { i18n: { messages }, }; </script> <style lang="scss" scoped src="./pagination-results-per-page.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div class="b-pagination"> <nav :aria-label="computedAriaLabel" class="b-pagination__navigation" :class="navigationConditionalClasses()"> <bento-pagination-results-per-page v-if="!hidePageSize" class="b-pagination__results-per-page" :items-per-page="size" :total-count="totalCount" @select="onItemsPerPageChange" /> <bento-typography v-else-if="hasSlot('default')"><slot /></bento-typography> <div class="b-pagination__page-navigator"> <bento-pagination-context class="b-pagination__context" :total-pages="totalPages" :page="page" :
|
|
1
|
+
<template> <div class="b-pagination"> <nav :aria-label="computedAriaLabel" class="b-pagination__navigation" :class="navigationConditionalClasses()"> <bento-pagination-results-per-page v-if="!hidePageSize" class="b-pagination__results-per-page" :hide-page-size-selection="hidePageSizeSelection" :items-per-page="size" :total-count="totalCount" :page-size-items="pageSizeItems" @select="onItemsPerPageChange" /> <bento-typography v-else-if="hasSlot('default')"><slot /></bento-typography> <div class="b-pagination__page-navigator"> <bento-pagination-context class="b-pagination__context" :total-pages="totalPages" :page="page" :virtual-scroll="virtualScroll" :hide-page-selection="hidePageSelection" @page-selected="onPageSelected" /> <div class="b-pagination__divider"></div> <bento-pagination-controls class="b-pagination__controls" :has-next="hasNext" :page="page" :total-pages="totalPages" @navigate="navigate" /> </div> </nav> </div> </template> <script setup lang="ts"> import { computed, ref, toRefs, useAttrs, useSlots } from 'vue'; import { BentoTypography } from '@/components/typography'; import BentoPaginationResultsPerPage from './components/pagination-results-per-page/pagination-results-per-page.vue'; import BentoPaginationContext from './components/pagination-context/pagination-context.vue'; import BentoPaginationControls from './components/pagination-controls/pagination-controls.vue'; import { useAriaLabel } from '@/utils/ts/aria-label'; import { useHasSlot } from '@/composables'; import type { BentoPaginationProps } from './pagination.types'; const props = withDefaults(defineProps<BentoPaginationProps>(), { ariaLabel: null, hasNext: null, hidePageSize: false, hidePageSelection: false, hidePageSizeSelection: false, page: 1, pageSizeItems: undefined, size: 20, totalCount: null, virtualScroll: false, }); const emit = defineEmits<{ /** * Triggered when the navigation controls are clicked or the items per page is changed (i.e. when the arrow buttons to the * rightmost of the screen or either dropdown is updated). It indicates to which page it should go next and also the page size. */ (e: 'navigate', page: number, items?: number): void; /** * Triggered when the number of items per page is changed from the left hand dropdown. * It indicates the new number of items that will be displayed. */ (e: 'items-page', size: number): void; /** * Triggered when the navigation controls are clicked, whether the arrow buttons to the * right most of the screen or the dropdown on the right hand is updated. * It indicates to which page it should go next * (easily achieved using the [sync modifier](https://v2.vuejs.org/v2/guide/components-custom-events.html#sync-Modifier)) * e.g. `:page.sync="currentPage"` */ (e: 'update:page', page: number): void; /** * Triggered when the number of items per page is changed from the left hand dropdown. * It indicates the new number of items that will be displayed. * (easily achieved using the [sync modifier](https://v2.vuejs.org/v2/guide/components-custom-events.html#sync-Modifier)) * e.g. `:size.sync="itemsPerPage"` */ (e: 'update:size', size: number): void; }>(); const slots = useSlots(); const hasSlot = useHasSlot(slots); const attrs = useAttrs(); const ariaLabelAttribute = ref(attrs['aria-label']); const { ariaLabel } = toRefs(props); const computedAriaLabel = useAriaLabel({ ariaLabel: ariaLabelAttribute.value as string, // TODO: take this line away when the aria-label props is removed label: ariaLabel, }); const totalPages = computed(() => !props.totalCount || !props.size ? null : Math.ceil(props.totalCount / props.size) ); const navigate = (pageArg: number, itemsPerPage?: number) => { if (props.hidePageSize) { emit('navigate', pageArg); } else { emit('navigate', pageArg, itemsPerPage ?? props?.size); } emit('update:page', pageArg); }; const onPageSelected = (pageNumber: number) => { navigate(pageNumber); }; const onItemsPerPageChange = (elements: number) => { emit('items-page', elements); emit('update:size', elements); // Always go to first page when the // number of items page changes navigate(1, elements); }; const navigationConditionalClasses = () => ({ 'b-pagination__navigation--only-page-navigator': !hasSlot('default') && props.hidePageSize, }); </script> <script lang="ts"> /** * Paginiation component to help users page through data sets. * * There are two ways of doing navigation in this component: * 1. (Basic) You tell the component if there is a next page (hasNext) * 2. (Enhanced) You tell the component: * - How many total items exist (totalCount) * - The max number of items that you show/fetch per page (size) * - In this case you should not set the hasNext props * * If you have the data available and opt to use the Enhanced way you get extra controls: * - Page dropdown selector * - Last page button * - "Showing 10 of 200 items" text * * @usage * import { BentoPagination } from '@adyen/bento-vue2'; * * export default { * components: { BentoPagination }, * template: ` * <bento-pagination :has-next="isNextPage" /> * `, * } */ export default {}; </script> <style lang="scss" scoped src="./pagination.scss" />
|