@adyen/bento-mcp 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +38 -12
- package/dist/assets/components/anchor-scroller/anchor-scroller.vue +1 -1
- package/dist/assets/components/avatar/avatar.stories.ts +1 -1
- package/dist/assets/components/avatar/avatar.types.ts +1 -0
- package/dist/assets/components/avatar/avatar.vue +1 -1
- package/dist/assets/components/avatar/components/avatar-image/avatar-image.vue +1 -1
- package/dist/assets/components/dropdown/components/dropdown-base-textbox/dropdown-base-textbox.vue +1 -1
- package/dist/assets/components/dropdown/components/dropdown-default-textbox/dropdown-default-textbox.vue +1 -1
- package/dist/assets/components/dropdown/components/dropdown-options-container/dropdown-options-container.vue +1 -1
- package/dist/assets/components/dropdown/components/dropdown-small-textbox/dropdown-small-textbox.vue +1 -1
- package/dist/assets/components/dropdown/composables/use-keyboard-navigation.types.ts +1 -1
- package/dist/assets/components/dropdown/dropdown.vue +1 -1
- package/dist/assets/components/empty-state/empty-state.docs.mdx +61 -31
- package/dist/assets/components/empty-state/empty-state.types.ts +1 -1
- package/dist/assets/components/empty-state/empty-state.vue +1 -1
- package/dist/assets/components/input-field-password/input-field-password.vue +1 -1
- package/dist/assets/components/internal/listbox/components/listbox-option/listbox-option.vue +1 -1
- package/dist/assets/components/internal/listbox/components/listbox-single-select/listbox-single-select.vue +1 -1
- package/dist/assets/components/internal/listbox/components/listbox-single-select-option/listbox-single-select-option.vue +1 -1
- package/dist/assets/components/internal/listbox/listbox.vue +1 -1
- package/dist/assets/components/pagination/components/pagination-controls/pagination-controls.vue +1 -1
- package/dist/assets/components/pagination/pagination.docs.mdx +16 -10
- package/dist/assets/components/pagination/pagination.types.ts +1 -1
- package/dist/assets/components/pagination/pagination.vue +1 -1
- package/dist/assets/components/table-of-contents/table-of-contents.vue +1 -1
- package/dist/assets/components/typography/typography.docs.mdx +26 -14
- package/dist/assets/components/typography/typography.types.ts +1 -1
- package/dist/assets/components/typography/typography.vue +1 -1
- package/dist/assets/deprecations.md +173 -173
- package/dist/assets/usage.json +73 -73
- package/dist/assets/variables.css +3 -2
- package/dist/main.js +2 -2
- package/package.json +2 -2
- package/dist/assets/components/avatar/components/avatar-image/avatar-image.types.ts +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div ref="rootRef" class="b-empty-state"> <!-- illustration --> <img v-if="image && imageVariant" alt="" class="b-empty-state__image" :class="imageConditionalClasses" :src="imageSvg" /> <!-- heading --> <bento-typography v-bind="headingProps" class="b-empty-state__title"> <slot name="title"> {{ title }} </slot> </bento-typography> <!-- details --> <bento-typography v-if="variant !== 'condensed'" variant="body" class="b-empty-state__details"> <slot> {{ description }} </slot> </bento-typography> <!-- action --> <bento-button v-if="action" type="button" :variant="actionVariant" @click="action.event"> <template v-if="action.icon" #iconLeft> <component :is="action.icon" :svg-title="action.title"></component> </template> {{ action.title }} </bento-button> </div> </template> <script setup lang="ts"> import { computed, ref, watch } from 'vue'; import { BentoButton, type BentoButtonVariant } from '@/components/button'; import { BentoTypography, type BentoTypographyElement, type BentoTypographyVariant } from '@/components/typography'; import { observeSizeOfElement } from '@/utils/ts/resize'; import { BentoEmptyStateImageVariant, type BentoEmptyStateProps, BentoEmptyStateVariant, } from './empty-state.types'; const WIDTH_BREAKPOINT = 740; const props = withDefaults(defineProps<BentoEmptyStateProps>(), { action: null, description: null, headingEl: null, image: null, title: '', variant: 'basic', }); const rootRef = ref<HTMLDivElement>(null); const containerWidth = ref(0); const isBelowBreakpoint = computed(() => containerWidth.value < WIDTH_BREAKPOINT); const imageVariant = computed<BentoEmptyStateImageVariant>(() => { switch (props.variant) { case BentoEmptyStateVariant.FULL_PAGE: return isBelowBreakpoint.value ? BentoEmptyStateImageVariant.SMALL : BentoEmptyStateImageVariant.LARGE; case BentoEmptyStateVariant.EMBEDDED: return isBelowBreakpoint.value ? null : BentoEmptyStateImageVariant.SMALL; default: return null; } }); const imageSvg = ref(''); watch( [() => props.image, imageVariant], async () => { if (props.image && imageVariant.value) { // rollup only supports one variable in a dynamic path /* v8 ignore next 4 */ if (imageVariant.value === 'small') { imageSvg.value = (await import(`./assets/small/${props.image}.svg`)).default; } else { imageSvg.value = (await import(`./assets/large/${props.image}.svg`)).default; } } }, { immediate: true } ); const imageConditionalClasses = computed(() => ({ 'b-empty-state__image--small': imageVariant.value === 'small', 'b-empty-state__image--large': imageVariant.value === 'large', })); const HEADING_ELEMENT_MAP = { [BentoEmptyStateVariant.FULL_PAGE]: 'h2', [BentoEmptyStateVariant.EMBEDDED]: 'h3', [BentoEmptyStateVariant.BASIC]: 'h3', [BentoEmptyStateVariant.CONDENSED]: 'div', }; const headingElement = computed( () => (props.headingEl || HEADING_ELEMENT_MAP[props.variant]) as BentoTypographyElement ); const headingProps = computed(() => ({ el: headingElement.value, medium: props.variant !== BentoEmptyStateVariant.CONDENSED, strongest: props.variant === BentoEmptyStateVariant.CONDENSED, variant: (props.variant === BentoEmptyStateVariant.CONDENSED ? 'body' : 'title') as BentoTypographyVariant, })); const actionVariant = computed<`${BentoButtonVariant}`>(() => { switch (props.variant) { case BentoEmptyStateVariant.FULL_PAGE: return 'primary'; case BentoEmptyStateVariant.CONDENSED: return 'tertiary'; default: return 'secondary'; } }); // Lifecycle watch(rootRef, () => { // Use ResizeObserver so that image is only fetched if needed (visible). observeSizeOfElement(rootRef.value, () => { if (rootRef.value) { containerWidth.value = rootRef.value.offsetWidth; } }); }); </script> <script lang="ts"> /** * Empty states are moments in the user experience when there is nothing to display. * This component can be used to provide: * - Information about system status * - Contextual learning cues * - Direct pathways for key tasks * * @example * import { BentoEmptyState } from '@adyen/bento-vue2'; * * export default { * components: { BentoEmptyState }, * template: ` * <bento-empty-state * title="No results were found" * image="no-results-found" * variant="full-page" * :action="{ title: 'Reset filters', event: () => {} }" * > * Try a different term or reset search filters * </bento-empty-state> * ` * } */ export default {}; </script> <style lang="scss" scoped src="./empty-state.scss" />
|
|
1
|
+
<template> <div ref="rootRef" class="b-empty-state"> <!-- illustration --> <img v-if="image && imageVariant" alt="" class="b-empty-state__image" :class="imageConditionalClasses" :src="imageSvg" /> <!-- heading --> <bento-typography v-bind="headingProps" class="b-empty-state__title"> <slot name="title"> {{ title }} </slot> </bento-typography> <!-- details --> <bento-typography v-if="variant !== 'condensed'" variant="body" class="b-empty-state__details"> <slot> {{ description }} </slot> </bento-typography> <!-- action --> <bento-menu v-if="action && action.data" :data="action.data" :button="{ variant: actionVariant }" :close-menu-on-item-select="action.closeMenuOnItemSelect" :menu-position="action.menuPosition" :menu-fixed-positioning="action.menuFixedPositioning" :menu-width="action.menuWidth" :teleport="action.teleport" > <template v-if="action.icon" #iconLeft> <component :is="action.icon" :svg-title="action.title"></component> </template> {{ action.title }} </bento-menu> <bento-button v-else-if="action" type="button" :variant="actionVariant" @click="action.event"> <template v-if="action.icon" #iconLeft> <component :is="action.icon" :svg-title="action.title"></component> </template> {{ action.title }} </bento-button> </div> </template> <script setup lang="ts"> import { computed, ref, watch } from 'vue'; import { BentoButton, type BentoButtonVariant } from '@/components/button'; import { BentoMenu } from '@/components/menu'; import { BentoTypography, type BentoTypographyElement, type BentoTypographyVariant } from '@/components/typography'; import { observeSizeOfElement } from '@/utils/ts/resize'; import { BentoEmptyStateImageVariant, type BentoEmptyStateProps, BentoEmptyStateVariant, } from './empty-state.types'; const WIDTH_BREAKPOINT = 740; const props = withDefaults(defineProps<BentoEmptyStateProps>(), { action: null, description: null, headingEl: null, image: null, title: '', variant: 'basic', }); const rootRef = ref<HTMLDivElement>(null); const containerWidth = ref(0); const isBelowBreakpoint = computed(() => containerWidth.value < WIDTH_BREAKPOINT); const imageVariant = computed<BentoEmptyStateImageVariant>(() => { switch (props.variant) { case BentoEmptyStateVariant.FULL_PAGE: return isBelowBreakpoint.value ? BentoEmptyStateImageVariant.SMALL : BentoEmptyStateImageVariant.LARGE; case BentoEmptyStateVariant.EMBEDDED: return isBelowBreakpoint.value ? null : BentoEmptyStateImageVariant.SMALL; default: return null; } }); const imageSvg = ref(''); watch( [() => props.image, imageVariant], async () => { if (props.image && imageVariant.value) { // rollup only supports one variable in a dynamic path /* v8 ignore next 4 */ if (imageVariant.value === 'small') { imageSvg.value = (await import(`./assets/small/${props.image}.svg`)).default; } else { imageSvg.value = (await import(`./assets/large/${props.image}.svg`)).default; } } }, { immediate: true } ); const imageConditionalClasses = computed(() => ({ 'b-empty-state__image--small': imageVariant.value === 'small', 'b-empty-state__image--large': imageVariant.value === 'large', })); const HEADING_ELEMENT_MAP = { [BentoEmptyStateVariant.FULL_PAGE]: 'h2', [BentoEmptyStateVariant.EMBEDDED]: 'h3', [BentoEmptyStateVariant.BASIC]: 'h3', [BentoEmptyStateVariant.CONDENSED]: 'div', }; const headingElement = computed( () => (props.headingEl || HEADING_ELEMENT_MAP[props.variant]) as BentoTypographyElement ); const headingProps = computed(() => ({ el: headingElement.value, medium: props.variant !== BentoEmptyStateVariant.CONDENSED, strongest: props.variant === BentoEmptyStateVariant.CONDENSED, variant: (props.variant === BentoEmptyStateVariant.CONDENSED ? 'body' : 'title') as BentoTypographyVariant, })); const actionVariant = computed<`${BentoButtonVariant}`>(() => { switch (props.variant) { case BentoEmptyStateVariant.FULL_PAGE: return 'primary'; case BentoEmptyStateVariant.CONDENSED: return 'tertiary'; default: return 'secondary'; } }); // Lifecycle watch(rootRef, () => { // Use ResizeObserver so that image is only fetched if needed (visible). observeSizeOfElement(rootRef.value, () => { if (rootRef.value) { containerWidth.value = rootRef.value.offsetWidth; } }); }); </script> <script lang="ts"> /** * Empty states are moments in the user experience when there is nothing to display. * This component can be used to provide: * - Information about system status * - Contextual learning cues * - Direct pathways for key tasks * * @example * import { BentoEmptyState } from '@adyen/bento-vue2'; * * export default { * components: { BentoEmptyState }, * template: ` * <bento-empty-state * title="No results were found" * image="no-results-found" * variant="full-page" * :action="{ title: 'Reset filters', event: () => {} }" * > * Try a different term or reset search filters * </bento-empty-state> * ` * } */ export default {}; </script> <style lang="scss" scoped src="./empty-state.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div ref="inputFieldPasswordRef"> <bento-input-field ref="inputFieldPasswordInputRef" v-bind="inputFieldArgs" class="b-input-field-password__input" autocapitalize="none" autocorrect="off" spellcheck="false" @blur="onBlur" @focus="onFocus" @update:model-value="onUpdateModelValue" @keydown="checkCapsLock" @keyup="checkCapsLock" > <template v-if="label">{{ label }}</template> <template v-else><slot /></template> <template v-if="description" #description> <slot name="description">{{ description }}</slot> </template> <template v-if="isCapsLockOn" #iconBefore> <div class="b-input-field-password__caps-lock-indicator" @mouseenter="handleCapsLockMouseEnter" @mouseleave="handleCapsLockMouseLeave" > <bento-image ref="inputFieldPasswordCapsLockIconRef" :src="CapsLockIcon" :alt="t('capsLockEnabled')" /> </div> <tooltip v-if="inputFieldPasswordCapsLockIconRef" :class="tooltipConditionalClass" :content="t('capsLockEnabled')" :disabled-focus-trap="true" :fallback-position="['top', 'bottom']" :is-shown="isTooltipDisplayed" :target-element="inputFieldPasswordCapsLockIconRef" /> </template> <template v-if="!disabled" #iconAfter> <bento-button variant="tertiary" @click="toggleInputType"> <template #iconLeft> <show-icon v-if="!isPasswordReadable" :svg-title="t('showPassword')" /> <hide-icon v-if="isPasswordReadable" :svg-title="t('hidePassword')" /> </template> </bento-button> </template> </bento-input-field> <bento-popover v-if="hasValidations" :open="isHintDisplayed" :target-element="inputFieldPasswordInputRef" :disable-focus-trap="true" :aria-label="computedValidations.title" :aria-describedby="validationListId" position="right-start" > <div :id="validationPopoverId"> <bento-typography v-if="computedValidations.title" class="b-input-field-password__validation-title"> {{ computedValidations.title }} </bento-typography> <p v-if="!hasErrors" class="b-input-field-password__validation-status--valid" role="alert"> {{ t('validationValid') }} </p> <ul :id="validationListId" class="b-input-field-password__validation-list"> <li v-for="(rule, index) in computedValidationList" :key="index" :class="passwordValidityConditionalClass[index]" aria-live="polite" aria-atomic="true" > <span class="b-input-field-password__validation-list-icon"> <checkmark-circle-fill-icon v-if="passwordValidity[index]" :svg-title="t('validationChecked')" /> <dot-icon v-else :svg-title="t('validationUnchecked')" /> </span> <bento-typography class="b-input-field-password__validation-list-text" el="span"> {{ rule.label }} </bento-typography> </li> </ul> <bento-typography v-if="computedValidations.suggestion" class="b-input-field-password__validation-suggestion" > {{ computedValidations.suggestion }} </bento-typography> </div> </bento-popover> </div> </template> <script setup lang="ts"> import { BentoButton } from '@/components/button'; import { BentoImage } from '@/components/image'; import { BentoInputField } from '@/components/input-field'; import { BentoPopover } from '@/components/popover'; import { BentoTypography } from '@/components/typography'; import { Tooltip } from '@/internal'; import type { BentoInputFieldProps } from '@/components/input-field/input-field.types'; import { BentoInputFieldPasswordDefaultRule, type BentoInputFieldPasswordErrorsList, type BentoInputFieldPasswordProps, BentoInputFieldPasswordType, type BentoInputFieldPasswordValidation, type BentoInputFieldPasswordValidationListItem, } from './input-field-password.types'; import CheckmarkCircleFillIcon from '@adyen/ui-assets-icons-16/vue/checkmark-circle-fill'; import DotIcon from '@adyen/ui-assets-icons-16/vue/dot'; import HideIcon from '@adyen/ui-assets-icons-16/vue/hide'; import ShowIcon from '@adyen/ui-assets-icons-16/vue/show'; import CapsLockIcon from './assets/capslock.svg'; import { useFocusWithin, watchDebounced } from '@vueuse/core'; import { computed, provide, ref, type SetupContext, useAttrs, watch } from 'vue'; import { useI18n } from '@/utils/ts/i18n'; import { minLength, requireLowercase, requireNumeric, requireSpecialCharacter, requireUppercase, } from '@/components/input-field-password/utilities/input-field-password.validation'; import { generateUid } from '@/core/utils/ts'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; import { deprecate } from '@/utils/ts/deprecate'; 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, n } = useI18n<{ message: MessageSchema }>({ messages }); const props = withDefaults(defineProps<BentoInputFieldPasswordProps>(), { condensed: false, debounceTime: DEBOUNCE_DURATION, description: null, disabled: false, disableValidation: false, errorMessage: null, label: null, optional: false, placeholder: '', readonly: false, required: false, tooltipText: null, validation: undefined, value: undefined, modelValue: undefined, withHint: true, }); const emit = defineEmits<{ /** * Emitted when the element has lost focus */ (e: 'blur'): void; /** * Emitted when the element has received focus */ (e: 'focus'): void; /** * Emits an `input:valid` event when the value is validated against the provided validations. * This emit has a debounce and is emitted after an `input` event. */ (e: 'input:valid'): void; /** * Emits an `input:error` event when the value fails the validation. * This emit has a debounce and is emitted after an `input` event. */ (e: 'input:error', errors: BentoInputFieldPasswordErrorsList): void; /** * Emits an `input` event whenever the value in the input is changed. * @deprecated Since v2.0.0. Use `v-model` or `update:model-value` instead. */ (e: 'input', value: BentoInputFieldPasswordProps['value']): void; /** * Emits an `update:model-value` event whenever the value in the input is changed. */ (e: 'update:model-value', value: BentoInputFieldPasswordProps['modelValue']): void; }>(); const attrs = useAttrs(); const inputType = ref(BentoInputFieldPasswordType.PASSWORD); const inputValue = ref(props.modelValue ?? props.value); const isHintDisplayed = ref(false); const isTouched = ref(false); const passwordValidity = ref<Array<boolean>>([]); const errorsList = ref<BentoInputFieldPasswordErrorsList>({}); const capsLockTooltipTimeoutID = ref<ReturnType<typeof setTimeout>>(null); const isTooltipDisplayed = ref(false); const inputFieldPasswordRef = ref(null); const inputFieldPasswordInputRef = ref(null); const inputFieldPasswordCapsLockIconRef = ref(null); const isCapsLockOn = ref(false); provide(INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, 'bento-input-field-password'); const { focused } = useFocusWithin(inputFieldPasswordRef); const { emitValue } = useFormFieldEmits<BentoInputFieldPasswordProps['modelValue']>(emit); const validationListId = generateUid('validationListId'); const validationPopoverId = generateUid('validationPopoverId'); const isPasswordReadable = computed(() => inputType.value === BentoInputFieldPasswordType.TEXT); const passwordValidityConditionalClass = computed(() => passwordValidity.value.map(validity => validity ? 'b-input-field-password__validation-list--valid' : 'b-input-field-password__validation-list--invalid' ) ); const tooltipConditionalClass = computed(() => capsLockTooltipTimeoutID.value ? 'b-input-field-password__input-tooltip--timed' : '' ); const inputFieldArgs = computed<BentoInputFieldProps & SetupContext['attrs']>(() => ({ ariaHidden: false, condensed: props.condensed, disabled: props.disabled, errorMessage: props.errorMessage, optional: props.optional, placeholder: props.placeholder, readonly: props.readonly, required: props.required, tooltipText: props.tooltipText, type: inputType.value, modelValue: inputValue.value, 'aria-haspopup': hasValidations.value ? 'dialog' : false, 'aria-describedby': hasValidations.value ? validationPopoverId : '', ...attrs, })); const computedValidations = computed<BentoInputFieldPasswordValidation>(() => { if (props.validation && !props.validation.extendDefaultValidation) { return props.validation; } const defaultValidationList = [ { label: t('validationMinLength', { numberOfCharacters: n(MIN_PASSWORD_LENGTH) }), key: BentoInputFieldPasswordDefaultRule.MIN_LENGTH, validate: minLength, }, { label: t('validationRequireLowercase', { numberOne: n(1) }), key: BentoInputFieldPasswordDefaultRule.REQUIRE_LOWERCASE, validate: requireLowercase, }, { label: t('validationRequireUppercase', { numberOne: n(1) }), key: BentoInputFieldPasswordDefaultRule.REQUIRE_UPPERCASE, validate: requireUppercase, }, { label: t('validationRequireNumeric', { numberOne: n(1) }), key: BentoInputFieldPasswordDefaultRule.REQUIRE_NUMERIC, validate: requireNumeric, }, { label: t('validationRequireSpecialCharacter', { numberOne: n(1) }), key: BentoInputFieldPasswordDefaultRule.REQUIRE_SPECIAL_CHARACTER, validate: requireSpecialCharacter, }, ]; let validationList: Array<BentoInputFieldPasswordValidationListItem> = defaultValidationList; if (props.validation) { const defaultValidationKeys: Array<string> = defaultValidationList.map(rule => rule.key); const customRuleKeys = props.validation?.list?.map(rule => rule.key); // remove from the custom list any rules with default keys (rules that are overriding existent rules) const customValidationList = props.validation?.list?.filter(rule => !defaultValidationKeys.includes(rule.key)) ?? []; // loop through the default list and replace default rule with the custom rule if it exists const validationListWithOverrides = defaultValidationList.map(rule => { if (customRuleKeys?.includes(rule.key)) { const customRule = props.validation?.list?.find(customRule => customRule.key === rule.key); return customRule; } else { return rule; } }); validationList = [...validationListWithOverrides, ...customValidationList]; } return { title: props.validation?.title ?? t('validationTitle'), suggestion: props.validation?.suggestion ?? t('validationSuggestion'), list: validationList, }; }); const computedValidationList = computed(() => computedValidations.value?.list?.filter(rule => !rule.disabled)); const hasErrors = computed(() => passwordValidity.value?.some(validation => !validation)); const hasValidations = computed( () => inputFieldPasswordInputRef.value && computedValidations.value?.list?.length > 0 ); const checkCapsLock = (e: KeyboardEvent) => { // Check if getModifierState exists and is a function in case the user's browser is autofilling if (typeof e.getModifierState === 'function') { isCapsLockOn.value = e.getModifierState('CapsLock'); } }; const toggleInputType = () => { if (isPasswordReadable.value) { inputType.value = BentoInputFieldPasswordType.PASSWORD; } else { inputType.value = BentoInputFieldPasswordType.TEXT; } }; const onUpdateModelValue = (value: string) => { if (props.disabled) { return; } inputValue.value = value; emitValue(inputValue.value); }; const onFocus = () => { emit('focus'); }; const onBlur = () => { emit('blur'); }; const checkValidity = (value: BentoInputFieldPasswordProps['value']) => { const rulesValidityObj = computedValidationList.value?.map(validationItem => ({ key: validationItem.key, label: validationItem.label, isValid: validationItem.validate(value, ...(validationItem.additionalArgs ?? [])), })); let customRuleIndex = 1; passwordValidity.value = rulesValidityObj?.map(error => error.isValid); errorsList.value = rulesValidityObj ?.filter(rule => !rule.isValid) .reduce((errorsObject, error) => { const key = error.key ?? `custom-rule-${customRuleIndex++}`; return { ...errorsObject, [key]: error.label, }; }, {}); }; const handleCapsLockMouseLeave = () => { isTooltipDisplayed.value = false; capsLockTooltipTimeoutID.value = null; }; const handleCapsLockMouseEnter = () => { isTooltipDisplayed.value = true; }; watch( () => [props.value, props.modelValue], () => { inputValue.value = props.modelValue ?? props.value; } ); watch(focused, focused => { // Always set dirty to true when blur occurs if (!focused) { isTouched.value = true; } // Disable validations & hint if "disableValidation" is "true" if (!props.disableValidation) { if (props.withHint) { isHintDisplayed.value = focused; } checkValidity(inputValue.value); } }); watch(isCapsLockOn, isCapsLockOn => { if (!isCapsLockOn) { clearTimeout(capsLockTooltipTimeoutID.value); capsLockTooltipTimeoutID.value = null; isTooltipDisplayed.value = false; } else { isTooltipDisplayed.value = true; capsLockTooltipTimeoutID.value = setTimeout(() => { capsLockTooltipTimeoutID.value = null; isTooltipDisplayed.value = false; }, TOOLTIP_TIMEOUT); } }); watchDebounced( inputValue, inputValue => { checkValidity(inputValue); if (!hasErrors.value) { emit('input:valid'); } else { emit('input:error', errorsList.value); } }, // wait for 0.5s before checking validity OR 2s of continuous typing { debounce: props.debounceTime, maxWait: DEBOUNCE_MAX_WAIT } ); if (props.value) { deprecate( 'BentoInputFieldPassword "value" property', `The use of "value" prop in "BentoInputFieldPassword" is no longer supported. Use the "v-model" or "model-value" property instead.`, '2.0.0' ); } </script> <script lang="ts"> const MIN_PASSWORD_LENGTH = 12; const DEBOUNCE_DURATION = 500; const DEBOUNCE_MAX_WAIT = 2000; const TOOLTIP_TIMEOUT = 2000; /** * The input field password is a component designed to support users' preference for entering values securely. * This is an extension of the `bento-input-field` component. * * @example * import { BentoInputFieldPassword } from '@adyen/bento-vue2'; * * export default { * components: { BentoInputFieldPassword }, * template: ` * <bento-input-field-password * label="Label" * error-message="Error" * :validation="{ * extendDefaultValidation: true, * list: [ * { label: '18 characters', key: 'minLength', validate: minLength, additionalArgs: [18] }, * { label: 'Always true', key: 'alwaysTrue', validate: () => true }, * { key: 'requireSpecialCharacter', disabled: true } * ], * }" * /> * ` * } */ export default { i18n: { messages }, name: 'bento-input-field-password', model: { prop: 'modelValue' }, }; </script> <style lang="scss" scoped src="./input-field-password.scss" />
|
|
1
|
+
<template> <div ref="inputFieldPasswordRef" @focusout="onFocusOut"> <bento-input-field ref="inputFieldPasswordInputRef" v-bind="inputFieldArgs" class="b-input-field-password__input" autocapitalize="none" autocorrect="off" spellcheck="false" @blur="onBlur" @focus="onFocus" @update:model-value="onUpdateModelValue" @keydown="checkCapsLock" @keyup="checkCapsLock" > <template v-if="label">{{ label }}</template> <template v-else><slot /></template> <template v-if="description" #description> <slot name="description">{{ description }}</slot> </template> <template v-if="isCapsLockOn" #iconBefore> <div class="b-input-field-password__caps-lock-indicator" @mouseenter="handleCapsLockMouseEnter" @mouseleave="handleCapsLockMouseLeave" > <bento-image ref="inputFieldPasswordCapsLockIconRef" :src="CapsLockIcon" :alt="t('capsLockEnabled')" /> </div> <tooltip v-if="inputFieldPasswordCapsLockIconRef" :class="tooltipConditionalClass" :content="t('capsLockEnabled')" :disabled-focus-trap="true" :fallback-position="['top', 'bottom']" :is-shown="isTooltipDisplayed" :target-element="inputFieldPasswordCapsLockIconRef" /> </template> <template v-if="!disabled" #iconAfter> <bento-button variant="tertiary" @click="toggleInputType" @mousedown.native.prevent> <template #iconLeft> <show-icon v-if="!isPasswordReadable" :svg-title="t('showPassword')" /> <hide-icon v-if="isPasswordReadable" :svg-title="t('hidePassword')" /> </template> </bento-button> </template> </bento-input-field> <bento-popover v-if="hasValidations" :open="isHintDisplayed" :target-element="inputFieldPasswordInputRef" :disable-focus-trap="true" :aria-label="computedValidations.title" :aria-describedby="validationListId" position="right-start" > <div :id="validationPopoverId"> <bento-typography v-if="computedValidations.title" class="b-input-field-password__validation-title"> {{ computedValidations.title }} </bento-typography> <p v-if="!hasErrors" class="b-input-field-password__validation-status--valid" role="alert"> {{ t('validationValid') }} </p> <ul :id="validationListId" class="b-input-field-password__validation-list"> <li v-for="(rule, index) in computedValidationList" :key="index" :class="passwordValidityConditionalClass[index]" aria-live="polite" aria-atomic="true" > <span class="b-input-field-password__validation-list-icon"> <checkmark-circle-fill-icon v-if="passwordValidity[index]" :svg-title="t('validationChecked')" /> <dot-icon v-else :svg-title="t('validationUnchecked')" /> </span> <bento-typography class="b-input-field-password__validation-list-text" el="span"> {{ rule.label }} </bento-typography> </li> </ul> <bento-typography v-if="computedValidations.suggestion" class="b-input-field-password__validation-suggestion" > {{ computedValidations.suggestion }} </bento-typography> </div> </bento-popover> </div> </template> <script setup lang="ts"> import { BentoButton } from '@/components/button'; import { BentoImage } from '@/components/image'; import { BentoInputField } from '@/components/input-field'; import { BentoPopover } from '@/components/popover'; import { BentoTypography } from '@/components/typography'; import { Tooltip } from '@/internal'; import type { BentoInputFieldProps } from '@/components/input-field/input-field.types'; import { BentoInputFieldPasswordDefaultRule, type BentoInputFieldPasswordErrorsList, type BentoInputFieldPasswordProps, BentoInputFieldPasswordType, type BentoInputFieldPasswordValidation, type BentoInputFieldPasswordValidationListItem, } from './input-field-password.types'; import CheckmarkCircleFillIcon from '@adyen/ui-assets-icons-16/vue/checkmark-circle-fill'; import DotIcon from '@adyen/ui-assets-icons-16/vue/dot'; import HideIcon from '@adyen/ui-assets-icons-16/vue/hide'; import ShowIcon from '@adyen/ui-assets-icons-16/vue/show'; import CapsLockIcon from './assets/capslock.svg'; import { watchDebounced } from '@vueuse/core'; import { computed, provide, ref, type SetupContext, useAttrs, watch } from 'vue'; import { useI18n } from '@/utils/ts/i18n'; import { minLength, requireLowercase, requireNumeric, requireSpecialCharacter, requireUppercase, } from '@/components/input-field-password/utilities/input-field-password.validation'; import { generateUid } from '@/core/utils/ts'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; import { deprecate } from '@/utils/ts/deprecate'; 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, n } = useI18n<{ message: MessageSchema }>({ messages }); const props = withDefaults(defineProps<BentoInputFieldPasswordProps>(), { condensed: false, debounceTime: DEBOUNCE_DURATION, description: null, disabled: false, disableValidation: false, errorMessage: null, label: null, optional: false, placeholder: '', readonly: false, required: false, tooltipText: null, validation: undefined, value: undefined, modelValue: undefined, withHint: true, }); const emit = defineEmits<{ /** * Emitted when the element has lost focus */ (e: 'blur'): void; /** * Emitted when the element has received focus */ (e: 'focus'): void; /** * Emits an `input:valid` event when the value is validated against the provided validations. * This emit has a debounce and is emitted after an `input` event. */ (e: 'input:valid'): void; /** * Emits an `input:error` event when the value fails the validation. * This emit has a debounce and is emitted after an `input` event. */ (e: 'input:error', errors: BentoInputFieldPasswordErrorsList): void; /** * Emits an `input` event whenever the value in the input is changed. * @deprecated Since v2.0.0. Use `v-model` or `update:model-value` instead. */ (e: 'input', value: BentoInputFieldPasswordProps['value']): void; /** * Emits an `update:model-value` event whenever the value in the input is changed. */ (e: 'update:model-value', value: BentoInputFieldPasswordProps['modelValue']): void; }>(); const attrs = useAttrs(); const inputType = ref(BentoInputFieldPasswordType.PASSWORD); const inputValue = ref(props.modelValue ?? props.value); const isHintDisplayed = ref(false); const isTouched = ref(false); const passwordValidity = ref<Array<boolean>>([]); const errorsList = ref<BentoInputFieldPasswordErrorsList>({}); const capsLockTooltipTimeoutID = ref<ReturnType<typeof setTimeout>>(null); const isTooltipDisplayed = ref(false); const inputFieldPasswordRef = ref(null); const inputFieldPasswordInputRef = ref(null); const inputFieldPasswordCapsLockIconRef = ref(null); const isCapsLockOn = ref(false); provide(INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, 'bento-input-field-password'); const onFocusOut = (event: FocusEvent) => { const container = inputFieldPasswordRef.value as HTMLElement | null; // If focus moved to another element still inside the container, keep the popover open. if (container && event.relatedTarget instanceof Node && container.contains(event.relatedTarget)) { return; } isTouched.value = true; if (!props.disableValidation) { if (props.withHint) { isHintDisplayed.value = false; } checkValidity(inputValue.value); } }; const { emitValue } = useFormFieldEmits<BentoInputFieldPasswordProps['modelValue']>(emit); const validationListId = generateUid('validationListId'); const validationPopoverId = generateUid('validationPopoverId'); const isPasswordReadable = computed(() => inputType.value === BentoInputFieldPasswordType.TEXT); const passwordValidityConditionalClass = computed(() => passwordValidity.value.map(validity => validity ? 'b-input-field-password__validation-list--valid' : 'b-input-field-password__validation-list--invalid' ) ); const tooltipConditionalClass = computed(() => capsLockTooltipTimeoutID.value ? 'b-input-field-password__input-tooltip--timed' : '' ); const inputFieldArgs = computed<BentoInputFieldProps & SetupContext['attrs']>(() => ({ ariaHidden: false, condensed: props.condensed, disabled: props.disabled, errorMessage: props.errorMessage, optional: props.optional, placeholder: props.placeholder, readonly: props.readonly, required: props.required, tooltipText: props.tooltipText, type: inputType.value, modelValue: inputValue.value, 'aria-haspopup': hasValidations.value ? 'dialog' : false, 'aria-describedby': hasValidations.value ? validationPopoverId : '', ...attrs, })); const computedValidations = computed<BentoInputFieldPasswordValidation>(() => { if (props.validation && !props.validation.extendDefaultValidation) { return props.validation; } const defaultValidationList = [ { label: t('validationMinLength', { numberOfCharacters: n(MIN_PASSWORD_LENGTH) }), key: BentoInputFieldPasswordDefaultRule.MIN_LENGTH, validate: minLength, }, { label: t('validationRequireLowercase', { numberOne: n(1) }), key: BentoInputFieldPasswordDefaultRule.REQUIRE_LOWERCASE, validate: requireLowercase, }, { label: t('validationRequireUppercase', { numberOne: n(1) }), key: BentoInputFieldPasswordDefaultRule.REQUIRE_UPPERCASE, validate: requireUppercase, }, { label: t('validationRequireNumeric', { numberOne: n(1) }), key: BentoInputFieldPasswordDefaultRule.REQUIRE_NUMERIC, validate: requireNumeric, }, { label: t('validationRequireSpecialCharacter', { numberOne: n(1) }), key: BentoInputFieldPasswordDefaultRule.REQUIRE_SPECIAL_CHARACTER, validate: requireSpecialCharacter, }, ]; let validationList: Array<BentoInputFieldPasswordValidationListItem> = defaultValidationList; if (props.validation) { const defaultValidationKeys: Array<string> = defaultValidationList.map(rule => rule.key); const customRuleKeys = props.validation?.list?.map(rule => rule.key); // remove from the custom list any rules with default keys (rules that are overriding existent rules) const customValidationList = props.validation?.list?.filter(rule => !defaultValidationKeys.includes(rule.key)) ?? []; // loop through the default list and replace default rule with the custom rule if it exists const validationListWithOverrides = defaultValidationList.map(rule => { if (customRuleKeys?.includes(rule.key)) { const customRule = props.validation?.list?.find(customRule => customRule.key === rule.key); return customRule; } else { return rule; } }); validationList = [...validationListWithOverrides, ...customValidationList]; } return { title: props.validation?.title ?? t('validationTitle'), suggestion: props.validation?.suggestion ?? t('validationSuggestion'), list: validationList, }; }); const computedValidationList = computed(() => computedValidations.value?.list?.filter(rule => !rule.disabled)); const hasErrors = computed(() => passwordValidity.value?.some(validation => !validation)); const hasValidations = computed( () => inputFieldPasswordInputRef.value && computedValidations.value?.list?.length > 0 ); const checkCapsLock = (e: KeyboardEvent) => { // Check if getModifierState exists and is a function in case the user's browser is autofilling if (typeof e.getModifierState === 'function') { isCapsLockOn.value = e.getModifierState('CapsLock'); } }; const toggleInputType = () => { if (isPasswordReadable.value) { inputType.value = BentoInputFieldPasswordType.PASSWORD; } else { inputType.value = BentoInputFieldPasswordType.TEXT; } }; const onUpdateModelValue = (value: string) => { if (props.disabled) { return; } inputValue.value = value; emitValue(inputValue.value); }; const onFocus = () => { emit('focus'); if (!props.disableValidation) { if (props.withHint) { isHintDisplayed.value = true; } checkValidity(inputValue.value); } }; const onBlur = () => { emit('blur'); }; const checkValidity = (value: BentoInputFieldPasswordProps['value']) => { const rulesValidityObj = computedValidationList.value?.map(validationItem => ({ key: validationItem.key, label: validationItem.label, isValid: validationItem.validate(value, ...(validationItem.additionalArgs ?? [])), })); let customRuleIndex = 1; passwordValidity.value = rulesValidityObj?.map(error => error.isValid); errorsList.value = rulesValidityObj ?.filter(rule => !rule.isValid) .reduce((errorsObject, error) => { const key = error.key ?? `custom-rule-${customRuleIndex++}`; return { ...errorsObject, [key]: error.label, }; }, {}); }; const handleCapsLockMouseLeave = () => { isTooltipDisplayed.value = false; capsLockTooltipTimeoutID.value = null; }; const handleCapsLockMouseEnter = () => { isTooltipDisplayed.value = true; }; watch( () => [props.value, props.modelValue], () => { inputValue.value = props.modelValue ?? props.value; } ); watch(isCapsLockOn, isCapsLockOn => { if (!isCapsLockOn) { clearTimeout(capsLockTooltipTimeoutID.value); capsLockTooltipTimeoutID.value = null; isTooltipDisplayed.value = false; } else { isTooltipDisplayed.value = true; capsLockTooltipTimeoutID.value = setTimeout(() => { capsLockTooltipTimeoutID.value = null; isTooltipDisplayed.value = false; }, TOOLTIP_TIMEOUT); } }); watchDebounced( inputValue, inputValue => { checkValidity(inputValue); if (!hasErrors.value) { emit('input:valid'); } else { emit('input:error', errorsList.value); } }, // wait for 0.5s before checking validity OR 2s of continuous typing { debounce: props.debounceTime, maxWait: DEBOUNCE_MAX_WAIT } ); if (props.value) { deprecate( 'BentoInputFieldPassword "value" property', `The use of "value" prop in "BentoInputFieldPassword" is no longer supported. Use the "v-model" or "model-value" property instead.`, '2.0.0' ); } </script> <script lang="ts"> const MIN_PASSWORD_LENGTH = 12; const DEBOUNCE_DURATION = 500; const DEBOUNCE_MAX_WAIT = 2000; const TOOLTIP_TIMEOUT = 2000; /** * The input field password is a component designed to support users' preference for entering values securely. * This is an extension of the `bento-input-field` component. * * @example * import { BentoInputFieldPassword } from '@adyen/bento-vue2'; * * export default { * components: { BentoInputFieldPassword }, * template: ` * <bento-input-field-password * label="Label" * error-message="Error" * :validation="{ * extendDefaultValidation: true, * list: [ * { label: '18 characters', key: 'minLength', validate: minLength, additionalArgs: [18] }, * { label: 'Always true', key: 'alwaysTrue', validate: () => true }, * { key: 'requireSpecialCharacter', disabled: true } * ], * }" * /> * ` * } */ export default { i18n: { messages }, name: 'bento-input-field-password', model: { prop: 'modelValue' }, }; </script> <style lang="scss" scoped src="./input-field-password.scss" />
|
package/dist/assets/components/internal/listbox/components/listbox-option/listbox-option.vue
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div class="b-listbox-option" role="option" :class="conditionalClasses" v-bind="$attrs" @click="onClick" @keydown.enter="onOptionSelected(BentoListboxEvent.ENTER, $event)" @keydown.space="onOptionSelected(BentoListboxEvent.SPACE, $event)" @keydown.tab="onOptionSelected(BentoListboxEvent.TAB, $event)" > <slot></slot> </div> </template> <script lang="ts"> import { computed, defineComponent } from 'vue'; import { BentoListboxEvent } from '@/types/listbox'; import { BentoKeyboardNavigationKeyDownEvent } from '@/types/keyboard-navigation'; /** * listbox option wrapper. * Contains only styles and allows the Multi-Select and Single-Select DRY the styles. * * Options and Attributes are inherited (`inheritAttrs: true`) * * @example * <bento-listbox-option * ...props * ...attrs * /> */ export default defineComponent({ name: 'bento-listbox-option', props: { /** * Setting this property to true will disable the option. */ disabled: { type: Boolean, default: false }, /** * Indicates if the value is selected. Only one element can be selected at a time. */ selected: { type: Boolean, default: false }, }, emits: [BentoListboxEvent.CLICK, BentoListboxEvent.ENTER, BentoListboxEvent.SPACE, BentoListboxEvent.TAB], setup(props, { emit }) { const conditionalClasses = computed(() => ({ 'b-listbox-option--disabled': props.disabled, 'b-listbox-option--selected': props.selected, })); const onClick = (e: Event) => { emit(BentoListboxEvent.CLICK, e); }; const onOptionSelected = (eventName, e: Event) => { emit(eventName, e); }; return { // Enums BentoKeyboardNavigationKeyDownEvent, BentoListboxEvent, // Values conditionalClasses, // Events onClick, onOptionSelected, }; }, }); </script> <style lang="scss" scoped src="./listbox-option.scss" />
|
|
1
|
+
<template> <div class="b-listbox-option" role="option" :class="conditionalClasses" v-bind="$attrs" @click="onClick" @keydown.enter="onOptionSelected(BentoListboxEvent.ENTER, $event)" @keydown.space="onOptionSelected(BentoListboxEvent.SPACE, $event)" @keydown.tab="onOptionSelected(BentoListboxEvent.TAB, $event)" > <slot></slot> </div> </template> <script lang="ts"> import { computed, defineComponent } from 'vue'; import { BentoListboxEvent } from '@/types/listbox'; import { BentoKeyboardNavigationKeyDownEvent } from '@/types/keyboard-navigation'; /** * listbox option wrapper. * Contains only styles and allows the Multi-Select and Single-Select DRY the styles. * * Options and Attributes are inherited (`inheritAttrs: true`) * * @example * <bento-listbox-option * ...props * ...attrs * /> */ export default defineComponent({ name: 'bento-listbox-option', props: { /** * Indicates if the option is active. */ active: { type: Boolean, default: false }, /** * Setting this property to true will disable the option. */ disabled: { type: Boolean, default: false }, /** * Indicates if the value is selected. Only one element can be selected at a time. */ selected: { type: Boolean, default: false }, }, emits: [BentoListboxEvent.CLICK, BentoListboxEvent.ENTER, BentoListboxEvent.SPACE, BentoListboxEvent.TAB], setup(props, { emit }) { const conditionalClasses = computed(() => ({ 'b-listbox-option--active': props.active, 'b-listbox-option--disabled': props.disabled, 'b-listbox-option--selected': props.selected, })); const onClick = (e: Event) => { emit(BentoListboxEvent.CLICK, e); }; const onOptionSelected = (eventName, e: Event) => { emit(eventName, e); }; return { // Enums BentoKeyboardNavigationKeyDownEvent, BentoListboxEvent, // Values conditionalClasses, // Events onClick, onOptionSelected, }; }, }); </script> <style lang="scss" scoped src="./listbox-option.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div ref="listboxRef" v-bento-keyboard-navigation-directive v-bind="ariaAttributes" role="listbox" v-on="listboxListeners" @focusin="handleVirtualScrollingFocus" > <template v-if="hasCategories && staticCategories"> <template v-for="item in items"> <bento-listbox-single-select-category v-if="item.items && item.items.length > 0" :key="`category-${item.value}`" :category-label="item.label" > <bento-listbox-single-select-option v-for="option in item.items" :key="option.value" ref="listboxItemRef" role="option" :option="option" :disabled="isOptionDisabled ? isOptionDisabled(option) : null" :selected="option.value === selectedItem.value" v-on="listboxOptionListeners" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </bento-listbox-single-select-option> </bento-listbox-single-select-category> <bento-listbox-single-select-option v-else :key="`option-${item.value}`" ref="listboxItemRef" role="option" :option="item" :disabled="isOptionDisabled ? isOptionDisabled(item) : null" :selected="item.value === selectedItem.value" v-on="listboxOptionListeners" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </bento-listbox-single-select-option> </template> </template> <template v-else> <bento-listbox-single-select-option v-for="option in items" :key="option.value" ref="listboxItemRef" role="option" :option="option" :disabled="isOptionDisabled ? isOptionDisabled(option) : null" :selected="option.value === selectedItem.value" v-on="listboxOptionListeners" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </bento-listbox-single-select-option> </template> </div> </template> <script lang="ts"> import { computed, defineComponent, type HTMLAttributes, nextTick, type PropType, ref, toRef } from 'vue'; import { BentoListboxSingleSelectCategory } from './components/listbox-single-select-category'; import { BentoListboxSingleSelectOption } from '../listbox-single-select-option'; import { useListboxKeyboardNavigation } from '../../composables/useListboxKeyboardNavigation'; import { useVisibleDomOptions } from '@/internal/listbox/composables/use-visible-dom-options/use-visible-dom-options'; import { BentoListboxEvent, type BentoListboxIsOptionDisabled, type BentoListboxOptions } from '@/types/listbox'; import { BentoKeyboardNavigationKeyDownEvent } from '@/types/keyboard-navigation'; import { type Booleanish } from '@/types/prop-types'; import { BentoKeyboardNavigationDirective } from '@/directives'; /** * Listbox single-select options list * * @example * import { BentoListboxMultiSelect } from '@adyen/bento-vue2'; * * export default { * components: { BentoListboxMultiSelect }, * template: ` * <bento-listbox-multi-select * :items="[{ label: 'Option 1', value: 'option-1' }]" * :selected-values="['option-1']" * /> * ` * } */ export default defineComponent({ name: 'bento-listbox-single-select', components: { BentoListboxSingleSelectCategory, BentoListboxSingleSelectOption, }, directives: { BentoKeyboardNavigationDirective }, inheritAttrs: false, props: { /** * Function that allows the options to be disabled * * @type {BentoListboxIsOptionDisabled} * @param {BentoListboxOptionItem} option - listbox option object * @param {string} option.label - Option's label text * @param {string|number} option.value - Option's value * @param {unknown} option.data - Option's extra data to be passed to the default slot */ isOptionDisabled: { type: Function as PropType<BentoListboxIsOptionDisabled>, default: () => false, }, /** * The option elements to populate the listbox with. * It must be an array of {@see BentoListboxOptionItem }. * * @property {string} value.label - Text to be displayed in the option * @property {string,number} value.value - Value of the option */ items: { type: Array as PropType<BentoListboxOptions>, required: true, }, /** * The `input` value. * Providing an empty string will select no options. * Set to an empty string `''` if you don't want any of the available options to be selected */ selectedValue: { type: Array as PropType<BentoListboxOptions>, default: undefined, }, /** * If enabled, dropdown will display non-selectable, static categories */ staticCategories: { type: Boolean, default: false }, /** * Total number of items in the full list (used for virtual scroll keyboard navigation). * When greater than 0, virtual scroll keyboard navigation is enabled. */ totalItemCount: { type: Number, default: 0 }, /** * Function to scroll the virtual list container to a given item index. */ scrollToIndex: { type: Function as PropType<(index: number) => void>, default: undefined }, /** * The start index of the currently rendered virtual scroll slice. */ virtualScrollStartIndex: { type: Number, default: 0 }, }, emits: [BentoListboxEvent.SELECT, BentoListboxEvent.ENTER, BentoListboxEvent.SPACE, BentoListboxEvent.TAB], setup(props, { emit, expose, slots, attrs }) { const listboxRef = ref<HTMLElement>(); const listboxItemRef = ref([]); const selectedItem = computed(() => !!props.selectedValue?.length && props.selectedValue.at(0)); const ariaAttributes = computed( () => ({ 'aria-label': attrs['aria-label']?.toString() as string, 'aria-busy': attrs['aria-busy']?.toString() as Booleanish, 'aria-atomic': attrs['aria-atomic']?.toString() as Booleanish, 'aria-live': attrs['aria-live']?.toString() as string, }) as HTMLAttributes ); /** * Finds out if items have sub items */ const hasCategories = computed(() => !!props.items.some(({ items }) => !!items)); const onOptionSelected = eventName => (selectedOption: BentoListboxOptions) => { emit(eventName, [selectedOption]); }; // Keyboard navigation const { visibleDomOptions, updateVisibleDomOptions } = useVisibleDomOptions( listboxRef, toRef(props, 'items'), ref('option'), false ); const isVirtualScrollEnabled = computed(() => props.totalItemCount > 0); // When virtual scroll re-renders items (e.g. on mouse wheel scroll), the // browser may auto-focus an option. Sync the keyboard navigation index to // match the focused element so subsequent arrow key presses navigate from // the correct position. const handleVirtualScrollingFocus = (event: FocusEvent) => { if (!isVirtualScrollEnabled.value) { return; } const target = event.target as HTMLElement; if (target === listboxRef.value || target?.getAttribute('role') !== 'option') { return; } const localIndex = visibleDomOptions.value.indexOf(target as HTMLDivElement); if (localIndex !== -1) { const globalIndex = localIndex + props.virtualScrollStartIndex; setListboxFocusedIndex(globalIndex); } }; const focusItem = async (globalIndex: number) => { if (isVirtualScrollEnabled.value && props.scrollToIndex) { props.scrollToIndex(globalIndex); await nextTick(); await updateVisibleDomOptions(); const newDomIndex = globalIndex - props.virtualScrollStartIndex; visibleDomOptions.value[newDomIndex]?.focus(); } else { visibleDomOptions.value[globalIndex]?.focus(); } }; const navigateToItem = async (globalIndex: number) => { setListboxFocusedIndex(globalIndex); await focusItem(globalIndex); }; const scrollIntoContainer = (listboxItemElement?: HTMLElement, listboxElement?: HTMLElement) => { if (!listboxItemElement || !listboxElement) { return; } const elementRect = listboxItemElement.getBoundingClientRect(); const scrollableContainer = listboxElement; const containerRect = scrollableContainer.getBoundingClientRect(); if (elementRect.top < containerRect.top) { scrollableContainer.scrollTop -= containerRect.top - elementRect.top; } else if (elementRect.bottom > containerRect.bottom) { scrollableContainer.scrollTop += elementRect.bottom - containerRect.bottom; } }; const scrollToItem = async (globalIndex: number, listboxElement?: HTMLElement) => { await updateVisibleDomOptions(); if (isVirtualScrollEnabled.value && props.scrollToIndex) { props.scrollToIndex(globalIndex); } else { scrollIntoContainer(visibleDomOptions.value[globalIndex], listboxElement); } }; const { moveToNextElement, moveToPreviousElement, moveToFirstElement, moveToLastElement, setListboxFocusedIndex, } = useListboxKeyboardNavigation( { items: visibleDomOptions, totalItemCount: toRef(props, 'totalItemCount'), }, focusItem ); const listboxListeners = { [BentoKeyboardNavigationKeyDownEvent.ARROW_UP]: moveToPreviousElement, [BentoKeyboardNavigationKeyDownEvent.ARROW_DOWN]: moveToNextElement, [BentoKeyboardNavigationKeyDownEvent.HOME]: moveToFirstElement, [BentoKeyboardNavigationKeyDownEvent.END]: moveToLastElement, }; const listboxOptionListeners = { [BentoListboxEvent.SELECT]: onOptionSelected(BentoListboxEvent.SELECT), [BentoListboxEvent.SPACE]: onOptionSelected(BentoListboxEvent.SPACE), [BentoListboxEvent.TAB]: onOptionSelected(BentoListboxEvent.TAB), [BentoListboxEvent.ENTER]: onOptionSelected(BentoListboxEvent.ENTER), }; expose({ /** * Reference for a single listbox item. * Required for keyboard navigation in dropdown as $children is removed in vue@3 */ listboxItemRef, /** * Method to set the index of the item to be focused on * Used for focus management to be kept in-sync with parent e.g. dropdowns */ setListboxFocusedIndex, /** * Navigates to an item by global index, handling virtual scroll if enabled. * Scrolls the virtual list and focuses the item. */ navigateToItem, /** * Scrolls to an item by global index without changing focus. * Used when the dropdown opens via click to bring the selected option into view. */ scrollToItem, }); return { // Enums BentoListboxEvent, // Values listboxRef, listboxItemRef, selectedItem, slots, ariaAttributes, // Computed hasCategories, // Methods onOptionSelected, // Keyboard navigation listboxListeners, listboxOptionListeners, // Virtual scroll focus management handleVirtualScrollingFocus, }; }, }); </script>
|
|
1
|
+
<template> <div ref="listboxRef" v-bento-keyboard-navigation-directive v-bind="ariaAttributes" role="listbox" v-on="listboxListeners" @focusin="handleVirtualScrollingFocus" > <template v-if="hasCategories && staticCategories"> <template v-for="item in items"> <bento-listbox-single-select-category v-if="item.items && item.items.length > 0" :key="`category-${item.value}`" :category-label="item.label" > <bento-listbox-single-select-option v-for="option in item.items" :id="getOptionId(option)" :key="option.value" ref="listboxItemRef" role="option" :active="activeDescendantId === getOptionId(option)" :option="option" :disabled="isOptionDisabled ? isOptionDisabled(option) : null" :selected="option.value === selectedItem.value" v-on="listboxOptionListeners" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </bento-listbox-single-select-option> </bento-listbox-single-select-category> <bento-listbox-single-select-option v-else :id="getOptionId(item)" :key="`option-${item.value}`" ref="listboxItemRef" role="option" :active="activeDescendantId === getOptionId(item)" :option="item" :disabled="isOptionDisabled ? isOptionDisabled(item) : null" :selected="item.value === selectedItem.value" v-on="listboxOptionListeners" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </bento-listbox-single-select-option> </template> </template> <template v-else> <bento-listbox-single-select-option v-for="option in items" :id="getOptionId(option)" :key="option.value" ref="listboxItemRef" role="option" :active="activeDescendantId === getOptionId(option)" :option="option" :disabled="isOptionDisabled ? isOptionDisabled(option) : null" :selected="option.value === selectedItem.value" v-on="listboxOptionListeners" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </bento-listbox-single-select-option> </template> </div> </template> <script lang="ts"> import { computed, defineComponent, type HTMLAttributes, nextTick, type PropType, ref, toRef } from 'vue'; import { BentoListboxSingleSelectCategory } from './components/listbox-single-select-category'; import { BentoListboxSingleSelectOption } from '../listbox-single-select-option'; import { useListboxKeyboardNavigation } from '../../composables/useListboxKeyboardNavigation'; import { useVisibleDomOptions } from '@/internal/listbox/composables/use-visible-dom-options/use-visible-dom-options'; import { BentoListboxEvent, type BentoListboxIsOptionDisabled, type BentoListboxOptionItem, type BentoListboxOptions, } from '@/types/listbox'; import { BentoKeyboardNavigationKeyDownEvent } from '@/types/keyboard-navigation'; import { type Booleanish } from '@/types/prop-types'; import { getListboxOptionId } from '@/utils/ts/get-listbox-option-id'; import { BentoKeyboardNavigationDirective } from '@/directives'; /** * Listbox single-select options list * * @example * import { BentoListboxMultiSelect } from '@adyen/bento-vue2'; * * export default { * components: { BentoListboxMultiSelect }, * template: ` * <bento-listbox-multi-select * :items="[{ label: 'Option 1', value: 'option-1' }]" * :selected-values="['option-1']" * /> * ` * } */ export default defineComponent({ name: 'bento-listbox-single-select', components: { BentoListboxSingleSelectCategory, BentoListboxSingleSelectOption, }, directives: { BentoKeyboardNavigationDirective }, inheritAttrs: false, props: { /** * Function that allows the options to be disabled * * @type {BentoListboxIsOptionDisabled} * @param {BentoListboxOptionItem} option - listbox option object * @param {string} option.label - Option's label text * @param {string|number} option.value - Option's value * @param {unknown} option.data - Option's extra data to be passed to the default slot */ isOptionDisabled: { type: Function as PropType<BentoListboxIsOptionDisabled>, default: () => false, }, /** * Identifies the currently active option in the listbox. */ activeDescendantId: { type: String, default: null }, /** * The option elements to populate the listbox with. * It must be an array of {@see BentoListboxOptionItem }. * * @property {string} value.label - Text to be displayed in the option * @property {string,number} value.value - Value of the option */ items: { type: Array as PropType<BentoListboxOptions>, required: true, }, /** * Prefix used to generate stable option IDs. */ optionIdBase: { type: String, required: true }, /** * The `input` value. * Providing an empty string will select no options. * Set to an empty string `''` if you don't want any of the available options to be selected */ selectedValue: { type: Array as PropType<BentoListboxOptions>, default: undefined, }, /** * If enabled, dropdown will display non-selectable, static categories */ staticCategories: { type: Boolean, default: false }, /** * Total number of items in the full list (used for virtual scroll keyboard navigation). * When greater than 0, virtual scroll keyboard navigation is enabled. */ totalItemCount: { type: Number, default: 0 }, /** * Function to scroll the virtual list container to a given item index. */ scrollToIndex: { type: Function as PropType<(index: number) => void>, default: undefined }, /** * The start index of the currently rendered virtual scroll slice. */ virtualScrollStartIndex: { type: Number, default: 0 }, }, emits: [BentoListboxEvent.SELECT, BentoListboxEvent.ENTER, BentoListboxEvent.SPACE, BentoListboxEvent.TAB], setup(props, { emit, expose, slots, attrs }) { const listboxRef = ref<HTMLElement>(); const listboxItemRef = ref([]); const selectedItem = computed(() => !!props.selectedValue?.length && props.selectedValue.at(0)); const ariaAttributes = computed( () => ({ 'aria-label': attrs['aria-label']?.toString() as string, 'aria-busy': attrs['aria-busy']?.toString() as Booleanish, 'aria-atomic': attrs['aria-atomic']?.toString() as Booleanish, 'aria-live': attrs['aria-live']?.toString() as string, }) as HTMLAttributes ); /** * Finds out if items have sub items */ const hasCategories = computed(() => !!props.items.some(({ items }) => !!items)); const onOptionSelected = eventName => (selectedOption: BentoListboxOptions) => { emit(eventName, [selectedOption]); }; const getOptionId = (option: BentoListboxOptionItem) => getListboxOptionId(props.optionIdBase, option.value); // Keyboard navigation const { visibleDomOptions, updateVisibleDomOptions } = useVisibleDomOptions( listboxRef, toRef(props, 'items'), ref('option'), false ); const isVirtualScrollEnabled = computed(() => props.totalItemCount > 0); // When virtual scroll re-renders items (e.g. on mouse wheel scroll), the // browser may auto-focus an option. Sync the keyboard navigation index to // match the focused element so subsequent arrow key presses navigate from // the correct position. const handleVirtualScrollingFocus = (event: FocusEvent) => { if (!isVirtualScrollEnabled.value) { return; } const target = event.target as HTMLElement; if (target === listboxRef.value || target?.getAttribute('role') !== 'option') { return; } const localIndex = visibleDomOptions.value.indexOf(target as HTMLDivElement); if (localIndex !== -1) { const globalIndex = localIndex + props.virtualScrollStartIndex; setListboxFocusedIndex(globalIndex); } }; const focusItem = async (globalIndex: number) => { if (isVirtualScrollEnabled.value && props.scrollToIndex) { props.scrollToIndex(globalIndex); await nextTick(); await updateVisibleDomOptions(); const newDomIndex = globalIndex - props.virtualScrollStartIndex; visibleDomOptions.value[newDomIndex]?.focus(); } else { visibleDomOptions.value[globalIndex]?.focus(); } }; const navigateToItem = async (globalIndex: number) => { setListboxFocusedIndex(globalIndex); await focusItem(globalIndex); }; const scrollIntoContainer = (listboxItemElement?: HTMLElement, listboxElement?: HTMLElement) => { if (!listboxItemElement || !listboxElement) { return; } const elementRect = listboxItemElement.getBoundingClientRect(); const scrollableContainer = listboxElement; const containerRect = scrollableContainer.getBoundingClientRect(); if (elementRect.top < containerRect.top) { scrollableContainer.scrollTop -= containerRect.top - elementRect.top; } else if (elementRect.bottom > containerRect.bottom) { scrollableContainer.scrollTop += elementRect.bottom - containerRect.bottom; } }; const scrollToItem = async (globalIndex: number, listboxElement?: HTMLElement) => { await updateVisibleDomOptions(); if (isVirtualScrollEnabled.value && props.scrollToIndex) { props.scrollToIndex(globalIndex); } else { scrollIntoContainer(visibleDomOptions.value[globalIndex], listboxElement); } }; const { moveToNextElement, moveToPreviousElement, moveToFirstElement, moveToLastElement, setListboxFocusedIndex, } = useListboxKeyboardNavigation( { items: visibleDomOptions, totalItemCount: toRef(props, 'totalItemCount'), }, focusItem ); const listboxListeners = { [BentoKeyboardNavigationKeyDownEvent.ARROW_UP]: moveToPreviousElement, [BentoKeyboardNavigationKeyDownEvent.ARROW_DOWN]: moveToNextElement, [BentoKeyboardNavigationKeyDownEvent.HOME]: moveToFirstElement, [BentoKeyboardNavigationKeyDownEvent.END]: moveToLastElement, }; const listboxOptionListeners = { [BentoListboxEvent.SELECT]: onOptionSelected(BentoListboxEvent.SELECT), [BentoListboxEvent.SPACE]: onOptionSelected(BentoListboxEvent.SPACE), [BentoListboxEvent.TAB]: onOptionSelected(BentoListboxEvent.TAB), [BentoListboxEvent.ENTER]: onOptionSelected(BentoListboxEvent.ENTER), }; expose({ /** * Reference for a single listbox item. * Required for keyboard navigation in dropdown as $children is removed in vue@3 */ listboxItemRef, /** * Method to set the index of the item to be focused on * Used for focus management to be kept in-sync with parent e.g. dropdowns */ setListboxFocusedIndex, /** * Navigates to an item by global index, handling virtual scroll if enabled. * Scrolls the virtual list and focuses the item. */ navigateToItem, /** * Scrolls to an item by global index without changing focus. * Used when the dropdown opens via click to bring the selected option into view. */ scrollToItem, }); return { // Enums BentoListboxEvent, // Values listboxRef, listboxItemRef, selectedItem, slots, ariaAttributes, getOptionId, // Computed hasCategories, // Methods onOptionSelected, // Keyboard navigation listboxListeners, listboxOptionListeners, // Virtual scroll focus management handleVirtualScrollingFocus, }; }, }); </script>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <bento-listbox-option class="b-listbox-single-select-option" :disabled="disabled" :selected="selected" :aria-selected="ariaSelected" :aria-disabled="ariaDisabled" :tabindex="tabIndex" @click="onOptionSelected(BentoListboxEvent.SELECT)" @enter-pressed="onOptionSelected(BentoListboxEvent.ENTER)" @space-pressed.prevent="onOptionSelected(BentoListboxEvent.SPACE)" @tab-pressed="onOptionSelected(BentoListboxEvent.TAB)" > <slot v-bind="option"> <span class="b-listbox-single-select-option__text"> <bento-typography el="span">{{ option.label }}</bento-typography> <bento-typography v-if="option.description" class="b-listbox-single-select-option__description" el="span" > {{ option.description }} </bento-typography> </span> </slot> <span class="b-listbox-single-select-option__check-icon"> <checkmark-icon v-show="selected" svg-title="selected" /> </span> </bento-listbox-option> </template> <script lang="ts"> import { computed, defineComponent, type PropType } from 'vue'; import { BentoTypography } from '@/components/typography'; import { BentoListboxOption } from '../listbox-option'; import { BentoListboxEvent, type BentoListboxOptionItem } from '@/types/listbox'; import CheckmarkIcon from '@adyen/ui-assets-icons-16/vue/checkmark'; /** * Listbox option element. * * @example * <bento-listbox-single-select-option * v-for="option in options" * :key="option.value" * :option="option" * :selected="option.selected" * @select="onOptionSelected" * /> */ export default defineComponent({ name: 'bento-listbox-single-select-option', components: { BentoListboxOption, BentoTypography, CheckmarkIcon, }, props: { /** * Setting this property to true will disable the option. */ disabled: { type: Boolean, default: false }, /** * The option item that contains all the option item's data. * @type {BentoListboxOptionItem} */ option: { type: Object as PropType<BentoListboxOptionItem>, required: true, }, /** * Indicates if the value is selected. Only one element can be selected at a time. */ selected: { type: Boolean, default: false }, }, emits: [BentoListboxEvent.ENTER, BentoListboxEvent.SELECT, BentoListboxEvent.SPACE, BentoListboxEvent.TAB], setup(props, { emit }) { const onOptionSelected = eventName => { if (props.disabled) { return; } /** * Trigerred when an option is clicked or selected via keyboard navigation. * Indicates which option was selected. * * @event {string} - The corresponding event * @property {number} selectedValue - Selected option's value */ emit(eventName, props.option); }; const ariaDisabled = computed(() => (props.disabled ? true : null)); const ariaSelected = computed(() => (props.selected ? true : null)); const tabIndex = computed(() => (props.disabled ? -1 : 0)); return { // Values ariaDisabled, ariaSelected, tabIndex, // Enums BentoListboxEvent, // Events onOptionSelected, }; }, }); </script> <style lang="scss" scoped src="./listbox-single-select-option.scss" />
|
|
1
|
+
<template> <bento-listbox-option class="b-listbox-single-select-option" :active="active" :disabled="disabled" :selected="selected" :aria-selected="ariaSelected" :aria-disabled="ariaDisabled" :tabindex="tabIndex" @click="onOptionSelected(BentoListboxEvent.SELECT)" @enter-pressed="onOptionSelected(BentoListboxEvent.ENTER)" @space-pressed.prevent="onOptionSelected(BentoListboxEvent.SPACE)" @tab-pressed="onOptionSelected(BentoListboxEvent.TAB)" > <slot v-bind="option"> <span class="b-listbox-single-select-option__text"> <bento-typography el="span">{{ option.label }}</bento-typography> <bento-typography v-if="option.description" class="b-listbox-single-select-option__description" el="span" > {{ option.description }} </bento-typography> </span> </slot> <span class="b-listbox-single-select-option__check-icon"> <checkmark-icon v-show="selected" svg-title="selected" /> </span> </bento-listbox-option> </template> <script lang="ts"> import { computed, defineComponent, type PropType } from 'vue'; import { BentoTypography } from '@/components/typography'; import { BentoListboxOption } from '../listbox-option'; import { BentoListboxEvent, type BentoListboxOptionItem } from '@/types/listbox'; import CheckmarkIcon from '@adyen/ui-assets-icons-16/vue/checkmark'; /** * Listbox option element. * * @example * <bento-listbox-single-select-option * v-for="option in options" * :key="option.value" * :option="option" * :selected="option.selected" * @select="onOptionSelected" * /> */ export default defineComponent({ name: 'bento-listbox-single-select-option', components: { BentoListboxOption, BentoTypography, CheckmarkIcon, }, props: { /** * Indicates if the option is active. */ active: { type: Boolean, default: false }, /** * Setting this property to true will disable the option. */ disabled: { type: Boolean, default: false }, /** * The option item that contains all the option item's data. * @type {BentoListboxOptionItem} */ option: { type: Object as PropType<BentoListboxOptionItem>, required: true, }, /** * Indicates if the value is selected. Only one element can be selected at a time. */ selected: { type: Boolean, default: false }, }, emits: [BentoListboxEvent.ENTER, BentoListboxEvent.SELECT, BentoListboxEvent.SPACE, BentoListboxEvent.TAB], setup(props, { emit }) { const onOptionSelected = eventName => { if (props.disabled) { return; } /** * Trigerred when an option is clicked or selected via keyboard navigation. * Indicates which option was selected. * * @event {string} - The corresponding event * @property {number} selectedValue - Selected option's value */ emit(eventName, props.option); }; const ariaDisabled = computed(() => (props.disabled ? true : null)); const ariaSelected = computed(() => (props.selected ? true : null)); const tabIndex = computed(() => (props.disabled ? -1 : 0)); return { // Values ariaDisabled, ariaSelected, tabIndex, // Enums BentoListboxEvent, // Events onOptionSelected, }; }, }); </script> <style lang="scss" scoped src="./listbox-single-select-option.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div :id="id" ref="listboxRef" v-bento-keyboard-navigation-directive class="b-listbox" tabindex="-1" v-bind="listboxContainerAttributes" :style="containerProps?.style" data-testid="listbox-container" @scroll="onListboxContainerScroll" > <div v-bind="wrapperProps"> <!-- Loading filtered items loading --> <div v-if="componentLoading" class="b-listbox__loading-filtered-items"> <bento-loading-indicator /> </div> <!-- Empty search message --> <div v-else-if="isSearchResultEmpty" class="b-listbox__empty-search-message"> <bento-empty-state v-if="emptyState" v-bind="limitedEmptyStateProps" /> <bento-typography v-else el="span">{{ noResultsMessage }}</bento-typography> </div> <template v-else> <!-- Multi select --> <template v-if="multiple"> <bento-listbox-multi-select ref="listboxItemRef" :has-more-items="hasMoreItems" v-bind="ariaAttributes" :items="items" :lazy-load-type="lazyLoadType" :static-categories="staticCategories" :selected-values="selectedValue" :searching="searching" :is-option-disabled="isOptionDisabled" v-on="listboxListeners" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </bento-listbox-multi-select> </template> <!-- Single select --> <template v-else> <bento-listbox-single-select ref="listboxItemRef" :items="virtualisedItems" :total-item-count="totalItemCount" :scroll-to-index="scrollToIndex" :virtual-scroll-start-index="virtualScrollStartIndex" v-bind="ariaAttributes" :selected-value="selectedValue" :static-categories="staticCategories" :is-option-disabled="isOptionDisabled" v-on="listboxListeners" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </bento-listbox-single-select> </template> <!-- Lazy Loading --> <bento-listbox-lazy-load v-if="lazyLoadType !== 'none'" :has-more-items="hasMoreItems" :loading="loading" :type="lazyLoadType" @show-more="emitShowMore" /> </template> </div> </div> </template> <script lang="ts"> import { computed, defineComponent, type PropType, ref, toRef } from 'vue'; import { type BentoListboxEmptyStateProps, BentoListboxEvent, type BentoListboxIsOptionDisabled, BentoListboxLazyLoadType, type BentoListboxOptionItem, type BentoListboxOptions, type BentoListboxVirtulisationOptions, } from '@/types/listbox'; import { BentoKeyboardNavigationKeyDownEvent } from '@/types/keyboard-navigation'; import { BentoKeyboardNavigationDirective } from '@/directives'; import { BentoListboxSingleSelect } from './components/listbox-single-select'; import { BentoListboxMultiSelect } from './components/listbox-multi-select'; import { BentoTypography } from '@/components/typography'; import { BentoLoadingIndicator } from '@/components/loading-indicator'; import { BentoListboxLazyLoad } from './components/listbox-lazy-load'; import { BentoLoadingButtonState } from '@/components/button'; import { BentoEmptyState } from '@/components/empty-state'; import { debounce } from '@/utils/ts/debounce'; import { useVirtualList } from '@/composables/use-virtual-list'; import type { HTMLAttributes } from 'vue/types/jsx.d.ts'; const DEFAULT_DROPDOWN_ITEM_HEIGHT = 36; /** * Listbox container. * It lists all the available options. * * @example * <bento-listbox * v-slot="{label, value}" * :id="listboxOptionsContainerId" * :aria-label="ariaLabel" * :disabled="disabled" * :multiple="multiple" * :selected="value" * :isOptionDisabled="option => option.value === 2" * :items="[{ * { label: 'Option 1', value: 1 }, * { label: 'Option 2', value: 2 }, * }]" * @select="onOptionSelected" * > * </bento-listbox> */ export default defineComponent({ name: 'bento-listbox', components: { BentoEmptyState, BentoListboxSingleSelect, BentoListboxMultiSelect, BentoTypography, BentoLoadingIndicator, BentoListboxLazyLoad, }, directives: { BentoKeyboardNavigationDirective }, props: { /** * Defines a string value that labels an interactive element. */ ariaLabel: { type: String, default: null }, /** * Empty state props that will be used with the inner empty state component to be displayed when no search results are found. */ emptyState: { type: Object as PropType<BentoListboxEmptyStateProps>, default: undefined, }, /** * Indicates whether there are more items to load */ hasMoreItems: { type: Boolean, default: false }, /** * Identifies the listbox whose contents are controlled by the the combobox on which the aria-controls attribute is set. */ id: { type: String, required: true }, /** * Function that allows the options to be disabled * * @type {BentoListboxIsOptionDisabled} * @param {BentoListboxOptionItem} option - Listbox option object * @param {string} option.label - Option's label text * @param {string|number} option.value - Option's value * @param {unknown} option.data - Option's extra data to be passed to the default slot */ isOptionDisabled: { type: Function as PropType<BentoListboxIsOptionDisabled>, default: () => undefined, }, /** * The option elements to populate the listbox with. * It must be an array of {@see BentoListboxOptionItem } * * @property {string} value.label - Text to be displayed in the option * * @property {string,number} value.value - Value of the option */ items: { type: Array as PropType<BentoListboxOptions>, required: true, }, /** * Indicates if new options are lazy loading. */ loading: { type: Boolean, default: false }, /** * The type of `Lazy Load`. * Type "automatic" will enable infinite scrolling * Type "button" will enable lazy loading with "Show more" button */ lazyLoadType: { type: String as PropType<BentoListboxLazyLoadType | `${BentoListboxLazyLoadType}`>, default: BentoListboxLazyLoadType.NONE, validator: (value: BentoListboxLazyLoadType) => Object.values(BentoListboxLazyLoadType).includes(value), }, /** * Indicates the listbox is loading filtered items. */ componentLoading: { type: Boolean, default: false }, /** * If enabled, the user will be able to select multiple items. */ multiple: { type: Boolean, default: false }, /** * If enabled, dropdown will display non-selectable, static categories */ staticCategories: { type: Boolean, default: false }, /** * Messaged displayed when no items are listed in the listbox */ noResultsMessage: { type: String, required: true }, /** * Flag that indicates that an external search it's being made * to filter out the items inside the the multi-select listbox * * If enabled, hides the "Select all" checkbox in "multiple" mode. */ searching: BentoListboxMultiSelect.props.searching, /** * The `input` value. * Providing an empty string or empty array will select no options. * Set to an empty string `''` if you don't want any of the available options to be selected for single select */ selectedValue: { type: Array as PropType<BentoListboxOptions>, default: () => [], }, /** * Enables virtual scrolling if set to true or by providing an object with itemHeight function. * The itemHeight function is used to calculate the height of rendered item given it's index. */ virtualScroll: { type: [Boolean, Object] as PropType<BentoListboxVirtulisationOptions>, default: false, }, }, emits: [ BentoListboxEvent.SELECT, BentoListboxEvent.SPACE, BentoListboxEvent.SHOW_MORE, BentoListboxEvent.ENTER, BentoListboxEvent.ESCAPE, BentoListboxEvent.TAB, ], setup(props, { emit, slots, expose }) { const listboxItemRef = ref([]); const items = toRef(props, 'items'); const isLazyLoadListbox = computed(() => props.lazyLoadType !== BentoListboxLazyLoadType.AUTOMATIC); const hasAutomaticLazyLoader = computed(() => props.lazyLoadType === BentoListboxLazyLoadType.AUTOMATIC); // Virtual scrolling const virtualScrollItemHeight = () => { if (typeof props?.virtualScroll === 'object' && props?.virtualScroll?.itemHeight) { return props.virtualScroll.itemHeight; } return DEFAULT_DROPDOWN_ITEM_HEIGHT; }; const isVirtualScrollEnabled = !props?.multiple && props?.virtualScroll !== false && props?.lazyLoadType === 'none'; const { containerRef: listboxRef, containerProps, onScroll: onContainerScroll, list: virtualisedItems, wrapperProps, scrollToIndex, startIndex: virtualScrollStartIndex, } = useVirtualList<BentoListboxOptionItem>(items, { itemHeight: virtualScrollItemHeight(), // Disable virtual scrolling if not enabled or if lazy loading is enabled or if multi-select disabled: !isVirtualScrollEnabled, }); const totalItemCount = computed(() => (isVirtualScrollEnabled ? items.value.length : 0)); // Aria a11y const ariaAtomic = computed<HTMLAttributes['aria-atomic']>(() => props.componentLoading !== undefined || isLazyLoadListbox.value ? 'true' : undefined ); const ariaBusy = computed<HTMLAttributes['aria-busy']>(() => (isLazyLoadListbox.value && props.loading) || props.componentLoading ? 'true' : 'false' ); const ariaLive = computed<HTMLAttributes['aria-live']>(() => isLazyLoadListbox.value || props.componentLoading !== undefined ? 'polite' : undefined ); const ariaAttributes = computed( () => ({ 'aria-label': props.ariaLabel, 'aria-busy': ariaBusy.value, 'aria-atomic': ariaAtomic.value, 'aria-live': ariaLive.value, }) as HTMLAttributes ); const listboxContainerAttributes = computed( () => (props.componentLoading ? { ...ariaAttributes.value, role: 'alert', } : {}) as HTMLAttributes ); const onOptionSelected = eventName => (selectedItem: BentoListboxOptions) => { emit(eventName, selectedItem); }; const emitShowMore = () => { emit(BentoListboxEvent.SHOW_MORE); }; const emitShowMoreOnScrollEnd = debounce(() => { const container = listboxRef.value; if (!container) { return; } const isAtBottom = Math.round(container.scrollTop) + container.clientHeight >= container.scrollHeight; if (props.hasMoreItems && isAtBottom && !props.loading) { emitShowMore(); } }); const onListboxContainerScroll = () => { if (hasAutomaticLazyLoader.value) { emitShowMoreOnScrollEnd(); } else { onContainerScroll(); } }; const isSearchResultEmpty = computed(() => props.items?.length === 0); const shouldDisplayShowMoreButton = computed( () => props.lazyLoadType === BentoListboxLazyLoadType.BUTTON && props.hasMoreItems ); const loadingButtonState = computed(() => props.loading ? BentoLoadingButtonState.LOADING : BentoLoadingButtonState.START ); const limitedEmptyStateProps = computed<InstanceType<typeof BentoEmptyState>['$props']>(() => ({ ...props.emptyState, variant: 'condensed', })); const listboxListeners = { [BentoListboxEvent.SELECT]: onOptionSelected(BentoListboxEvent.SELECT), [BentoListboxEvent.SPACE]: onOptionSelected(BentoListboxEvent.SPACE), [BentoListboxEvent.TAB]: onOptionSelected(BentoListboxEvent.TAB), [BentoListboxEvent.ENTER]: onOptionSelected(BentoListboxEvent.ENTER), }; expose({ listboxRef, listboxItemRef }); return { // Refs listboxRef, listboxItemRef, // a11y ariaAttributes, listboxContainerAttributes, // Values containerProps, isSearchResultEmpty, limitedEmptyStateProps, loadingButtonState, slots, shouldDisplayShowMoreButton, virtualisedItems, wrapperProps, // Virtual scroll totalItemCount, scrollToIndex, virtualScrollStartIndex, // Enums BentoKeyboardNavigationKeyDownEvent, BentoListboxEvent, // Methods onListboxContainerScroll, onOptionSelected, emitShowMore, listboxListeners, }; }, }); </script> <style lang="scss" scoped src="./listbox.scss" />
|
|
1
|
+
<template> <div :id="id" ref="listboxRef" v-bento-keyboard-navigation-directive class="b-listbox" tabindex="-1" v-bind="listboxContainerAttributes" :style="containerProps?.style" data-testid="listbox-container" @scroll="onListboxContainerScroll" > <div v-bind="wrapperProps"> <!-- Loading filtered items loading --> <div v-if="componentLoading" class="b-listbox__loading-filtered-items"> <bento-loading-indicator /> </div> <!-- Empty search message --> <div v-else-if="isSearchResultEmpty" class="b-listbox__empty-search-message"> <bento-empty-state v-if="emptyState" v-bind="limitedEmptyStateProps" /> <bento-typography v-else el="span">{{ noResultsMessage }}</bento-typography> </div> <template v-else> <!-- Multi select --> <template v-if="multiple"> <bento-listbox-multi-select ref="listboxItemRef" :has-more-items="hasMoreItems" v-bind="ariaAttributes" :items="items" :lazy-load-type="lazyLoadType" :static-categories="staticCategories" :selected-values="selectedValue" :searching="searching" :is-option-disabled="isOptionDisabled" v-on="listboxListeners" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </bento-listbox-multi-select> </template> <!-- Single select --> <template v-else> <bento-listbox-single-select ref="listboxItemRef" :active-descendant-id="activeDescendantId" :items="virtualisedItems" :option-id-base="id" :total-item-count="totalItemCount" :scroll-to-index="scrollToIndex" :virtual-scroll-start-index="virtualScrollStartIndex" v-bind="ariaAttributes" :selected-value="selectedValue" :static-categories="staticCategories" :is-option-disabled="isOptionDisabled" v-on="listboxListeners" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </bento-listbox-single-select> </template> <!-- Lazy Loading --> <bento-listbox-lazy-load v-if="lazyLoadType !== 'none'" :has-more-items="hasMoreItems" :loading="loading" :type="lazyLoadType" @show-more="emitShowMore" /> </template> </div> </div> </template> <script lang="ts"> import { computed, defineComponent, type PropType, ref, toRef } from 'vue'; import { type BentoListboxEmptyStateProps, BentoListboxEvent, type BentoListboxIsOptionDisabled, BentoListboxLazyLoadType, type BentoListboxOptionItem, type BentoListboxOptions, type BentoListboxVirtulisationOptions, } from '@/types/listbox'; import { BentoKeyboardNavigationKeyDownEvent } from '@/types/keyboard-navigation'; import { BentoKeyboardNavigationDirective } from '@/directives'; import { BentoListboxSingleSelect } from './components/listbox-single-select'; import { BentoListboxMultiSelect } from './components/listbox-multi-select'; import { BentoTypography } from '@/components/typography'; import { BentoLoadingIndicator } from '@/components/loading-indicator'; import { BentoListboxLazyLoad } from './components/listbox-lazy-load'; import { BentoLoadingButtonState } from '@/components/button'; import { BentoEmptyState } from '@/components/empty-state'; import { debounce } from '@/utils/ts/debounce'; import { useVirtualList } from '@/composables/use-virtual-list'; import type { HTMLAttributes } from 'vue/types/jsx.d.ts'; const DEFAULT_DROPDOWN_ITEM_HEIGHT = 36; /** * Listbox container. * It lists all the available options. * * @example * <bento-listbox * v-slot="{label, value}" * :id="listboxOptionsContainerId" * :aria-label="ariaLabel" * :disabled="disabled" * :multiple="multiple" * :selected="value" * :isOptionDisabled="option => option.value === 2" * :items="[{ * { label: 'Option 1', value: 1 }, * { label: 'Option 2', value: 2 }, * }]" * @select="onOptionSelected" * > * </bento-listbox> */ export default defineComponent({ name: 'bento-listbox', components: { BentoEmptyState, BentoListboxSingleSelect, BentoListboxMultiSelect, BentoTypography, BentoLoadingIndicator, BentoListboxLazyLoad, }, directives: { BentoKeyboardNavigationDirective }, props: { /** * Defines a string value that labels an interactive element. */ ariaLabel: { type: String, default: null }, /** * Identifies the currently active option in the listbox. */ activeDescendantId: { type: String, default: null }, /** * Empty state props that will be used with the inner empty state component to be displayed when no search results are found. */ emptyState: { type: Object as PropType<BentoListboxEmptyStateProps>, default: undefined, }, /** * Indicates whether there are more items to load */ hasMoreItems: { type: Boolean, default: false }, /** * Identifies the listbox whose contents are controlled by the the combobox on which the aria-controls attribute is set. */ id: { type: String, required: true }, /** * Function that allows the options to be disabled * * @type {BentoListboxIsOptionDisabled} * @param {BentoListboxOptionItem} option - Listbox option object * @param {string} option.label - Option's label text * @param {string|number} option.value - Option's value * @param {unknown} option.data - Option's extra data to be passed to the default slot */ isOptionDisabled: { type: Function as PropType<BentoListboxIsOptionDisabled>, default: () => undefined, }, /** * The option elements to populate the listbox with. * It must be an array of {@see BentoListboxOptionItem } * * @property {string} value.label - Text to be displayed in the option * * @property {string,number} value.value - Value of the option */ items: { type: Array as PropType<BentoListboxOptions>, required: true, }, /** * Indicates if new options are lazy loading. */ loading: { type: Boolean, default: false }, /** * The type of `Lazy Load`. * Type "automatic" will enable infinite scrolling * Type "button" will enable lazy loading with "Show more" button */ lazyLoadType: { type: String as PropType<BentoListboxLazyLoadType | `${BentoListboxLazyLoadType}`>, default: BentoListboxLazyLoadType.NONE, validator: (value: BentoListboxLazyLoadType) => Object.values(BentoListboxLazyLoadType).includes(value), }, /** * Indicates the listbox is loading filtered items. */ componentLoading: { type: Boolean, default: false }, /** * If enabled, the user will be able to select multiple items. */ multiple: { type: Boolean, default: false }, /** * If enabled, dropdown will display non-selectable, static categories */ staticCategories: { type: Boolean, default: false }, /** * Messaged displayed when no items are listed in the listbox */ noResultsMessage: { type: String, required: true }, /** * Flag that indicates that an external search it's being made * to filter out the items inside the the multi-select listbox * * If enabled, hides the "Select all" checkbox in "multiple" mode. */ searching: BentoListboxMultiSelect.props.searching, /** * The `input` value. * Providing an empty string or empty array will select no options. * Set to an empty string `''` if you don't want any of the available options to be selected for single select */ selectedValue: { type: Array as PropType<BentoListboxOptions>, default: () => [], }, /** * Enables virtual scrolling if set to true or by providing an object with itemHeight function. * The itemHeight function is used to calculate the height of rendered item given it's index. */ virtualScroll: { type: [Boolean, Object] as PropType<BentoListboxVirtulisationOptions>, default: false, }, }, emits: [ BentoListboxEvent.SELECT, BentoListboxEvent.SPACE, BentoListboxEvent.SHOW_MORE, BentoListboxEvent.ENTER, BentoListboxEvent.ESCAPE, BentoListboxEvent.TAB, ], setup(props, { emit, slots, expose }) { const listboxItemRef = ref([]); const items = toRef(props, 'items'); const isLazyLoadListbox = computed(() => props.lazyLoadType !== BentoListboxLazyLoadType.AUTOMATIC); const hasAutomaticLazyLoader = computed(() => props.lazyLoadType === BentoListboxLazyLoadType.AUTOMATIC); // Virtual scrolling const virtualScrollItemHeight = () => { if (typeof props?.virtualScroll === 'object' && props?.virtualScroll?.itemHeight) { return props.virtualScroll.itemHeight; } return DEFAULT_DROPDOWN_ITEM_HEIGHT; }; const isVirtualScrollEnabled = !props?.multiple && props?.virtualScroll !== false && props?.lazyLoadType === 'none'; const { containerRef: listboxRef, containerProps, onScroll: onContainerScroll, list: virtualisedItems, wrapperProps, scrollToIndex, startIndex: virtualScrollStartIndex, } = useVirtualList<BentoListboxOptionItem>(items, { itemHeight: virtualScrollItemHeight(), // Disable virtual scrolling if not enabled or if lazy loading is enabled or if multi-select disabled: !isVirtualScrollEnabled, }); const totalItemCount = computed(() => (isVirtualScrollEnabled ? items.value.length : 0)); // Aria a11y const ariaAtomic = computed<HTMLAttributes['aria-atomic']>(() => props.componentLoading !== undefined || isLazyLoadListbox.value ? 'true' : undefined ); const ariaBusy = computed<HTMLAttributes['aria-busy']>(() => (isLazyLoadListbox.value && props.loading) || props.componentLoading ? 'true' : 'false' ); const ariaLive = computed<HTMLAttributes['aria-live']>(() => isLazyLoadListbox.value || props.componentLoading !== undefined ? 'polite' : undefined ); const ariaAttributes = computed( () => ({ 'aria-label': props.ariaLabel, 'aria-busy': ariaBusy.value, 'aria-atomic': ariaAtomic.value, 'aria-live': ariaLive.value, }) as HTMLAttributes ); const listboxContainerAttributes = computed( () => (props.componentLoading ? { ...ariaAttributes.value, role: 'alert', } : {}) as HTMLAttributes ); const onOptionSelected = eventName => (selectedItem: BentoListboxOptions) => { emit(eventName, selectedItem); }; const emitShowMore = () => { emit(BentoListboxEvent.SHOW_MORE); }; const emitShowMoreOnScrollEnd = debounce(() => { const container = listboxRef.value; if (!container) { return; } const isAtBottom = Math.round(container.scrollTop) + container.clientHeight >= container.scrollHeight; if (props.hasMoreItems && isAtBottom && !props.loading) { emitShowMore(); } }); const onListboxContainerScroll = () => { if (hasAutomaticLazyLoader.value) { emitShowMoreOnScrollEnd(); } else { onContainerScroll(); } }; const isSearchResultEmpty = computed(() => props.items?.length === 0); const shouldDisplayShowMoreButton = computed( () => props.lazyLoadType === BentoListboxLazyLoadType.BUTTON && props.hasMoreItems ); const loadingButtonState = computed(() => props.loading ? BentoLoadingButtonState.LOADING : BentoLoadingButtonState.START ); const limitedEmptyStateProps = computed<InstanceType<typeof BentoEmptyState>['$props']>(() => ({ ...props.emptyState, variant: 'condensed', })); const listboxListeners = { [BentoListboxEvent.SELECT]: onOptionSelected(BentoListboxEvent.SELECT), [BentoListboxEvent.SPACE]: onOptionSelected(BentoListboxEvent.SPACE), [BentoListboxEvent.TAB]: onOptionSelected(BentoListboxEvent.TAB), [BentoListboxEvent.ENTER]: onOptionSelected(BentoListboxEvent.ENTER), }; expose({ listboxRef, listboxItemRef }); return { // Refs listboxRef, listboxItemRef, // a11y ariaAttributes, listboxContainerAttributes, // Values containerProps, isSearchResultEmpty, limitedEmptyStateProps, loadingButtonState, slots, shouldDisplayShowMoreButton, virtualisedItems, wrapperProps, // Virtual scroll totalItemCount, scrollToIndex, virtualScrollStartIndex, // Enums BentoKeyboardNavigationKeyDownEvent, BentoListboxEvent, // Methods onListboxContainerScroll, onOptionSelected, emitShowMore, listboxListeners, }; }, }); </script> <style lang="scss" scoped src="./listbox.scss" />
|
package/dist/assets/components/pagination/components/pagination-controls/pagination-controls.vue
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <ul class="b-pagination-controls"> <li class="b-pagination-controls__item"> <!-- Navigate to the first page --> <bento-button :disabled="isCurrentPageFirst" variant="tertiary" condensed @click="navigate(1)"> <template #iconLeft> <skip-left :svg-title="t('navigateToTheFirstPage')" /> </template> </bento-button> </li> <li class="b-pagination-controls__item"> <!-- Navigate to the previous page --> <bento-button :disabled="isCurrentPageFirst" variant="tertiary" condensed @click="navigate(page - 1)"> <template #iconLeft> <chevron-left :svg-title="t('navigateToThePreviousPage')" /> </template> </bento-button> </li> <li class="b-pagination-controls__item"> <!-- Navigate to the next page --> <bento-button :disabled="isNextButtonDisabled" variant="tertiary" condensed @click="navigate(page + 1)"> <template #iconLeft> <chevron-right :svg-title="t('navigateToTheNextPage')" /> </template> </bento-button> </li> <li class="b-pagination-controls__item"> <!-- Navigate to the last page --> <bento-button :disabled="isLastPageButtonDisabled" variant="tertiary" condensed @click="navigate(totalPages)" > <template #iconLeft> <skip-right :svg-title="t('navigateToTheLastPage')" /> </template> </bento-button> </li> </ul> </template> <script setup lang="ts"> import { computed } from 'vue'; import { BentoButton } from '@/components/button'; import { useI18n } from '@/utils/ts/i18n'; import SkipLeft from '@adyen/ui-assets-icons-16/vue/skip-left'; import SkipRight from '@adyen/ui-assets-icons-16/vue/skip-right'; import ChevronLeft from '@adyen/ui-assets-icons-16/vue/chevron-left-small'; import ChevronRight from '@adyen/ui-assets-icons-16/vue/chevron-right-small'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const props = withDefaults( defineProps<{ /** * Use to determine if the pager has a "next page". */ hasNext?: boolean; /** * The current page number of the pager. */ page: number; /** * The current page number of the pager. */ totalPages?: number; }>(), { hasNext: null, totalPages: null, } ); const emit = defineEmits<{ (e: 'navigate', value: number); }>(); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const isCurrentPageFirst = computed(() => props.page === 1); const isCurrentPageLast = computed(() => props.page === props.totalPages); const isNextButtonDisabled = computed(() => { const hasNoNextPage = !props.hasNext; return hasNoNextPage || isCurrentPageLast.value; }); const isLastPageButtonDisabled = computed(() => { const hasNoNextPage = !props.hasNext; const hasNoTotalPagesNumber = !props.totalPages; return hasNoNextPage || isCurrentPageLast.value || hasNoTotalPagesNumber; }); const navigate = (pageArg: number) => { emit('navigate', pageArg); }; </script> <script lang="ts"> export default { i18n: { messages }, }; </script> <style lang="scss" scoped src="./pagination-controls.scss" />
|
|
1
|
+
<template> <ul class="b-pagination-controls"> <li v-if="!hideFirstLastPageButtons" key="first" class="b-pagination-controls__item"> <!-- Navigate to the first page --> <bento-button :disabled="isCurrentPageFirst" variant="tertiary" condensed @click="navigate(1)"> <template #iconLeft> <skip-left :svg-title="t('navigateToTheFirstPage')" /> </template> </bento-button> </li> <li key="previous" class="b-pagination-controls__item"> <!-- Navigate to the previous page --> <bento-button :disabled="isCurrentPageFirst" variant="tertiary" condensed @click="navigate(page - 1)"> <template #iconLeft> <chevron-left :svg-title="t('navigateToThePreviousPage')" /> </template> </bento-button> </li> <li key="next" class="b-pagination-controls__item"> <!-- Navigate to the next page --> <bento-button :disabled="isNextButtonDisabled" variant="tertiary" condensed @click="navigate(page + 1)"> <template #iconLeft> <chevron-right :svg-title="t('navigateToTheNextPage')" /> </template> </bento-button> </li> <li v-if="!hideFirstLastPageButtons" key="last" class="b-pagination-controls__item"> <!-- Navigate to the last page --> <bento-button :disabled="isLastPageButtonDisabled" variant="tertiary" condensed @click="navigate(totalPages)" > <template #iconLeft> <skip-right :svg-title="t('navigateToTheLastPage')" /> </template> </bento-button> </li> </ul> </template> <script setup lang="ts"> import { computed } from 'vue'; import { BentoButton } from '@/components/button'; import { useI18n } from '@/utils/ts/i18n'; import SkipLeft from '@adyen/ui-assets-icons-16/vue/skip-left'; import SkipRight from '@adyen/ui-assets-icons-16/vue/skip-right'; import ChevronLeft from '@adyen/ui-assets-icons-16/vue/chevron-left-small'; import ChevronRight from '@adyen/ui-assets-icons-16/vue/chevron-right-small'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const props = withDefaults( defineProps<{ /** * Use to determine if the pager has a "next page". */ hasNext?: boolean; /** * Hides the "navigate to the first page" and "navigate to the last page" buttons. * Useful for cursor-based pagination, where jumping directly to the first or last page is not supported. */ hideFirstLastPageButtons?: boolean; /** * The current page number of the pager. */ page: number; /** * The current page number of the pager. */ totalPages?: number; }>(), { hasNext: null, hideFirstLastPageButtons: false, totalPages: null, } ); const emit = defineEmits<{ (e: 'navigate', value: number); }>(); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const isCurrentPageFirst = computed(() => props.page === 1); const isCurrentPageLast = computed(() => props.page === props.totalPages); const isNextButtonDisabled = computed(() => { const hasNoNextPage = !props.hasNext; return hasNoNextPage || isCurrentPageLast.value; }); const isLastPageButtonDisabled = computed(() => { const hasNoNextPage = !props.hasNext; const hasNoTotalPagesNumber = !props.totalPages; return hasNoNextPage || isCurrentPageLast.value || hasNoTotalPagesNumber; }); const navigate = (pageArg: number) => { emit('navigate', pageArg); }; </script> <script lang="ts"> export default { i18n: { messages }, }; </script> <style lang="scss" scoped src="./pagination-controls.scss" />
|
|
@@ -15,9 +15,9 @@ page. It is also internally used in the data grid component.
|
|
|
15
15
|
Use a pagination component when there are too many results to show on one page. What constitutes “too many results”
|
|
16
16
|
varies case by case and can be influenced by:
|
|
17
17
|
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
18
|
+
- System load times
|
|
19
|
+
- Amount of data in each entry
|
|
20
|
+
- Screen space So speak to your product designer and decide accordingly based on your use case.
|
|
21
21
|
|
|
22
22
|
### Customize your pagination
|
|
23
23
|
|
|
@@ -53,6 +53,12 @@ using the arrows. There is also the possibility to go to the first and the last
|
|
|
53
53
|
it’s not possible to go further back or forth, the respective arrows get disabled. In case there is only one page to
|
|
54
54
|
display, all arrows are disabled and there is no dropdown shown.
|
|
55
55
|
|
|
56
|
+
##### Hide First/Last Page Buttons
|
|
57
|
+
|
|
58
|
+
You can hide the "navigate to the first page" and "navigate to the last page" buttons by setting
|
|
59
|
+
`hideFirstLastPageButtons` to `true`. This is useful for cursor-based pagination, where jumping directly to the first or
|
|
60
|
+
last page is not supported.
|
|
61
|
+
|
|
56
62
|
#### Setting the current visible page
|
|
57
63
|
|
|
58
64
|
You can set the page to display when the component is mounted by passing an appropriate `page` value. Note that you need
|
|
@@ -62,10 +68,10 @@ to set this value accordingly given the range of valid values you have based on
|
|
|
62
68
|
|
|
63
69
|
When using the pagination component, do not:
|
|
64
70
|
|
|
65
|
-
-
|
|
66
|
-
|
|
67
|
-
-
|
|
68
|
-
|
|
71
|
+
- Set it with a `hasNext` property if there is only one page, as this will display an active forward arrow which won't
|
|
72
|
+
do anything when clicked upon.
|
|
73
|
+
- Set it with a current page value which is not equal or greater than one and is greater than the maximum value allowed
|
|
74
|
+
for your use case.
|
|
69
75
|
|
|
70
76
|
## Variations
|
|
71
77
|
|
|
@@ -111,6 +117,6 @@ it yet.
|
|
|
111
117
|
|
|
112
118
|
## Resources
|
|
113
119
|
|
|
114
|
-
-
|
|
115
|
-
-
|
|
116
|
-
-
|
|
120
|
+
- [Figma link](https://www.figma.com/file/uLabwF3243jdMDsNSP7U9I/Bento---Components?node-id=1951-30261&t=NHLd9msx8lGq3NNF-0)
|
|
121
|
+
- [WAI-ARIA menubar pattern](https://www.w3.org/WAI/ARIA/apg/patterns/menubar/)
|
|
122
|
+
- [W3C accessible pagination](https://design-system.w3.org/components/pagination.html)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export interface BentoPaginationProps { /** * A brief description of the purpose of the navigation (for a11y). * Omit the term "navigation", as the screen reader will read both the role and the contents of the label. * @deprecated Since v2.0.0. Use `aria-label` instead. */ ariaLabel?: string; /** * Use to determine if the pager has a "next page". */ hasNext?: boolean; /** * Toggles the visibility of the "results per page" part of the pagination component. */ hidePageSize?: boolean; /** * 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; /* * 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. * This prop only takes effect if `hidePageSize` is `false`. */ hidePageSizeSelection?: boolean; /** * The current page number of the pager. */ page?: number; /** * The size of the items the pager is paging through. */ size?: number; /** * The total number of items the pager is paging through. */ totalCount?: number; /** * Enables virtual scrolling on all dropdowns if set to true. */ virtualScroll?: boolean; /** * The predefined list of numeric options available in the 'results per page' selection dropdown. */ pageSizeItems?: Array<number>; }
|
|
1
|
+
export interface BentoPaginationProps { /** * A brief description of the purpose of the navigation (for a11y). * Omit the term "navigation", as the screen reader will read both the role and the contents of the label. * @deprecated Since v2.0.0. Use `aria-label` instead. */ ariaLabel?: string; /** * Use to determine if the pager has a "next page". */ hasNext?: boolean; /** * Hides the "navigate to the first page" and "navigate to the last page" buttons. * Useful for cursor-based pagination, where jumping directly to the first or last page is not supported. */ hideFirstLastPageButtons?: boolean; /** * Toggles the visibility of the "results per page" part of the pagination component. */ hidePageSize?: boolean; /** * 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; /* * 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. * This prop only takes effect if `hidePageSize` is `false`. */ hidePageSizeSelection?: boolean; /** * The current page number of the pager. */ page?: number; /** * The size of the items the pager is paging through. */ size?: number; /** * The total number of items the pager is paging through. */ totalCount?: number; /** * Enables virtual scrolling on all dropdowns if set to true. */ virtualScroll?: boolean; /** * The predefined list of numeric options available in the 'results per page' selection dropdown. */ pageSizeItems?: Array<number>; }
|
|
@@ -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" :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" />
|
|
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" :hide-first-last-page-buttons="hideFirstLastPageButtons" :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, hideFirstLastPageButtons: false, 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" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <nav class="b-table-of-contents" :aria-labelledby="titleId"> <bento-typography :id="titleId" el="h2" variant="body" stronger class="b-table-of-contents__header">{{ computedTitle }}</bento-typography> <table-of-contents-list :items="items" :active-section-href="activeSectionHref" :initial-focus-index="controlledActiveIndex" :scroll-offset="scrollOffset" /> </nav> </template> <script setup lang="ts"> import { computed } from 'vue'; import { BentoTypography } from '@/components/typography'; import { TableOfContentsList } from './components'; import { generateUid } from '@/core/utils/ts'; import { useScrollSpy } from '@/composables/use-scroll-spy'; import { getElementFragmentHref, type ScrollToSectionTarget } from '@/utils/ts/scroll-to-section'; import { unrefElement } from '@/directives/click-outside/utils'; import { useI18n } from '@/utils/ts/i18n'; import messages from './messages.json'; import type { BentoTableOfContentsItem, BentoTableOfContentsProps } from './table-of-contents.types'; type MessageSchema = (typeof messages)['en-US']; const { t } = useI18n<{ message: MessageSchema }>({ messages }); const props = withDefaults(defineProps<BentoTableOfContentsProps>(), { activeIndex: undefined, scrollOffset: undefined, title: undefined, }); const titleId = generateUid('table-of-contents-title'); const computedTitle = computed(() => props.title ?? t('onThisPage')); const resolvedItems = computed(() => props.items .map((item, originalIndex) => ({ element: unrefElement(item.elementRef), originalIndex, })) .filter((item): item is { element: Element; originalIndex: number } => !!item.element) ); const sectionTargetElements = computed(() => resolvedItems.value.map(item => item.element)); const controlledActiveIndex = computed(() => props.activeIndex ?? -1); const scrollSpyDefaultIndex = computed(() => props.activeIndex === undefined ? -1 : resolvedItems.value.findIndex(({ originalIndex }) => originalIndex === props.activeIndex) ); const getSectionFragmentHref = (elementRef?: BentoTableOfContentsItem['elementRef']) => getElementFragmentHref(elementRef as ScrollToSectionTarget); const { activeIndex: scrollSpyActiveIndex } = useScrollSpy(sectionTargetElements, { default: scrollSpyDefaultIndex, }); const currentActiveIndex = computed(() => { if (props.activeIndex !== undefined) { return props.activeIndex; } return resolvedItems.value[scrollSpyActiveIndex.value]?.originalIndex ?? -1; }); const activeSectionHref = computed(() => getSectionFragmentHref(props.items[currentActiveIndex.value]?.elementRef)); </script> <script lang="ts"> /** * This is a navigational component that serves as a list outlining * the structure and hierarchy of content on a page. It allows users * to find the content in a page more quickly with anchor links in * the content. * * ARIA: https://www.w3.org/TR/dpub-aria-1.1/#doc-toc * * @usage * import { BentoTableOfContents } from '@adyen/bento-vue2'; * * export default { * components: { BentoTableOfContents }, * template: ` * <bento-table-of-contents * :items="[{ title: 'Section text', elementRef: sectionRef }]" * /> * `, * setup() { * const sectionRef = ref(null); * return { * sectionRef, * } * } * } */ export default { name: 'bento-table-of-contents', i18n: { messages }, }; </script> <style lang="scss" scoped src="./table-of-contents.scss" />
|
|
1
|
+
<template> <nav class="b-table-of-contents" :aria-labelledby="titleId"> <bento-typography :id="titleId" el="h2" variant="body" stronger class="b-table-of-contents__header">{{ computedTitle }}</bento-typography> <table-of-contents-list :items="items" :active-section-href="activeSectionHref" :initial-focus-index="controlledActiveIndex" :scroll-offset="scrollOffset" /> </nav> </template> <script setup lang="ts"> import { computed } from 'vue'; import { BentoTypography } from '@/components/typography'; import { TableOfContentsList } from './components'; import { generateUid } from '@/core/utils/ts'; import { useScrollSpy } from '@/composables/use-scroll-spy'; import { getElementFragmentHref, type ScrollToSectionTarget } from '@/utils/ts/scroll-to-section'; import { unrefElement } from '@/directives/click-outside/utils'; import { useI18n } from '@/utils/ts/i18n'; import messages from './messages.json'; import type { BentoTableOfContentsItem, BentoTableOfContentsProps } from './table-of-contents.types'; type MessageSchema = (typeof messages)['en-US']; const { t } = useI18n<{ message: MessageSchema }>({ messages }); const props = withDefaults(defineProps<BentoTableOfContentsProps>(), { activeIndex: undefined, scrollOffset: undefined, title: undefined, }); const titleId = generateUid('table-of-contents-title'); const computedTitle = computed(() => props.title ?? t('onThisPage')); const resolvedItems = computed(() => props.items .map((item, originalIndex) => ({ element: unrefElement(item.elementRef), originalIndex, })) .filter((item): item is { element: Element; originalIndex: number } => !!item.element) ); const sectionTargetElements = computed(() => resolvedItems.value.map(item => item.element)); const controlledActiveIndex = computed(() => props.activeIndex ?? -1); const scrollSpyDefaultIndex = computed(() => props.activeIndex === undefined ? -1 : resolvedItems.value.findIndex(({ originalIndex }) => originalIndex === props.activeIndex) ); const getSectionFragmentHref = (elementRef?: BentoTableOfContentsItem['elementRef']) => getElementFragmentHref(elementRef as ScrollToSectionTarget); const { activeIndex: scrollSpyActiveIndex } = useScrollSpy(sectionTargetElements, { default: scrollSpyDefaultIndex, debounce: 300, }); const currentActiveIndex = computed(() => { if (props.activeIndex !== undefined) { return props.activeIndex; } return resolvedItems.value[scrollSpyActiveIndex.value]?.originalIndex ?? -1; }); const activeSectionHref = computed(() => getSectionFragmentHref(props.items[currentActiveIndex.value]?.elementRef)); </script> <script lang="ts"> /** * This is a navigational component that serves as a list outlining * the structure and hierarchy of content on a page. It allows users * to find the content in a page more quickly with anchor links in * the content. * * ARIA: https://www.w3.org/TR/dpub-aria-1.1/#doc-toc * * @usage * import { BentoTableOfContents } from '@adyen/bento-vue2'; * * export default { * components: { BentoTableOfContents }, * template: ` * <bento-table-of-contents * :items="[{ title: 'Section text', elementRef: sectionRef }]" * /> * `, * setup() { * const sectionRef = ref(null); * return { * sectionRef, * } * } * } */ export default { name: 'bento-table-of-contents', i18n: { messages }, }; </script> <style lang="scss" scoped src="./table-of-contents.scss" />
|
|
@@ -14,15 +14,15 @@ and the component without having to write a single line of font styling CSS code
|
|
|
14
14
|
|
|
15
15
|
Here are some examples:
|
|
16
16
|
|
|
17
|
-
-
|
|
18
|
-
-
|
|
17
|
+
- Writing an article for a blog.
|
|
18
|
+
- A sub-title for a section on a web page.
|
|
19
19
|
|
|
20
20
|
### Do not use
|
|
21
21
|
|
|
22
22
|
Here are some examples:
|
|
23
23
|
|
|
24
|
-
-
|
|
25
|
-
-
|
|
24
|
+
- Don't use to replace a `label` instead wrap the label around this component.
|
|
25
|
+
- Don't use inside a button component's `default` slot.
|
|
26
26
|
|
|
27
27
|
## Variations
|
|
28
28
|
|
|
@@ -43,6 +43,10 @@ A bolder caption.
|
|
|
43
43
|
|
|
44
44
|
A caption with more letter spacing.
|
|
45
45
|
|
|
46
|
+
##### Monospace
|
|
47
|
+
|
|
48
|
+
A caption with a monospace font.
|
|
49
|
+
|
|
46
50
|
### Body
|
|
47
51
|
|
|
48
52
|
<Canvas of={TypographyStories.Body} />
|
|
@@ -63,6 +67,10 @@ The most emboldened body text.
|
|
|
63
67
|
|
|
64
68
|
A caption with more letter spacing.
|
|
65
69
|
|
|
70
|
+
##### Monospace
|
|
71
|
+
|
|
72
|
+
Body text with a monospace font.
|
|
73
|
+
|
|
66
74
|
### Title
|
|
67
75
|
|
|
68
76
|
Text used for a headline of a section of content or web page.
|
|
@@ -79,6 +87,10 @@ A larger (than default) title.
|
|
|
79
87
|
|
|
80
88
|
The largest title text.
|
|
81
89
|
|
|
90
|
+
##### Monospace
|
|
91
|
+
|
|
92
|
+
A title with a monospace font.
|
|
93
|
+
|
|
82
94
|
##### ⚠️ Subtitle (deprecated) ⚠️
|
|
83
95
|
|
|
84
96
|
> ⚠️ **Deprecation Notice**: This modifier is deprecated and will be removed in release **2.0.0**. Use the `'title'`
|
|
@@ -109,16 +121,16 @@ The `el` property affects output HTML DOM element that is wrapped around the tex
|
|
|
109
121
|
|
|
110
122
|
It accepts one of the following values:
|
|
111
123
|
|
|
112
|
-
-
|
|
113
|
-
-
|
|
114
|
-
-
|
|
115
|
-
-
|
|
116
|
-
-
|
|
117
|
-
-
|
|
118
|
-
-
|
|
119
|
-
-
|
|
120
|
-
-
|
|
124
|
+
- `'h1'`
|
|
125
|
+
- `'h2'`
|
|
126
|
+
- `'h3'`
|
|
127
|
+
- `'h4'`
|
|
128
|
+
- `'h5'`
|
|
129
|
+
- `'h6'`
|
|
130
|
+
- `'div'`
|
|
131
|
+
- `'paragraph'` (default)
|
|
132
|
+
- `'span'`
|
|
121
133
|
|
|
122
134
|
## Resources
|
|
123
135
|
|
|
124
|
-
-
|
|
136
|
+
- [Figma link](https://www.figma.com/file/Diqr47KAACSr6ohrQhcNgO/Bento---Product---Fundamentals?node-id=1423-55445&t=hpcF61AsBuhZRdWW-0)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
/** * @deprecated Since v2.0.0. Use string literal instead. */ export enum BentoTypographyVariant { CAPTION = 'caption', BODY = 'body', /** @deprecated Since v2.0.0. Use `TITLE` instead. */ SUBTITLE = 'subtitle', TITLE = 'title', } export enum BentoTypographyModifier { WIDE = 'wide', STRONGER = 'stronger', STRONGER_WIDE = 'stronger-wide', STRONGEST = 'strongest', STRONGEST_WIDE = 'strongest-wide', MEDIUM = 'm', LARGE = 'l', MOBILE = 'mobile', } /** * @deprecated Since v2.0.0. Use string literal instead. */ export enum BentoTypographyElement { H1 = 'h1', H2 = 'h2', H3 = 'h3', H4 = 'h4', H5 = 'h5', H6 = 'h6', DIV = 'div', PARAGRAPH = 'p', SPAN = 'span', } export interface BentoTypographyProps { /** * Sets the HTML DOM element. * @values h1, h2, h3, h4, h5, h6, div, p, span */ el?: BentoTypographyElement | `${BentoTypographyElement}`; /** * Adds the large class modifier. * Applicable to the Title variant. */ large?: boolean; /** * Adds the medium class modifier. * Applicable to the Title variant. */ medium?: boolean; /** * Adds the rich-text modifier, which allows for the use of HTML tags inside typography. */ richText?: boolean; /** * Adds the stronger class modifier. * Applicable to Caption, Body and Subtitle variants. */ stronger?: boolean; /** * Adds the strongest class modifier. * Applicable to Caption, Body and Subtitle variants. */ strongest?: boolean; /** * Sets the type of typography variant. * @values caption, body, title. */ variant?: BentoTypographyVariant | `${BentoTypographyVariant}`; /** * Adds the medium class modifier. * Applicable to the Body variant. */ wide?: boolean; }
|
|
1
|
+
/** * @deprecated Since v2.0.0. Use string literal instead. */ export enum BentoTypographyVariant { CAPTION = 'caption', BODY = 'body', /** @deprecated Since v2.0.0. Use `TITLE` instead. */ SUBTITLE = 'subtitle', TITLE = 'title', } export enum BentoTypographyModifier { WIDE = 'wide', STRONGER = 'stronger', STRONGER_WIDE = 'stronger-wide', STRONGEST = 'strongest', STRONGEST_WIDE = 'strongest-wide', MEDIUM = 'm', LARGE = 'l', MOBILE = 'mobile', MONOSPACE = 'monospace', } /** * @deprecated Since v2.0.0. Use string literal instead. */ export enum BentoTypographyElement { H1 = 'h1', H2 = 'h2', H3 = 'h3', H4 = 'h4', H5 = 'h5', H6 = 'h6', DIV = 'div', PARAGRAPH = 'p', SPAN = 'span', } export interface BentoTypographyProps { /** * Sets the HTML DOM element. * @values h1, h2, h3, h4, h5, h6, div, p, span */ el?: BentoTypographyElement | `${BentoTypographyElement}`; /** * Adds the large class modifier. * Applicable to the Title variant. */ large?: boolean; /** * Adds the medium class modifier. * Applicable to the Title variant. */ medium?: boolean; /** * Changes the font family to monospace. * Applicable to all variants. */ monospace?: boolean; /** * Adds the rich-text modifier, which allows for the use of HTML tags inside typography. */ richText?: boolean; /** * Adds the stronger class modifier. * Applicable to Caption, Body and Subtitle variants. */ stronger?: boolean; /** * Adds the strongest class modifier. * Applicable to Caption, Body and Subtitle variants. */ strongest?: boolean; /** * Sets the type of typography variant. * @values caption, body, title. */ variant?: BentoTypographyVariant | `${BentoTypographyVariant}`; /** * Adds the medium class modifier. * Applicable to the Body variant. */ wide?: boolean; }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <component :is="el" class="b-typography" :class="conditionalClasses" data-testid="typography"> <slot /> </component> </template> <script setup lang="ts"> import { computed, onMounted } from 'vue'; import { deprecate } from '@/utils/ts/deprecate'; import { BentoTypographyElement, BentoTypographyModifier, type BentoTypographyProps, BentoTypographyVariant, } from './typography.types'; const props = withDefaults(defineProps<BentoTypographyProps>(), { el: BentoTypographyElement.PARAGRAPH, large: false, medium: false, stronger: false, strongest: false, variant: BentoTypographyVariant.BODY, wide: false, }); if (props.variant === BentoTypographyVariant.SUBTITLE) { deprecate( 'BentoTypography "subtitle" variant (BentoTypographyVariant.SUBTITLE)', `Use the "title" or "BentoTypographyVariant.TITLE" variant and (optionally) combine it with a size property. e.g. medium / large. <bento-typography :variant="BentoTypographyVariant.TITLE" medium>`, '2.0.0' ); } const conditionalClasses = computed(() => ({ // Rich text ['b-typography--rich-text']: props.richText, // Caption [`b-typography--${BentoTypographyVariant.CAPTION}`]: props.variant === BentoTypographyVariant.CAPTION, [`b-typography--${BentoTypographyVariant.CAPTION}-${BentoTypographyModifier.WIDE}`]: props.variant === BentoTypographyVariant.CAPTION && props.wide, [`b-typography--${BentoTypographyVariant.CAPTION}-${BentoTypographyModifier.STRONGER}`]: props.variant === BentoTypographyVariant.CAPTION && props.stronger, // Body [`b-typography--${BentoTypographyVariant.BODY}`]: props.variant === BentoTypographyVariant.BODY, [`b-typography--${BentoTypographyVariant.BODY}-${BentoTypographyModifier.WIDE}`]: props.variant === BentoTypographyVariant.BODY && props.wide, [`b-typography--${BentoTypographyVariant.BODY}-${BentoTypographyModifier.STRONGER}`]: props.variant === BentoTypographyVariant.BODY && props.stronger, [`b-typography--${BentoTypographyVariant.BODY}-${BentoTypographyModifier.STRONGEST}`]: props.variant === BentoTypographyVariant.BODY && props.strongest, // Subtitle [`b-typography--${BentoTypographyVariant.SUBTITLE}`]: props.variant === BentoTypographyVariant.SUBTITLE, [`b-typography--${BentoTypographyVariant.SUBTITLE}-${BentoTypographyModifier.STRONGER}`]: props.variant === BentoTypographyVariant.SUBTITLE && props.stronger, // Title [`b-typography--${BentoTypographyVariant.TITLE}`]: props.variant === BentoTypographyVariant.TITLE && !props.medium && !props.large, [`b-typography--${BentoTypographyVariant.TITLE}-${BentoTypographyModifier.MEDIUM}`]: props.variant === BentoTypographyVariant.TITLE && props.medium, [`b-typography--${BentoTypographyVariant.TITLE}-${BentoTypographyModifier.LARGE}`]: props.variant === BentoTypographyVariant.TITLE && props.large, })); onMounted(() => { if (props.richText && props.el !== 'div') { throw new Error('Rich-text prop must be used with el="div" for correct HTML semantics'); } if (props.richText && props.variant === 'title') { throw new Error('Rich-text prop cannot be used with the title variant.'); } if (props.richText && props.variant === 'subtitle') { throw new Error('Rich-text prop cannot be used with the subtitle variant.'); } }); </script> <script lang="ts"> /** * The Typography component makes it easy to apply a default set of font weights, sizes and other text styles * easily to text. The variants and properties match with those found in Figma. * * For example, seeing text with the typography styling of * * @example * import { BentoTypography } from '@adyen/bento-vue2'; * * export default { * components: { BentoTypography }, * template: ` * <bento-typography> * ... text content * </bento-typography> * ` * } */ export default { name: 'bento-typography', inheritAttrs: true, }; </script> <style lang="scss" scoped src="./typography.scss" />
|
|
1
|
+
<template> <component :is="el" class="b-typography" :class="conditionalClasses" data-testid="typography"> <slot /> </component> </template> <script setup lang="ts"> import { computed, onMounted } from 'vue'; import { deprecate } from '@/utils/ts/deprecate'; import { BentoTypographyElement, BentoTypographyModifier, type BentoTypographyProps, BentoTypographyVariant, } from './typography.types'; const props = withDefaults(defineProps<BentoTypographyProps>(), { el: BentoTypographyElement.PARAGRAPH, large: false, medium: false, monospace: false, stronger: false, strongest: false, variant: BentoTypographyVariant.BODY, wide: false, }); if (props.variant === BentoTypographyVariant.SUBTITLE) { deprecate( 'BentoTypography "subtitle" variant (BentoTypographyVariant.SUBTITLE)', `Use the "title" or "BentoTypographyVariant.TITLE" variant and (optionally) combine it with a size property. e.g. medium / large. <bento-typography :variant="BentoTypographyVariant.TITLE" medium>`, '2.0.0' ); } const conditionalClasses = computed(() => ({ // Rich text ['b-typography--rich-text']: props.richText, // Monospace [`b-typography--${BentoTypographyModifier.MONOSPACE}`]: props.monospace, // Caption [`b-typography--${BentoTypographyVariant.CAPTION}`]: props.variant === BentoTypographyVariant.CAPTION, [`b-typography--${BentoTypographyVariant.CAPTION}-${BentoTypographyModifier.WIDE}`]: props.variant === BentoTypographyVariant.CAPTION && props.wide, [`b-typography--${BentoTypographyVariant.CAPTION}-${BentoTypographyModifier.STRONGER}`]: props.variant === BentoTypographyVariant.CAPTION && props.stronger, // Body [`b-typography--${BentoTypographyVariant.BODY}`]: props.variant === BentoTypographyVariant.BODY, [`b-typography--${BentoTypographyVariant.BODY}-${BentoTypographyModifier.WIDE}`]: props.variant === BentoTypographyVariant.BODY && props.wide, [`b-typography--${BentoTypographyVariant.BODY}-${BentoTypographyModifier.STRONGER}`]: props.variant === BentoTypographyVariant.BODY && props.stronger, [`b-typography--${BentoTypographyVariant.BODY}-${BentoTypographyModifier.STRONGEST}`]: props.variant === BentoTypographyVariant.BODY && props.strongest, // Subtitle [`b-typography--${BentoTypographyVariant.SUBTITLE}`]: props.variant === BentoTypographyVariant.SUBTITLE, [`b-typography--${BentoTypographyVariant.SUBTITLE}-${BentoTypographyModifier.STRONGER}`]: props.variant === BentoTypographyVariant.SUBTITLE && props.stronger, // Title [`b-typography--${BentoTypographyVariant.TITLE}`]: props.variant === BentoTypographyVariant.TITLE && !props.medium && !props.large, [`b-typography--${BentoTypographyVariant.TITLE}-${BentoTypographyModifier.MEDIUM}`]: props.variant === BentoTypographyVariant.TITLE && props.medium, [`b-typography--${BentoTypographyVariant.TITLE}-${BentoTypographyModifier.LARGE}`]: props.variant === BentoTypographyVariant.TITLE && props.large, })); onMounted(() => { if (props.richText && props.el !== 'div') { throw new Error('Rich-text prop must be used with el="div" for correct HTML semantics'); } if (props.richText && props.variant === 'title') { throw new Error('Rich-text prop cannot be used with the title variant.'); } if (props.richText && props.variant === 'subtitle') { throw new Error('Rich-text prop cannot be used with the subtitle variant.'); } }); </script> <script lang="ts"> /** * The Typography component makes it easy to apply a default set of font weights, sizes and other text styles * easily to text. The variants and properties match with those found in Figma. * * For example, seeing text with the typography styling of * * @example * import { BentoTypography } from '@adyen/bento-vue2'; * * export default { * components: { BentoTypography }, * template: ` * <bento-typography> * ... text content * </bento-typography> * ` * } */ export default { name: 'bento-typography', inheritAttrs: true, }; </script> <style lang="scss" scoped src="./typography.scss" />
|