@adyen/bento-mcp 0.7.0 → 0.9.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/dist/assets/components/anchor-scroller/anchor-scroller.vue +1 -1
- package/dist/assets/components/currency/currency.types.ts +1 -1
- package/dist/assets/components/currency/currency.vue +1 -1
- package/dist/assets/components/date-range-picker/components/date-range-picker-calendar/date-range-picker-calendar.vue +1 -1
- package/dist/assets/components/date-range-picker/date-range-picker.vue +1 -1
- package/dist/assets/components/dropdown/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/input-field.vue +1 -1
- package/dist/assets/components/input-field-password/input-field-password.vue +1 -1
- package/dist/assets/components/inspector/components/inspector-page/inspector-page.types.ts +1 -0
- package/dist/assets/components/inspector/components/inspector-page/inspector-page.vue +1 -0
- package/dist/assets/components/inspector/inspector.docs.mdx +14 -0
- package/dist/assets/components/inspector/inspector.stories.ts +1 -1
- package/dist/assets/components/inspector/inspector.types.ts +1 -1
- package/dist/assets/components/inspector/inspector.vue +1 -1
- package/dist/assets/components/internal/dialog-page/dialog-page.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/tag/tag.types.ts +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/components.json +1 -0
- package/dist/assets/usage.json +9 -8
- package/dist/main.js +2 -2
- package/package.json +2 -2
|
@@ -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> <bento-typography class="b-input-field" v-bind="getRootAttributes($attrs)" :data-testid="getDataTestId($attrs)" :class="inputFieldConditionalClasses" el="div" variant="body" > <field-label v-if="hasSlot('default') || label" :for="inputFieldId" data-testid="input-field-label" :tooltip-text="tooltipText" :optional="optional" :required="required" :label="label" @click="focusInput" > <slot></slot> </field-label> <div class="b-input-field__input-box" data-testid="input-box" :aria-disabled="disabled" @click="focusInput"> <!-- Optional Icon for Default text variant --> <span v-if="isDefaultVariant && (hasSlot('defaultIconBefore') || hasSlot('iconBefore'))" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__icon-before" > <slot v-if="hasSlot('iconBefore')" name="iconBefore" /> <slot v-if="hasSlot('defaultIconBefore') && !hasSlot('iconBefore')" name="defaultIconBefore" /> </span> <!-- Mandatory Icon for Payment Method variant --> <span v-if="isPaymentMethodVariant && hasSlot('paymentMethod')" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__payment-method" > <slot name="paymentMethod" /> </span> <!-- Dropdown at start --> <div v-if="shouldDisplayDropdownAtStart" :id="dropdownId" class="b-input-field__dropdown b-input-field__dropdown--start" @click.stop > <bento-dropdown v-if="dropdown" ref="inputDropdownElement" v-bind="inputFieldDropdownProps" :disabled="disabled" :readonly="isDropdownReadOnly" @update:model-value="onDropdownInput" /> </div> <!-- Static Value at start --> <bento-typography v-if="shouldDisplayStaticValueAtStart" :id="staticValueId" class="b-input-field__static-value b-input-field__static-value--start" el="span" variant="body" > <slot name="staticValue" /> </bento-typography> <div class="b-input-field__input-container"> <input :id="inputFieldId" v-bind="$attrs" ref="inputFieldElement" class="b-input-field__input" :aria-label="computedAriaLabel" :aria-describedby="computedAriaDescribedBy" :aria-owns="computedAriaOwns" :aria-invalid="shouldShowError" :placeholder="placeholder" :required="required" :aria-required="required" :type="type" :value="modelValue ?? value" :disabled="disabled" :readonly="isReadOnly" @input="onInput" @change="emit('change')" @focus="onFocus" @blur="onBlur" @keydown="emit('keydown', $event)" @keyup="emit('keyup', $event)" @keydown.esc="emit('escape-pressed')" @click="emit('click', $event)" /> <!-- Hint at the end --> <bento-typography v-if="hint && isFocused && (modelValue || value)" el="span" class="b-input-field__hint" > {{ hint }} </bento-typography> </div> <span v-if="hasSlot('iconAfter')" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__icon-after" data-testid="input-text__icon-after" > <slot name="iconAfter" /> </span> <!-- Static Value at the end --> <bento-typography v-if="shouldDisplayStaticValueAtEnd" :id="staticValueId" class="b-input-field__static-value b-input-field__static-value--end" el="span" variant="body" > <slot name="staticValue" /> </bento-typography> <!-- Dropdown at end --> <div v-if="shouldDisplayDropdownAtEnd" :id="dropdownId" data-testid="input-field-dropdown-container-end" class="b-input-field__dropdown b-input-field__dropdown--end" @click.stop > <bento-dropdown v-if="dropdown" :id="dropdownId" ref="inputDropdownElement" v-bind="inputFieldDropdownProps" :disabled="disabled" :readonly="isDropdownReadOnly" @update:model-value="onDropdownInput" /> </div> </div> <error-message v-if="shouldShowError && !!errorMessage" :id="errorId" :error-message="errorMessage" class="b-input-field__error-message" /> <span v-if="hasSlot('description') || description" :id="descriptionId" class="b-input-field__description" @click="focusInput" > <bento-typography el="span" variant="body"> <slot id="description" name="description"> {{ description }} </slot> </bento-typography> </span> </bento-typography> </template> <script setup lang="ts"> import { computed, inject, provide, type Ref, ref, toRef, toRefs, useAttrs, useSlots } from 'vue'; // Components import { BentoDropdown } from '@/components/dropdown'; import { DROPDOWN_SMALL_SIZE_INJECTION_KEY } from '@/components/dropdown/dropdown.keys'; import { BentoTypography } from '@/components/typography'; import { ErrorMessage, FieldLabel } from '@/internal'; // Utils import { deprecate } from '@/utils/ts/deprecate'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; import { generateUid } from '@/core/utils/ts'; import { getSlotText } from '@/utils/ts/get-slot-text'; import { useAriaLabel } from '@/utils/ts/aria-label'; import { useI18n } from '@/utils/ts/i18n'; import { printDevelopmentWarning } from '@/utils/ts/print-development-warning'; import { INPUT_FIELD_HINT_INJECTION_KEY, INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, } from '@/components/input-field/input-field.keys'; import { type BentoListboxSelectedValue } from '@/types/listbox'; // Composables import { useFormLayoutFieldLoading, useHasSlot } from '@/composables'; // Types import { type BentoInputDropdownProps, BentoInputFieldElementPosition, type BentoInputFieldProps, BentoInputFieldStateClass, BentoInputFieldType, type BentoInputFieldValue, BentoInputFieldVariant, } from './input-field.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const attrs = useAttrs(); const slots = useSlots(); const props = withDefaults(defineProps<BentoInputFieldProps>(), { ariaHidden: true, ariaLabel: undefined, condensed: false, description: '', disabled: false, dropdown: undefined, dropdownPosition: BentoInputFieldElementPosition.START, error: false, errorMessage: null, label: '', modelValue: undefined, optional: false, placeholder: '', required: false, readonly: false, slashedZero: false, staticValue: '', staticValuePosition: BentoInputFieldElementPosition.START, tooltipText: null, type: BentoInputFieldType.TEXT, value: undefined, variant: BentoInputFieldVariant.DEFAULT, }); const emit = defineEmits<{ /** * Emitted when the element has lost focus */ (e: 'blur'): void; /** * Emitted when the input is clicked */ (e: 'click', event: MouseEvent); /** * Emitted when the user modifies the element's value. Unlike the 'input' event, the change event is not necessarily fired for each alteration to an element's value */ (e: 'change'): void; /** * Emitted when the Input Field's internal dropdown changes it's value */ (e: 'dropdown-input', selectedValue: BentoListboxSelectedValue): void; /** * Emitted when the "ESC" key is pressed */ (e: 'escape-pressed'): void; /** * Emitted when the element has received focus */ (e: 'focus'): void; /** * Emitted when a key is pressed down */ (e: 'keydown', event: KeyboardEvent): void; /** * Emitted when a key is pressed and lifted up */ (e: 'keyup', event: KeyboardEvent): void; /** * Emitted when the component's model changes * @deprecated Since v2.0.0. Use `v-model` or `update:model-value` instead. */ (e: 'input', inputValue: string): void; /** * Emitted when the Input Field's internal dropdown changes it's value. Contains every dropdown prop passed with the updated selected value as 'value' */ (e: 'update:dropdown', updatedDropdownProps: BentoInputDropdownProps): void; /** * Emitted when the component's model changes */ (e: 'update:model-value', inputValue: BentoInputFieldValue): void; }>(); const { emitValue } = useFormFieldEmits<BentoInputFieldValue>(emit); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const hasSlot = useHasSlot(slots); const inputFieldElement = ref<HTMLInputElement>(null); const inputDropdownElement = ref(null); const inputFieldId = generateUid('input'); const dropdownId = generateUid('input-dropdown'); const staticValueId = generateUid('input-static-value'); const descriptionId = generateUid('input-description'); const errorId = generateUid('input-error'); const isFocused = ref(false); /** * Gets the overriden or the default data-testid attribute * @param attributes - the live attributes object */ function getDataTestId(attributes: Record<string, unknown>) { return (attributes['data-testid'] as string) ?? 'input-text-container'; } /** * Filters out all aria attributes for the root element. * @param attributes - the live attributes object */ function getRootAttributes(attributes: Record<string, unknown>) { return Object.fromEntries( Object.entries(attributes).filter(([key]) => !key.startsWith('aria-') && key !== 'data-testid') ); } // Provide DROPDOWN_SMALL_SIZE_INJECTION_KEY as true, to set the input only dropdown size variant in the BentoDropdown comp provide(DROPDOWN_SMALL_SIZE_INJECTION_KEY, true); // Renders the input with a hint if a string is provided const hint = inject<Ref<string | undefined>>(INPUT_FIELD_HINT_INJECTION_KEY, ref(undefined)); const onFocus = () => { isFocused.value = true; emit('focus'); }; const onBlur = () => { isFocused.value = false; emit('blur'); }; const { isReadOnly } = useFormLayoutFieldLoading(toRef(props, 'readonly')); const isDropdownReadOnly = computed<BentoInputDropdownProps['readonly']>( () => isReadOnly.value || props.dropdown?.readonly ); const shouldShowStaticValue = computed( () => props.variant === BentoInputFieldVariant.STATIC_VALUE && !!slots.staticValue ); const isDefaultVariant = computed(() => props.variant === BentoInputFieldVariant.DEFAULT); const isPaymentMethodVariant = computed(() => props.variant === BentoInputFieldVariant.PAYMENT_METHOD); const shouldDisplayStaticValueAtStart = computed( () => shouldShowStaticValue.value && props.staticValuePosition === BentoInputFieldElementPosition.START ); const shouldDisplayStaticValueAtEnd = computed( () => shouldShowStaticValue.value && props.staticValuePosition === BentoInputFieldElementPosition.END ); const shouldShowDropdown = computed(() => props.variant === BentoInputFieldVariant.DROPDOWN && !!props.dropdown); const shouldDisplayDropdownAtStart = computed( () => shouldShowDropdown.value && props.dropdownPosition === BentoInputFieldElementPosition.START ); const shouldDisplayDropdownAtEnd = computed( () => shouldShowDropdown.value && props.dropdownPosition === BentoInputFieldElementPosition.END ); const shouldShowError = computed( () => !props.disabled && !isReadOnly.value && (!!props.errorMessage || props.error) ); const computedLabel = computed(() => (getSlotText(slots)('default') as string) || props.label); const computedDescription = computed(() => (getSlotText(slots)('description') as string) || props.description); const computedAriaOwns = computed(() => { if (shouldShowDropdown.value === true && shouldShowStaticValue.value === true) { return `${staticValueId} ${dropdownId}`; } if (shouldShowDropdown.value === true) { return `${dropdownId}`; } if (shouldShowStaticValue.value === true) { return `${staticValueId}`; } return null; }); // vue-i18n issue with t() used in script - direct usage in template works // requires either createI18n({ legacy: false }) or to be wrapped in computed() const computedAriaLabelFallbackMessage = computed(() => attrs?.['aria-labelledby'] ? undefined : t('ariaLabelFallback') ); const computedAriaLabelDropdowFallback = computed(() => t('ariaLabelDropdownFallback', { inputLabel: computedAriaLabel.value }) ); const ariaLabelAttribute = ref(attrs['aria-label']); const { ariaLabel } = toRefs(props); const computedAriaLabel = useAriaLabel({ ariaLabel: ariaLabelAttribute.value as string, // TODO: take this line away when the aria-label props is removed label: ariaLabel, defaultFallback: computedLabel.value || computedAriaLabelFallbackMessage, }); const computedAriaDescribedBy = computed( () => `${computedDescription.value ? descriptionId : ''} ${ shouldShowError.value && !!props.errorMessage ? errorId : '' }` ); const inputFieldConditionalClasses = computed(() => ({ [`b-input-field--${BentoInputFieldStateClass.CONDENSED}`]: props.condensed, [`b-input-field--${BentoInputFieldStateClass.DISABLED}`]: props.disabled, [`b-input-field--${BentoInputFieldStateClass.READONLY}`]: isReadOnly.value, [`b-input-field--${BentoInputFieldStateClass.ERROR}`]: shouldShowError.value, [`b-input-field--slashed-zero`]: props.slashedZero, })); const inputFieldDropdownProps = computed(() => ({ 'aria-label': computedAriaLabelDropdowFallback.value, ...props.dropdown, })); const onInput = (event: Event) => { const inputValue = (event.target as HTMLInputElement).value; if (!props.disabled && !isReadOnly.value) { emitValue(inputValue); } }; const onDropdownInput = (selectedValue: BentoListboxSelectedValue) => { emit('dropdown-input', selectedValue); emit('update:dropdown', { ...props?.dropdown, value: selectedValue }); }; const focusInput = () => { inputFieldElement.value.focus(); }; const focus = () => { if (props.variant === 'dropdown') { inputDropdownElement.value.focus(); } else { focusInput(); } }; // Expose focus methods for parent components in Vue 3. defineExpose({ /** * Focuses the input field. */ focusInput, /** * Focuses the dropdown when using the dropdown variant; otherwise focuses the input field. */ focus, /** * Reference to the native input element. */ inputFieldElement, }); const inputFieldParentComponent = inject(INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, ''); if (!Object.values(BentoInputFieldType).includes(props.type as BentoInputFieldType) && !inputFieldParentComponent) { if (props.type === 'tel') { printDevelopmentWarning( `"bento-input-field" dropdown does not support the type '${props.type}', use "bento-input-field-phone-number" instead.` ); } else if (props.type === 'password') { printDevelopmentWarning( `"bento-input-field" dropdown does not support the type '${props.type}', use "bento-input-field-password" instead.` ); } else { printDevelopmentWarning(`"bento-input-field" dropdown does not support the type '${props.type}'`); } } /** * Deprecations */ if (props.error) { deprecate( 'BentoInputField "error" property', `Use the BentoInputField "errorMessage" property instead to indicate that there is a modifier error. Having the field as "undefined" will hide/remove the error. <bento-dropdown errorMessage="Error message text" />`, '2.0.0' ); } if (hasSlot('defaultIconBefore')) { deprecate( 'BentoInputField "defaultIconBefore" slot', `Use the BentoInputField "iconBefore" slot instead to add an icon at the left side of the input field <bento-input-field> \t<template #iconBefore> \t\t<my-icon-goes-here /> \t</template> </bento-input-field>`, '2.0.0' ); } if (props.value) { deprecate( 'BentoInputField "value" property', `The use of "value" prop in "BentoInputField" is no longer supported. Use the "v-model" or "model-value" property instead.`, '2.0.0' ); } </script> <script lang="ts"> /** * Input field component is used to create interactive controls for web-based * forms in order to accept data from the user. * * @example * import { BentoInputField } from '@adyen/bento-vue2' * * const inputValue = ref(''); * * export default { * components: { BentoInputField }, * tempate: ` * <bento-input-field * :model-value="inputValue" * :@update:model-value="newValue => inputValue.value = newValue" * > * } */ export default { name: 'bento-input-field', inheritAttrs: false, model: { prop: 'modelValue' }, i18n: { messages }, }; </script> <style lang="scss" scoped src="./input-field.scss" />
|
|
1
|
+
<template> <bento-typography class="b-input-field" v-bind="getRootAttributes($attrs)" :data-testid="getDataTestId($attrs)" :class="inputFieldConditionalClasses" el="div" variant="body" > <field-label v-if="hasSlot('default') || label" :for="inputFieldId" data-testid="input-field-label" :tooltip-text="tooltipText" :optional="optional" :required="required" :label="label" @click="focusInput" > <slot></slot> </field-label> <div class="b-input-field__input-box" data-testid="input-box" :aria-disabled="disabled" @click="focusInput"> <!-- Optional Icon for Default text variant --> <span v-if="isDefaultVariant && (hasSlot('defaultIconBefore') || hasSlot('iconBefore'))" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__icon-before" > <slot v-if="hasSlot('iconBefore')" name="iconBefore" /> <slot v-if="hasSlot('defaultIconBefore') && !hasSlot('iconBefore')" name="defaultIconBefore" /> </span> <!-- Mandatory Icon for Payment Method variant --> <span v-if="isPaymentMethodVariant && hasSlot('paymentMethod')" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__payment-method" > <slot name="paymentMethod" /> </span> <!-- Dropdown at start --> <div v-if="shouldDisplayDropdownAtStart" :id="dropdownId" class="b-input-field__dropdown b-input-field__dropdown--start" @click.stop > <bento-dropdown v-if="dropdown" ref="inputDropdownElement" v-bind="inputFieldDropdownProps" :disabled="disabled" :readonly="isDropdownReadOnly" @update:model-value="onDropdownInput" /> </div> <!-- Static Value at start --> <bento-typography v-if="shouldDisplayStaticValueAtStart" :id="staticValueId" class="b-input-field__static-value b-input-field__static-value--start" el="span" variant="body" > <slot name="staticValue" /> </bento-typography> <div class="b-input-field__input-container"> <input :id="inputFieldId" v-bind="$attrs" ref="inputFieldElement" class="b-input-field__input" :aria-label="computedAriaLabel" :aria-describedby="computedAriaDescribedBy" :aria-owns="computedAriaOwns" :aria-invalid="shouldShowError" :placeholder="placeholder" :required="required" :aria-required="required" :type="type" :value="modelValue ?? value" :disabled="disabled" :readonly="isReadOnly" @input="onInput" @change="emit('change')" @focus="onFocus" @blur="onBlur" @keydown="emit('keydown', $event)" @keyup="emit('keyup', $event)" @keydown.esc="emit('escape-pressed')" @click="emit('click', $event)" /> <!-- Hint at the end --> <bento-typography v-if="hint && isFocused && (modelValue || value)" el="span" class="b-input-field__hint" > {{ hint }} </bento-typography> </div> <span v-if="hasSlot('iconAfter')" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__icon-after" data-testid="input-text__icon-after" > <slot name="iconAfter" /> </span> <!-- Static Value at the end --> <bento-typography v-if="shouldDisplayStaticValueAtEnd" :id="staticValueId" class="b-input-field__static-value b-input-field__static-value--end" el="span" variant="body" > <slot name="staticValue" /> </bento-typography> <!-- Dropdown at end --> <div v-if="shouldDisplayDropdownAtEnd" :id="dropdownId" data-testid="input-field-dropdown-container-end" class="b-input-field__dropdown b-input-field__dropdown--end" @click.stop > <bento-dropdown v-if="dropdown" :id="dropdownId" ref="inputDropdownElement" v-bind="inputFieldDropdownProps" :disabled="disabled" :readonly="isDropdownReadOnly" @update:model-value="onDropdownInput" /> </div> </div> <div v-if="(shouldShowError && !!errorMessage) || hasSlot('description') || description" class="b-input-field__footer" > <error-message v-if="shouldShowError && !!errorMessage" :id="errorId" :error-message="errorMessage" /> <span v-if="hasSlot('description') || description" :id="descriptionId" class="b-input-field__description" @click="focusInput" > <bento-typography el="span" variant="body"> <slot id="description" name="description"> {{ description }} </slot> </bento-typography> </span> </div> </bento-typography> </template> <script setup lang="ts"> import { computed, inject, provide, type Ref, ref, toRef, toRefs, useAttrs, useSlots } from 'vue'; // Components import { BentoDropdown } from '@/components/dropdown'; import { DROPDOWN_SMALL_SIZE_INJECTION_KEY } from '@/components/dropdown/dropdown.keys'; import { BentoTypography } from '@/components/typography'; import { ErrorMessage, FieldLabel } from '@/internal'; // Utils import { deprecate } from '@/utils/ts/deprecate'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; import { generateUid } from '@/core/utils/ts'; import { getSlotText } from '@/utils/ts/get-slot-text'; import { useAriaLabel } from '@/utils/ts/aria-label'; import { useI18n } from '@/utils/ts/i18n'; import { printDevelopmentWarning } from '@/utils/ts/print-development-warning'; import { INPUT_FIELD_HINT_INJECTION_KEY, INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, } from '@/components/input-field/input-field.keys'; import { type BentoListboxSelectedValue } from '@/types/listbox'; // Composables import { useFormLayoutFieldLoading, useHasSlot } from '@/composables'; // Types import { type BentoInputDropdownProps, BentoInputFieldElementPosition, type BentoInputFieldProps, BentoInputFieldStateClass, BentoInputFieldType, type BentoInputFieldValue, BentoInputFieldVariant, } from './input-field.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const attrs = useAttrs(); const slots = useSlots(); const props = withDefaults(defineProps<BentoInputFieldProps>(), { ariaHidden: true, ariaLabel: undefined, condensed: false, description: '', disabled: false, dropdown: undefined, dropdownPosition: BentoInputFieldElementPosition.START, error: false, errorMessage: null, label: '', modelValue: undefined, optional: false, placeholder: '', required: false, readonly: false, slashedZero: false, staticValue: '', staticValuePosition: BentoInputFieldElementPosition.START, tooltipText: null, type: BentoInputFieldType.TEXT, value: undefined, variant: BentoInputFieldVariant.DEFAULT, }); const emit = defineEmits<{ /** * Emitted when the element has lost focus */ (e: 'blur'): void; /** * Emitted when the input is clicked */ (e: 'click', event: MouseEvent); /** * Emitted when the user modifies the element's value. Unlike the 'input' event, the change event is not necessarily fired for each alteration to an element's value */ (e: 'change'): void; /** * Emitted when the Input Field's internal dropdown changes it's value */ (e: 'dropdown-input', selectedValue: BentoListboxSelectedValue): void; /** * Emitted when the "ESC" key is pressed */ (e: 'escape-pressed'): void; /** * Emitted when the element has received focus */ (e: 'focus'): void; /** * Emitted when a key is pressed down */ (e: 'keydown', event: KeyboardEvent): void; /** * Emitted when a key is pressed and lifted up */ (e: 'keyup', event: KeyboardEvent): void; /** * Emitted when the component's model changes * @deprecated Since v2.0.0. Use `v-model` or `update:model-value` instead. */ (e: 'input', inputValue: string): void; /** * Emitted when the Input Field's internal dropdown changes it's value. Contains every dropdown prop passed with the updated selected value as 'value' */ (e: 'update:dropdown', updatedDropdownProps: BentoInputDropdownProps): void; /** * Emitted when the component's model changes */ (e: 'update:model-value', inputValue: BentoInputFieldValue): void; }>(); const { emitValue } = useFormFieldEmits<BentoInputFieldValue>(emit); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const hasSlot = useHasSlot(slots); const inputFieldElement = ref<HTMLInputElement>(null); const inputDropdownElement = ref(null); const inputFieldId = generateUid('input'); const dropdownId = generateUid('input-dropdown'); const staticValueId = generateUid('input-static-value'); const descriptionId = generateUid('input-description'); const errorId = generateUid('input-error'); const isFocused = ref(false); /** * Gets the overriden or the default data-testid attribute * @param attributes - the live attributes object */ function getDataTestId(attributes: Record<string, unknown>) { return (attributes['data-testid'] as string) ?? 'input-text-container'; } /** * Filters out all aria attributes for the root element. * @param attributes - the live attributes object */ function getRootAttributes(attributes: Record<string, unknown>) { return Object.fromEntries( Object.entries(attributes).filter(([key]) => !key.startsWith('aria-') && key !== 'data-testid') ); } // Provide DROPDOWN_SMALL_SIZE_INJECTION_KEY as true, to set the input only dropdown size variant in the BentoDropdown comp provide(DROPDOWN_SMALL_SIZE_INJECTION_KEY, true); // Renders the input with a hint if a string is provided const hint = inject<Ref<string | undefined>>(INPUT_FIELD_HINT_INJECTION_KEY, ref(undefined)); const onFocus = () => { isFocused.value = true; emit('focus'); }; const onBlur = () => { isFocused.value = false; emit('blur'); }; const { isReadOnly } = useFormLayoutFieldLoading(toRef(props, 'readonly')); const isDropdownReadOnly = computed<BentoInputDropdownProps['readonly']>( () => isReadOnly.value || props.dropdown?.readonly ); const shouldShowStaticValue = computed( () => props.variant === BentoInputFieldVariant.STATIC_VALUE && !!slots.staticValue ); const isDefaultVariant = computed(() => props.variant === BentoInputFieldVariant.DEFAULT); const isPaymentMethodVariant = computed(() => props.variant === BentoInputFieldVariant.PAYMENT_METHOD); const shouldDisplayStaticValueAtStart = computed( () => shouldShowStaticValue.value && props.staticValuePosition === BentoInputFieldElementPosition.START ); const shouldDisplayStaticValueAtEnd = computed( () => shouldShowStaticValue.value && props.staticValuePosition === BentoInputFieldElementPosition.END ); const shouldShowDropdown = computed(() => props.variant === BentoInputFieldVariant.DROPDOWN && !!props.dropdown); const shouldDisplayDropdownAtStart = computed( () => shouldShowDropdown.value && props.dropdownPosition === BentoInputFieldElementPosition.START ); const shouldDisplayDropdownAtEnd = computed( () => shouldShowDropdown.value && props.dropdownPosition === BentoInputFieldElementPosition.END ); const shouldShowError = computed( () => !props.disabled && !isReadOnly.value && (!!props.errorMessage || props.error) ); const computedLabel = computed(() => (getSlotText(slots)('default') as string) || props.label); const computedDescription = computed(() => (getSlotText(slots)('description') as string) || props.description); const computedAriaOwns = computed(() => { if (shouldShowDropdown.value === true && shouldShowStaticValue.value === true) { return `${staticValueId} ${dropdownId}`; } if (shouldShowDropdown.value === true) { return `${dropdownId}`; } if (shouldShowStaticValue.value === true) { return `${staticValueId}`; } return null; }); // vue-i18n issue with t() used in script - direct usage in template works // requires either createI18n({ legacy: false }) or to be wrapped in computed() const computedAriaLabelFallbackMessage = computed(() => attrs?.['aria-labelledby'] ? undefined : t('ariaLabelFallback') ); const computedAriaLabelDropdowFallback = computed(() => t('ariaLabelDropdownFallback', { inputLabel: computedAriaLabel.value }) ); const ariaLabelAttribute = ref(attrs['aria-label']); const { ariaLabel } = toRefs(props); const computedAriaLabel = useAriaLabel({ ariaLabel: ariaLabelAttribute.value as string, // TODO: take this line away when the aria-label props is removed label: ariaLabel, defaultFallback: computedLabel.value || computedAriaLabelFallbackMessage, }); const computedAriaDescribedBy = computed( () => `${computedDescription.value ? descriptionId : ''} ${ shouldShowError.value && !!props.errorMessage ? errorId : '' }` ); const inputFieldConditionalClasses = computed(() => ({ [`b-input-field--${BentoInputFieldStateClass.CONDENSED}`]: props.condensed, [`b-input-field--${BentoInputFieldStateClass.DISABLED}`]: props.disabled, [`b-input-field--${BentoInputFieldStateClass.READONLY}`]: isReadOnly.value, [`b-input-field--${BentoInputFieldStateClass.ERROR}`]: shouldShowError.value, [`b-input-field--slashed-zero`]: props.slashedZero, })); const inputFieldDropdownProps = computed(() => ({ 'aria-label': computedAriaLabelDropdowFallback.value, ...props.dropdown, })); const onInput = (event: Event) => { const inputValue = (event.target as HTMLInputElement).value; if (!props.disabled && !isReadOnly.value) { emitValue(inputValue); } }; const onDropdownInput = (selectedValue: BentoListboxSelectedValue) => { emit('dropdown-input', selectedValue); emit('update:dropdown', { ...props?.dropdown, value: selectedValue }); }; const focusInput = () => { inputFieldElement.value.focus(); }; const focus = () => { if (props.variant === 'dropdown') { inputDropdownElement.value.focus(); } else { focusInput(); } }; // Expose focus methods for parent components in Vue 3. defineExpose({ /** * Focuses the input field. */ focusInput, /** * Focuses the dropdown when using the dropdown variant; otherwise focuses the input field. */ focus, /** * Reference to the native input element. */ inputFieldElement, }); const inputFieldParentComponent = inject(INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, ''); if (!Object.values(BentoInputFieldType).includes(props.type as BentoInputFieldType) && !inputFieldParentComponent) { if (props.type === 'tel') { printDevelopmentWarning( `"bento-input-field" dropdown does not support the type '${props.type}', use "bento-input-field-phone-number" instead.` ); } else if (props.type === 'password') { printDevelopmentWarning( `"bento-input-field" dropdown does not support the type '${props.type}', use "bento-input-field-password" instead.` ); } else { printDevelopmentWarning(`"bento-input-field" dropdown does not support the type '${props.type}'`); } } /** * Deprecations */ if (props.error) { deprecate( 'BentoInputField "error" property', `Use the BentoInputField "errorMessage" property instead to indicate that there is a modifier error. Having the field as "undefined" will hide/remove the error. <bento-dropdown errorMessage="Error message text" />`, '2.0.0' ); } if (hasSlot('defaultIconBefore')) { deprecate( 'BentoInputField "defaultIconBefore" slot', `Use the BentoInputField "iconBefore" slot instead to add an icon at the left side of the input field <bento-input-field> \t<template #iconBefore> \t\t<my-icon-goes-here /> \t</template> </bento-input-field>`, '2.0.0' ); } if (props.value) { deprecate( 'BentoInputField "value" property', `The use of "value" prop in "BentoInputField" is no longer supported. Use the "v-model" or "model-value" property instead.`, '2.0.0' ); } </script> <script lang="ts"> /** * Input field component is used to create interactive controls for web-based * forms in order to accept data from the user. * * @example * import { BentoInputField } from '@adyen/bento-vue2' * * const inputValue = ref(''); * * export default { * components: { BentoInputField }, * tempate: ` * <bento-input-field * :model-value="inputValue" * :@update:model-value="newValue => inputValue.value = newValue" * > * } */ export default { name: 'bento-input-field', inheritAttrs: false, model: { prop: 'modelValue' }, i18n: { messages }, }; </script> <style lang="scss" scoped src="./input-field.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" />
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import { type BentoButtonActionsList } from '@/components/button/components/button-actions/button-actions.types'; export interface BentoInspectorPageProps { /** * List of action buttons rendered in the page footer. * * @default undefined */ actions?: BentoButtonActionsList; /** * Unique identifier for this page within the inspector navigation. */ pageId: string; /** * Page ID to navigate back to when leaving this page. * * @default null */ previousPage?: string; /** * Heading text displayed in the page header. */ title: string; }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<template> <dialog-page class="b-inspector-page" :page-id="pageId" :previous-page="previousPage" :absolute-position="false"> <template #header> <div class="b-inspector-page__header"> <bento-typography el="h2" variant="title" class="b-inspector-page__title"> {{ title }} </bento-typography> </div> </template> <template #content> <div class="b-inspector-page__content"> <bento-typography el="div" variant="body"> <slot></slot> </bento-typography> </div> </template> <template v-if="actions && actions.length" #footer> <div class="b-inspector-page__actions"> <footer-actions :actions="actions" /> </div> </template> </dialog-page> </template> <script setup lang="ts"> import { DialogPage } from '@/internal/dialog-page'; import { BentoTypography } from '@/components/typography'; import { FooterActions } from '@/internal/footer-actions'; import { type BentoInspectorPageProps } from './inspector-page.types'; withDefaults(defineProps<BentoInspectorPageProps>(), { actions: undefined, previousPage: null, }); </script> <style lang="scss" scoped src="./inspector-page.scss" />
|
|
@@ -52,6 +52,20 @@ Use `large` (595px) inspectors:
|
|
|
52
52
|
- To provide a more expansive layout for detailed information.
|
|
53
53
|
- To accommodate rich content such as images and charts.
|
|
54
54
|
|
|
55
|
+
## Navigation
|
|
56
|
+
|
|
57
|
+
Inspector supports deeper level navigation (user can go deeper in hierarchy and go back) through the use of the
|
|
58
|
+
`bento-inspector-page` component.
|
|
59
|
+
|
|
60
|
+
Each `bento-inspector-page` should have a `pageId`, which can then be passed to the `active-page` prop in
|
|
61
|
+
`bento-inspector` to control which page is shown.
|
|
62
|
+
|
|
63
|
+
Unlike the sidepanel, the inspector does not track navigation history automatically. This means that every sub page
|
|
64
|
+
**must** set the `previous-page` prop to the `pageId` of its parent page. This is what renders the back arrow in the
|
|
65
|
+
header and links the sub page back to its parent.
|
|
66
|
+
|
|
67
|
+
<Canvas of={InspectorStories.MultiplePages} />
|
|
68
|
+
|
|
55
69
|
## Modifiers
|
|
56
70
|
|
|
57
71
|
### Title
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import { isVue2 } from 'vue-demi'; import { computed, ref, watch } from 'vue'; import { BentoButton } from '@/components/button'; import { BentoDataGrid } from '@/components/data-grid'; import { BentoStructuredList, BentoStructuredListItem } from '@/components/structured-list'; import { BentoTypography } from '@/components/typography'; import { BentoHeader } from '@/components/header'; import PlusIcon from '@adyen/ui-assets-icons-16/vue/plus'; import SearchIcon from '@adyen/ui-assets-icons-16/vue/search'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import BentoInspector from './inspector.vue'; import type { Meta, StoryObj } from '@storybook/vue'; import { BentoInspectorSizeOptions } from './inspector.types'; import './inspector.stories.scss?module'; const meta: Meta = { title: 'Inspector', component: BentoInspector, parameters: { layout: 'fullscreen', }, argTypes: { size: { options: BentoInspectorSizeOptions, control: { type: 'select', }, }, relativeLayout: { options: ['push-content', 'overlay-content'], control: { type: 'select', }, }, }, }; export default meta; type Story = StoryObj<typeof BentoInspector>; const DEFAULT_PROPS = { title: 'Title', size: 'large', isOpen: false, activePage: '', relativeLayout: 'push-content', }; const defaultCode = ` <template> <bento-inspector :is-open="isOpen" @update:is-open="isOpen = $event" :active-page="activePage" @update:active-page="activePage = $event" title="Inspector title" expandable > <template #page> // Your page content <bento-button :aria-expanded="isOpen" @click="openInspector">{{ inspectorButtonTitle }}</bento-button> </template> <template #content> // Your inspector content </template> </bento-inspector> </template> <script setup lang="ts"> import { ref } from 'vue'; import { BentoButton, BentoInspector } from '@adyen/bento-vue2'; const isOpen = ref(false); const activePage = ref('') const inspectorButtonTitle = computed(() => (isOpen.value ? 'Move to inspector' : 'Open inspector')); const openInspector = () => { isOpen.value = true; activePage.value = 'PageId1234'; }; </script> `; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoButton, BentoDataGrid, BentoHeader, BentoInspector, BentoStructuredList, BentoStructuredListItem, BentoTypography, SearchIcon, }, props: Object.keys(argTypes), template: ` <bento-inspector v-bind="args" :is-open="isOpen" :title="title" :active-page="activePage" @update:is-open="isOpen = $event" @update:active-page="activePage = $event" expandable > <template #page> <div style="padding:16px"> <bento-header v-bind="headerProps"></bento-header> <bento-data-grid v-bind="gridArgs"> <template #item-paymentMethods="{item}"> <div> {{ item.paymentMethods }} <bento-button variant="tertiary" @click="openInspector(item)" style="float:right" > <template #iconLeft> <search-icon :svg-title="inspectorButtonTitle" /> </template> </bento-button> </div> </template> </bento-data-grid> </div> </template> <template #content> <div style="display: flex; flex-direction: column; height: 100%"> <bento-structured-list style="flex-grow: 1"> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> </bento-structured-list> <div> <bento-button style="float: right">Action</bento-button> </div> </div> </template> </bento-inspector> `, setup(props) { const args = isVue2 ? props : _args; const isOpen = ref(args.isOpen); const title = ref(args.title); const activePage = ref(''); const inspectorButtonTitle = computed(() => (isOpen.value ? 'Move to inspector' : 'Open inspector')); const gridData = [ { id: 1, paymentMethods: 'Visa', amount: '1,234 USD', numeric: '54,213' }, { id: 2, paymentMethods: 'MasterCard', amount: '1,234 USD', numeric: '54,213' }, { id: 3, paymentMethods: 'iDeal', amount: '1,234 USD', numeric: '54,213' }, { id: 4, paymentMethods: 'Klarna', amount: '1,234 USD', numeric: '54,213' }, { id: 5, paymentMethods: 'Apple Pay', amount: '1,234 USD', numeric: '54,213' }, ]; const openInspector = value => { title.value = value.paymentMethods; isOpen.value = true; activePage.value = value.paymentMethods; }; const gridColumns = [ { field: 'paymentMethods', label: 'Payment methods', minWidth: 200, mandatory: true, }, { field: 'amount', label: 'Amount', numeric: true, minWidth: 300 }, { field: 'numeric', label: 'Numeric', numeric: true, minWidth: 300 }, ]; const gridArgs = { data: gridData, columns: gridColumns, pagination: {}, }; const headerProps = { title: 'Payment methods list', actions: [ { title: 'Primary action', event: () => undefined }, { title: 'Secondary action', icon: PlusIcon, event: () => undefined }, { title: 'Tertiary action', icon: PlusIcon, event: () => undefined }, { title: 'Another action', event: () => undefined }, ], lastUpdated: new Date(), }; watch( () => args.isOpen, () => { isOpen.value = args.isOpen; } ); watch( () => args.title, () => { title.value = args.title; } ); watch( () => args.activePage, () => { activePage.value = args.activePage; } ); return { args, isOpen, gridArgs, title, headerProps, activePage, openInspector, inspectorButtonTitle, }; }, }), args: DEFAULT_PROPS, parameters: storybookDocsParameter(defaultCode), };
|
|
1
|
+
import { isVue2 } from 'vue-demi'; import { computed, ref, watch } from 'vue'; import { BentoButton } from '@/components/button'; import { BentoDataGrid } from '@/components/data-grid'; import { BentoStructuredList, BentoStructuredListItem } from '@/components/structured-list'; import { BentoTypography } from '@/components/typography'; import { BentoHeader } from '@/components/header'; import { BentoInspectorPage } from './components/inspector-page/index.js'; import BentoInspector from './inspector.vue'; import InspectorWithMultiplePagesExample from './__tests__/inspector-with-multiple-pages-example.vue?raw'; import PlusIcon from '@adyen/ui-assets-icons-16/vue/plus'; import SearchIcon from '@adyen/ui-assets-icons-16/vue/search'; import ShieldIcon from '@adyen/ui-assets-icons-16/vue/shield'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import type { Meta, StoryObj } from '@storybook/vue'; import { BentoInspectorSizeOptions } from './inspector.types'; import './inspector.stories.scss?module'; const meta: Meta = { title: 'Inspector', component: BentoInspector, parameters: { layout: 'fullscreen', }, argTypes: { size: { options: BentoInspectorSizeOptions, control: { type: 'select', }, }, relativeLayout: { options: ['push-content', 'overlay-content'], control: { type: 'select', }, }, }, }; export default meta; type Story = StoryObj<typeof BentoInspector>; const DEFAULT_PROPS = { title: 'Title', size: 'large', isOpen: false, activePage: '', relativeLayout: 'push-content', }; const defaultCode = ` <template> <bento-inspector :is-open="isOpen" @update:is-open="isOpen = $event" :active-page="activePage" @update:active-page="activePage = $event" title="Inspector title" expandable > <template #page> // Your page content <bento-button :aria-expanded="isOpen" @click="openInspector">{{ inspectorButtonTitle }}</bento-button> </template> <template #content> // Your inspector content </template> </bento-inspector> </template> <script setup lang="ts"> import { ref } from 'vue'; import { BentoButton, BentoInspector } from '@adyen/bento-vue2'; const isOpen = ref(false); const activePage = ref('') const inspectorButtonTitle = computed(() => (isOpen.value ? 'Move to inspector' : 'Open inspector')); const openInspector = () => { isOpen.value = true; activePage.value = 'PageId1234'; }; </script> `; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoButton, BentoDataGrid, BentoHeader, BentoInspector, BentoStructuredList, BentoStructuredListItem, BentoTypography, SearchIcon, }, props: Object.keys(argTypes), template: ` <bento-inspector v-bind="args" :is-open="isOpen" :title="title" :active-page="activePage" @update:is-open="isOpen = $event" @update:active-page="activePage = $event" expandable > <template #page> <div style="padding:16px"> <bento-header v-bind="headerProps"></bento-header> <bento-data-grid v-bind="gridArgs"> <template #item-paymentMethods="{item}"> <div> {{ item.paymentMethods }} <bento-button variant="tertiary" @click="openInspector(item)" style="float:right" > <template #iconLeft> <search-icon :svg-title="inspectorButtonTitle" /> </template> </bento-button> </div> </template> </bento-data-grid> </div> </template> <template #content> <div style="display: flex; flex-direction: column; height: 100%"> <bento-structured-list style="flex-grow: 1"> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> </bento-structured-list> <div> <bento-button style="float: right">Action</bento-button> </div> </div> </template> </bento-inspector> `, setup(props) { const args = isVue2 ? props : _args; const isOpen = ref(args.isOpen); const title = ref(args.title); const activePage = ref(''); const inspectorButtonTitle = computed(() => (isOpen.value ? 'Move to inspector' : 'Open inspector')); const gridData = [ { id: 1, paymentMethods: 'Visa', amount: '1,234 USD', numeric: '54,213' }, { id: 2, paymentMethods: 'MasterCard', amount: '1,234 USD', numeric: '54,213' }, { id: 3, paymentMethods: 'iDeal', amount: '1,234 USD', numeric: '54,213' }, { id: 4, paymentMethods: 'Klarna', amount: '1,234 USD', numeric: '54,213' }, { id: 5, paymentMethods: 'Apple Pay', amount: '1,234 USD', numeric: '54,213' }, ]; const openInspector = value => { title.value = value.paymentMethods; isOpen.value = true; activePage.value = value.paymentMethods; }; const gridColumns = [ { field: 'paymentMethods', label: 'Payment methods', minWidth: 200, mandatory: true, }, { field: 'amount', label: 'Amount', numeric: true, minWidth: 300 }, { field: 'numeric', label: 'Numeric', numeric: true, minWidth: 300 }, ]; const gridArgs = { data: gridData, columns: gridColumns, pagination: {}, }; const headerProps = { title: 'Payment methods list', actions: [ { title: 'Primary action', event: () => undefined }, { title: 'Secondary action', icon: PlusIcon, event: () => undefined }, { title: 'Tertiary action', icon: PlusIcon, event: () => undefined }, { title: 'Another action', event: () => undefined }, ], lastUpdated: new Date(), }; watch( () => args.isOpen, () => { isOpen.value = args.isOpen; } ); watch( () => args.title, () => { title.value = args.title; } ); watch( () => args.activePage, () => { activePage.value = args.activePage; } ); return { args, isOpen, gridArgs, title, headerProps, activePage, openInspector, inspectorButtonTitle, }; }, }), args: DEFAULT_PROPS, parameters: storybookDocsParameter(defaultCode), }; export const MultiplePages: Story = { render: (_args, { argTypes }) => ({ components: { BentoButton, BentoDataGrid, BentoHeader, BentoInspector, BentoInspectorPage, BentoStructuredList, BentoStructuredListItem, BentoTypography, SearchIcon, ShieldIcon, }, props: Object.keys(argTypes), template: ` <bento-inspector v-bind="args" :is-open="isOpen" :active-page="activePage" title="What if this title is really really long? So long that it should overflow the content on the right where the buttons are" @update:is-open="isOpen = $event" @update:active-page="onUpdateActivePage" expandable > <template #page> <div style="padding:16px"> <bento-header v-bind="headerProps"></bento-header> <bento-data-grid v-bind="gridArgs"> <template #item-paymentMethods="{item}"> <div> {{ item.paymentMethods }} <bento-button variant="tertiary" @click="openInspector(item)" style="float:right" > <template #iconLeft> <search-icon :svg-title="inspectorButtonTitle" /> </template> </bento-button> <bento-button v-if="item.paymentMethods === 'Visa'" variant="tertiary" @click="openInspector(item, true)" style="float:right; margin-right: 10px;" > <template #iconLeft> <shield-icon :svg-title="inspectorButtonTitle" /> </template> </bento-button> </div> </template> </bento-data-grid> </div> </template> <template #content> <bento-inspector-page page-id="visa-secure" title="Visa secure with a wrap text title" :actions="actions"> <p style="margin-top: 0;">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus accumsan dui sed nulla tincidunt fermentum. Interdum et malesuada fames ac ante ipsum primis in faucibus. Donec tristique erat ut tincidunt aliquet. Nunc placerat aliquam consequat. Suspendisse non leo ut sem rutrum aliquet et at ex. Mauris facilisis arcu nec ipsum auctor, imperdiet fringilla tortor placerat. Aenean bibendum dui id velit ornare, eget porta tortor euismod.</p> <p>Phasellus posuere nec enim sed sodales. Mauris aliquet tristique nisl vitae sollicitudin. Aliquam nec diam lorem. Pellentesque eu ex condimentum, rhoncus ipsum et, varius elit. In lacinia elit eros, vel euismod nisl vestibulum et. Vivamus quis dui vel nibh facilisis venenatis nec molestie mi. Praesent porta suscipit elit, ut porta risus. Nunc gravida euismod convallis. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris in ornare lorem.</p> <p>Ut nec nisl erat. Nunc arcu ante, lobortis eu lorem eget, maximus cursus nunc. Etiam tortor tellus, condimentum a libero in, pellentesque dignissim turpis. Phasellus ac sodales dui. Proin at auctor leo. Nunc vehicula ut lorem eget tempus. Quisque lectus dui, auctor vitae ipsum nec, dapibus mollis justo. Donec purus lectus, malesuada non laoreet eu, sodales ac turpis.</p> <p>Mauris venenatis, eros et fringilla vulputate, ante velit euismod nisi, blandit volutpat eros elit nec mi. Donec rutrum felis vel nibh aliquet finibus. Mauris gravida dapibus dui, ut consectetur risus. Sed sagittis dignissim lacus nec ornare. Fusce ullamcorper pretium metus id congue. Vestibulum pellentesque sem orci, eget bibendum magna maximus vel. Sed id lectus ullamcorper, imperdiet velit ac, porttitor est. Duis risus nisi, facilisis at pharetra pellentesque, hendrerit malesuada sapien. Aenean tellus nisl, rhoncus sed nisl quis, sagittis fermentum mauris. Etiam rutrum leo et ligula varius volutpat. Fusce semper hendrerit accumsan. Nam efficitur vitae velit nec condimentum. Vestibulum pellentesque nec purus nec tristique. In libero ante, dictum at urna in, interdum pretium felis. Ut scelerisque sed sem a malesuada.</p> <p>Vestibulum elementum malesuada arcu, id aliquet nisi blandit eu. Proin vel sagittis odio. Maecenas sollicitudin neque non urna malesuada, at convallis sem vulputate. Ut volutpat leo at tincidunt malesuada. Quisque pharetra turpis eget velit commodo, eu scelerisque risus ultrices. Etiam non faucibus libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Mauris in odio massa.</p> </bento-inspector-page> <bento-inspector-page page-id="visa-secure-sub" title="Sub Visa secure" previous-page="visa-secure" > I'm a sub page of visa </bento-inspector-page> <bento-inspector-page page-id="payment-details" :title="title"> <div style="display: flex; flex-direction: column; height: 100%"> <bento-structured-list style="flex-grow: 1"> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> </bento-structured-list> </div> </bento-inspector-page> </template> </bento-inspector> `, setup(props) { const args = isVue2 ? props : _args; const isOpen = ref(args.isOpen); const title = ref(args.title); const activePage = ref(); const inspectorButtonTitle = computed(() => (isOpen.value ? 'Move to inspector' : 'Open inspector')); const gridData = [ { id: 1, paymentMethods: 'Visa', amount: '1,234 USD', numeric: '54,213' }, { id: 2, paymentMethods: 'MasterCard', amount: '1,234 USD', numeric: '54,213' }, { id: 3, paymentMethods: 'iDeal', amount: '1,234 USD', numeric: '54,213' }, { id: 4, paymentMethods: 'Klarna', amount: '1,234 USD', numeric: '54,213' }, { id: 5, paymentMethods: 'Apple Pay', amount: '1,234 USD', numeric: '54,213' }, ]; const onUpdateActivePage = (newActivePage: string) => { activePage.value = newActivePage; }; const openInspector = (value, isSecureView) => { if (isSecureView) { activePage.value = 'visa-secure'; } else { activePage.value = 'payment-details'; title.value = value.paymentMethods; } isOpen.value = true; }; const gridColumns = [ { field: 'paymentMethods', label: 'Payment methods', minWidth: 200, mandatory: true, }, { field: 'amount', label: 'Amount', numeric: true, minWidth: 300 }, { field: 'numeric', label: 'Numeric', numeric: true, minWidth: 300 }, ]; const gridArgs = { data: gridData, columns: gridColumns, pagination: {}, }; const actions = [ { title: 'Visa sub', event: () => { activePage.value = 'visa-secure-sub'; }, }, ]; const headerProps = { title: 'Payment methods list', actions: [ { title: 'Primary action', event: () => undefined }, { title: 'Secondary action', icon: PlusIcon, event: () => undefined }, { title: 'Tertiary action', icon: PlusIcon, event: () => undefined }, { title: 'Another action', event: () => undefined }, ], lastUpdated: new Date(), }; watch( () => args.isOpen, () => { isOpen.value = args.isOpen; } ); watch( () => args.title, () => { title.value = args.title; } ); return { args, isOpen, actions, gridArgs, title, headerProps, activePage, openInspector, onUpdateActivePage, inspectorButtonTitle, }; }, }), args: DEFAULT_PROPS, parameters: storybookDocsParameter(InspectorWithMultiplePagesExample), };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const BentoInspectorSizeOptions = ['small', 'large'] as const; export type BentoInspectorRelativeLayout = 'push-content' | 'overlay-content'; export type BentoInspectorSize = (typeof BentoInspectorSizeOptions)[number]; export interface BentoInspectorProps { /** * ID of the current active inspector page */ activePage?: string; /** * Shows the Expand icon in the inspector */ expandable?: boolean; /** * Opens and closes the inspector */ isOpen?: boolean; /** * Shows loading indicator for the content */ loading?: boolean; /** * Determines the size of the inspector */ size?: BentoInspectorSize; /** * The title of the component */ title: string; /** * The appearance of the inspector relative to the main content. Defaults to `push-content`. * * - `push-content`: The inspector pushes the main content to the side, resizing it to fit. * - `overlay-content`: The inspector opens on top of the main content, without affecting its layout. */ relativeLayout?: BentoInspectorRelativeLayout; /** * An optional HTMLElement to focus on when returning focus from the inspector * to the main page content (e.g. via the `Ctrl+Shift+S` keyboard shortcut). * * By default, the inspector attempts to return focus to the element that was active before it opened. * However, if that trigger element is removed from the DOM (e.g., via a `v-if`), focus cannot be returned. * * Providing a `pageFocusAnchor` (e.g. role="gridcell") ensures that the * focus context is preserved for accessibility. */ pageFocusAnchor?: HTMLElement; }
|
|
1
|
+
export const BentoInspectorSizeOptions = ['small', 'large'] as const; export type BentoInspectorRelativeLayout = 'push-content' | 'overlay-content'; export type BentoInspectorSize = (typeof BentoInspectorSizeOptions)[number]; export interface BentoInspectorProps { /** * ID of the current active inspector page */ activePage?: string; /** * Shows the Expand icon in the inspector */ expandable?: boolean; /** * Opens and closes the inspector */ isOpen?: boolean; /** * Shows loading indicator for the content */ loading?: boolean; /** * Determines the size of the inspector */ size?: BentoInspectorSize; /** * The title of the component. * Used for accessibility and the single page inspector. * When using `bento-inspector-page` this title is only used for accessibility purposes */ title: string; /** * The appearance of the inspector relative to the main content. Defaults to `push-content`. * * - `push-content`: The inspector pushes the main content to the side, resizing it to fit. * - `overlay-content`: The inspector opens on top of the main content, without affecting its layout. */ relativeLayout?: BentoInspectorRelativeLayout; /** * An optional HTMLElement to focus on when returning focus from the inspector * to the main page content (e.g. via the `Ctrl+Shift+S` keyboard shortcut). * * By default, the inspector attempts to return focus to the element that was active before it opened. * However, if that trigger element is removed from the DOM (e.g., via a `v-if`), focus cannot be returned. * * Providing a `pageFocusAnchor` (e.g. role="gridcell") ensures that the * focus context is preserved for accessibility. */ pageFocusAnchor?: HTMLElement; }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div ref="wrapper" class="b-inspector" :class="conditionalClass" @keydown="handleSectionFocus"> <div ref="page" class="b-inspector__page-wrapper"> <div class="b-inspector__page"> <slot name="page"
|
|
1
|
+
<template> <div ref="wrapper" class="b-inspector" :class="conditionalClass" @keydown="handleSectionFocus"> <div ref="page" class="b-inspector__page-wrapper"> <div class="b-inspector__page"> <slot name="page"></slot> </div> </div> <Transition name="b-inspector__animation" mode="out-in" @before-enter="setFocus" @after-leave="setFocus"> <aside v-if="isOpen" ref="inspector" class="b-inspector__container-wrapper" :aria-label="inspectorAriaLabel" @keydown.tab="handleTabNavigation" > <bento-focus-trap :disabled="!isCompact" class="b-inspector__focus-trap"> <div class="b-inspector__container" :class="computedContainerClasses"> <template v-if="!isCompact"> <bento-button class="b-inspector__return-content" variant="secondary" condensed :aria-describedby="returnToMainContentDescriptionId" @click="moveFocusBetweenSections" > {{ t('returnToMainContent') }} </bento-button> <span :id="returnToMainContentDescriptionId" class="b-inspector__return-content-description" > {{ t('returnToMainContentDescription') }} </span> </template> <div v-if="pages.length === 0" class="b-inspector__header"> <bento-typography el="h2" variant="title" class="b-inspector__title"> {{ title }} </bento-typography> </div> <div class="b-inspector__icons"> <bento-button v-if="expandable" variant="tertiary" @click="emit('expand')"> <template #iconLeft> <expand-icon :svg-title="t('expand')" /> </template> </bento-button> <bento-button variant="tertiary" @click="emit('update:is-open', false)"> <template #iconLeft> <cross-icon :svg-title="t('close')" /> </template> </bento-button> </div> <div class="b-inspector__content"> <bento-loading-indicator v-if="loading" large class="b-inspector__loading-indicator" /> <slot v-else name="content"></slot> </div> </div> </bento-focus-trap> </aside> </Transition> </div> </template> <script setup lang="ts"> import { computed, nextTick, provide, ref, toRef, watch } from 'vue'; import { BentoTypography } from '@/components/typography'; import { BentoButton } from '@/components/button'; import { BentoLoadingIndicator } from '@/components/loading-indicator'; import { BentoFocusTrap } from '@/components/focus-trap'; import ExpandIcon from '@adyen/ui-assets-icons-16/vue/expand'; import CrossIcon from '@adyen/ui-assets-icons-16/vue/cross'; import { generateUid } from '@/core/utils/ts'; import { useI18n } from '@/utils/ts/i18n'; import { deprecate } from '@/utils/ts/deprecate'; import { getNextFocusableElement } from '@/utils/ts/focus-trap.utils'; import { useElementSize } from '@vueuse/core'; import moveFocusInside, { focusInside, getActiveElement } from 'focus-lock'; import { type BentoInspectorProps } from './inspector.types'; import { useFocusHistory } from './composables/use-focus-history/use-focus-history'; import { DIALOG_PAGE_CONFIG_INJECTION_KEY, DIALOG_PAGE_REGISTER_PAGE_INJECTION_KEY, } from '@/internal/dialog-page/dialog-page.keys'; import { BentoDialogPageAnimation, type BentoDialogPageConfigData, type BentoDialogPageRegisterPage, } from '@/internal/dialog-page/dialog-page.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const props = withDefaults(defineProps<BentoInspectorProps>(), { activePage: '', expandable: false, isOpen: false, loading: false, size: 'small', relativeLayout: 'push-content', pageFocusAnchor: null, }); const emit = defineEmits<{ /** * Emits update to keep isOpen prop in sync */ (e: 'update:is-open', value: boolean): void; /** * Emits update to keep activePage prop in sync */ (e: 'update:active-page', value: string): void; /** * Emits event when user click expand button */ (e: 'expand'): void; }>(); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const returnToMainContentDescriptionId = generateUid('returnToMainContentDescription'); const wrapper = ref<HTMLElement>(null); const inspector = ref<HTMLElement>(null); const page = ref<HTMLElement>(null); const inspectorLastFocusedElement = ref<HTMLElement>(null); const internalActivePage = ref(props.activePage); // Multiple page navigation const pages = ref<Array<string>>([]); const previousPages = ref([]); const animation = ref(BentoDialogPageAnimation.TRANSITION_LEVEL_DEEPER); const isAnimationActive = ref(false); const { width: wrapperWidth } = useElementSize(wrapper); const computedContainerClasses = computed(() => { return { 'b-inspector__container--padded': pages.value.length === 0, // No pages are registered }; }); const isCompact = computed(() => { const COMPACT_THRESHOLD_SMALL = 740; const COMPACT_THRESHOLD_LARGE = 915; // small inspector to occupy full screen if < 740px // large inspector to occupy full screen if < 915px const threshold = props.size === 'small' ? COMPACT_THRESHOLD_SMALL : COMPACT_THRESHOLD_LARGE; return wrapperWidth.value > 0 && wrapperWidth.value < threshold; }); const computedPageLastFocusedElement = computed(() => props.pageFocusAnchor); const conditionalClass = computed(() => ({ [`b-inspector--${props.size}`]: true, [`b-inspector--${props.relativeLayout}`]: true, [`b-inspector--is-compact`]: isCompact.value, [`b-inspector--multi-paged`]: pages.value.length, })); const inspectorAriaLabel = computed(() => `${t('inspector')}, ${props.title}`); const { findValidFocusFallback } = useFocusHistory(page); const setFocus = async () => { if (props.isOpen) { await nextTick(); moveFocusInside(inspector.value, computedPageLastFocusedElement.value); } else { // This doesn't work with data grid cell actions, as the button gets destroyed once is not being hovered. // To be reviewed after LevelAccess revision focusElement(computedPageLastFocusedElement.value ?? findValidFocusFallback()); } }; const handleTabNavigation = event => { if (isCompact.value) { return; } const direction = event.shiftKey ? -1 : 1; const focusableEl = getNextFocusableElement(direction, inspector.value); // If the nextfocusableEl is the same as the activeElement that means it's the last focusable element // inside the inspector, so the focus is moved back to the last focused element on the page if (getActiveElement() === focusableEl) { event.preventDefault(); inspectorLastFocusedElement.value = document.activeElement as HTMLElement; focusElement(computedPageLastFocusedElement.value ?? findValidFocusFallback()); // Set active-page as undefined when focus is not on page emit('update:active-page', undefined); } }; const handleSectionFocus = event => { const sectionShortcut = event.shiftKey && event.ctrlKey && event.key === 'S'; if (props.isOpen && !isCompact.value && sectionShortcut) { moveFocusBetweenSections(); } }; const moveFocusBetweenSections = () => { if (focusInside(inspector.value)) { inspectorLastFocusedElement.value = document.activeElement as HTMLElement; focusElement(computedPageLastFocusedElement.value ?? findValidFocusFallback()); // Set active-page as undefined when focus is not on page emit('update:active-page', undefined); } else { focusElement(inspectorLastFocusedElement.value); } }; const focusElement = (element: HTMLElement) => { if (!element) { return; } element?.focus(); if (document.activeElement !== element) { focusElement(element.parentElement); } }; /** * Multiple page navigation and page registration */ const updatePage = page => emit('update:active-page', page); // unregister page if removed / destroyed const unregisterPage = (pageIdToRemove: string) => () => { pages.value = pages.value?.filter(pageId => pageId !== pageIdToRemove); }; const registerPage: BentoDialogPageRegisterPage = pageId => { pages.value.push(pageId); return { unregisterPage: unregisterPage(pageId) }; }; provide<BentoDialogPageConfigData>(DIALOG_PAGE_CONFIG_INJECTION_KEY, { activePage: internalActivePage, animation, isAnimationActive, isOpen: toRef(props, 'isOpen'), pages, previousPages, updatePage, }); provide(DIALOG_PAGE_REGISTER_PAGE_INJECTION_KEY, registerPage); watch( () => props.activePage, () => { if (props.activePage) { internalActivePage.value = props.activePage; setFocus(); } } ); if (!props.activePage) { deprecate( 'BentoInspector "active-page" as optional', `Make sure to set the "active-page" prop and keep it in sync with "update:active-page". "active-page" prop will be required in the next version`, '2.0.0' ); } </script> <script lang="ts"> /** * Inspector allows users to access additional information or details of an object/item. The inspector opens adjacent to the main content, pushing it aside to maintain visibility and accessibility to both the main content and inspector. * * @example * import { BentoInspector } from '@adyen/bento-vue2'; * * export default { * components: { BentoInspector }, * template: ` * <bento-inspector * title="Inspector" * size="large" * :is-open="isOpen" * :active-page="activePage" * expandable * @update:is-open="isOpen = $event" * @update:active-page="activePage = $event" * > * <template #page>Content of the page</template> * <template #content>Content of the inspector</template> * </bento-inspector> * ` * } */ export default { i18n: { messages } }; </script> <style lang="scss" scoped src="./inspector.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <Transition :name="animation" mode="out-in" @after-leave="updateAnimation" @leave="startAnimation"> <div v-if="isOpen && showPage" class="b-dialog-page"> <div v-if="hasSlot('header')" class="b-dialog-page__header" :class="headerConditionalClasses"> <div v-if="showBackButton" class="b-dialog-page__back-button" :style="backButtonStyling" data-testid="back-button" > <bento-button variant="tertiary" @click="goToPreviousPage(previousPage)"> <arrow-left-icon :svg-title="t('goToPreviousPage')" /> </bento-button> </div> <slot name="header"></slot> </div> <div v-if="hasSlot('content')" ref="contentElement" class="b-dialog-page__content" :tabindex="isScrollable ? 0 : null" @scroll="onScroll" > <slot name="content" /> </div> <div v-if="hasSlot('footer')" class="b-dialog-page__footer" :class="footerConditionalClasses"> <slot name="footer" /> </div> </div> </Transition> </template> <script setup lang="ts"> import { computed, inject, nextTick, onMounted, onUnmounted, type PropType, ref, useSlots, watch } from 'vue'; import { BentoButton } from '@/components/button'; import ArrowLeftIcon from '@adyen/ui-assets-icons-16/vue/arrow-left'; import { useHasSlot } from '@/composables/use-has-slot/use-has-slot'; import { useI18n } from '@/utils/ts/i18n'; import { isDevelopmentEnviroment } from '@/utils/ts/dev-environment'; import { BentoDialogPageAnimation, type BentoDialogPageConfigData, type BentoDialogPageRegisterPage, } from './dialog-page.types'; import type { CSSProperties } from 'vue/types/jsx.d.ts'; import { DIALOG_PAGE_CONFIG_INJECTION_KEY, DIALOG_PAGE_REGISTER_PAGE_INJECTION_KEY } from './dialog-page.keys'; import messages from './messages.json'; import { useScroll } from '@vueuse/core'; type MessageSchema = (typeof messages)['en-US']; const props = defineProps({ /** *
|
|
1
|
+
<template> <Transition :name="animation" mode="out-in" @after-leave="updateAnimation" @leave="startAnimation"> <div v-if="isOpen && showPage" class="b-dialog-page" :class="computedClasses"> <div v-if="hasSlot('header')" class="b-dialog-page__header" :class="headerConditionalClasses"> <div v-if="showBackButton" class="b-dialog-page__back-button" :style="backButtonStyling" data-testid="back-button" > <bento-button variant="tertiary" @click="goToPreviousPage(previousPage)"> <arrow-left-icon :svg-title="t('goToPreviousPage')" /> </bento-button> </div> <slot name="header"></slot> </div> <div v-if="hasSlot('content')" ref="contentElement" class="b-dialog-page__content" :tabindex="isScrollable ? 0 : null" @scroll="onScroll" > <slot name="content" /> </div> <div v-if="hasSlot('footer')" class="b-dialog-page__footer" :class="footerConditionalClasses"> <slot name="footer" /> </div> </div> </Transition> </template> <script setup lang="ts"> import { computed, inject, nextTick, onMounted, onUnmounted, type PropType, ref, useSlots, watch } from 'vue'; import { BentoButton } from '@/components/button'; import ArrowLeftIcon from '@adyen/ui-assets-icons-16/vue/arrow-left'; import { useHasSlot } from '@/composables/use-has-slot/use-has-slot'; import { useI18n } from '@/utils/ts/i18n'; import { isDevelopmentEnviroment } from '@/utils/ts/dev-environment'; import { BentoDialogPageAnimation, type BentoDialogPageConfigData, type BentoDialogPageRegisterPage, } from './dialog-page.types'; import type { CSSProperties } from 'vue/types/jsx.d.ts'; import { DIALOG_PAGE_CONFIG_INJECTION_KEY, DIALOG_PAGE_REGISTER_PAGE_INJECTION_KEY } from './dialog-page.keys'; import messages from './messages.json'; import { useScroll } from '@vueuse/core'; type MessageSchema = (typeof messages)['en-US']; const props = defineProps({ /** * Sets `position: absolute` on the dialog page root element. * Enabled by default as it is used by default in most places. * Inspector requires it to not be absolute positioned. * @default true */ absolutePosition: { type: Boolean, default: true, }, /** * Show header and actions border */ alwaysShowBorder: { type: Boolean, default: false, }, /** * Pass custom styling to the back button to position it correctly with the header slot */ backButtonStyling: { type: Object as PropType<CSSProperties>, default: null }, /** * Identifier of the page, used with activePage prop to handle navigation */ pageId: { type: String, default: undefined, }, /** * ID of the page for the back button to navigate to. Only use it to overwrite the default back behavior * Used when the back button should not take the user to the natural previous page * Set to `null` to hide back button in the page */ previousPage: { type: String, default: undefined, }, }); const emit = defineEmits<{ /** * Fires when the animation ends */ (e: 'animationend'): void; }>(); const slots = useSlots(); const hasSlot = useHasSlot(slots); const { t } = useI18n<{ message: MessageSchema }>({ messages }); // Refs const contentElement = ref<HTMLDivElement>(null); const hasScroll = ref(false); const isMounted = ref(false); const { arrivedState } = useScroll(contentElement); // Logic to choose which page is active/shown const showPage = computed(() => (activePage.value ? activePage.value === props.pageId : true)); const computedClasses = computed(() => ({ 'b-dialog-page--absolute-position': props.absolutePosition, })); // Scrolling styling logic const headerConditionalClasses = computed(() => ({ 'b-dialog-page__header--with-border': props.alwaysShowBorder || (hasScroll.value && !arrivedState.top), })); const footerConditionalClasses = computed(() => ({ 'b-dialog-page__footer--with-border': props.alwaysShowBorder || (hasScroll.value && !arrivedState.bottom), })); const isScrollable = computed(() => !(arrivedState.bottom && arrivedState.top)); const onScroll = () => { if (!contentElement.value) { return; } }; const calculateScroll = async watchedValue => { if (!watchedValue) { return; } // Wait for content to be rendered to be able to calculate the height await nextTick(); if (!isMounted.value) { return; } hasScroll.value = contentElement.value && contentElement.value.scrollHeight > contentElement.value.clientHeight; }; const { activePage, animation, isAnimationActive, isOpen, pages, previousPages, updatePage } = inject<BentoDialogPageConfigData>(DIALOG_PAGE_CONFIG_INJECTION_KEY); watch(() => isOpen.value, calculateScroll, { immediate: true }); watch(() => showPage.value, calculateScroll, { immediate: true }); // Logic to determine whether the back button should be shown on the page const showBackButton = computed(() => { if (props.previousPage) { return true; } if (props.previousPage === null) { return false; } return hasPreviousPages.value && showPage.value; }); // Animation const updateAnimation = () => { animation.value = BentoDialogPageAnimation.TRANSITION_LEVEL_DEEPER; if (isAnimationActive) { isAnimationActive.value = false; } emit('animationend'); }; const startAnimation = () => { if (isAnimationActive) { isAnimationActive.value = true; } }; // Previous page logic const hasPreviousPages = computed(() => previousPages.value.length > 1); const lastPage = computed(() => previousPages.value[previousPages.value.length - 1]); const isActivePageValid = computed(() => pages.value.some(page => page === activePage.value)); const goToPreviousPage = async (page: string) => { if (page) { const index = previousPages.value.indexOf(page); if (index !== -1) { previousPages.value = previousPages.value.slice(0, index + 1); } else { previousPages.value = [page]; } } else { previousPages.value.pop(); } animation.value = BentoDialogPageAnimation.TRANSITION_LEVEL_UPPER; await nextTick(); if (!isMounted.value) { return; } updatePage(lastPage.value); }; // Add new page to the previousPage array to keep track of user navigation watch( () => activePage.value, async () => { await nextTick(); if (!isMounted.value || !isOpen.value) { return; } if (!!activePage.value && !isActivePageValid.value) { if (isDevelopmentEnviroment) { // eslint-disable-next-line no-console console.error( `The activePage passed "${activePage.value}" does not match the id of any of the pages` ); return; } } if (activePage.value !== lastPage.value) { previousPages.value.push(activePage.value); } }, { immediate: true } ); // Register page to parent component and inject data from parent component const registerPage = inject<BentoDialogPageRegisterPage>(DIALOG_PAGE_REGISTER_PAGE_INJECTION_KEY); const { unregisterPage } = registerPage(props.pageId); onMounted(() => { isMounted.value = true; }); // Unregister page on unmounting the page onUnmounted(() => { isMounted.value = false; unregisterPage(); }); </script> <script lang="ts"> /** * DialogPage is an internal component used to render different pages of dialog components (Modal, ModalFullscreen and Sidepanel) * * @example * import { DialogPage } from '@/internal'; * * export default { * components: { DialogPage }, * template: ` * <dialog-page pageId='step1' always-show-border> * <template #header>Header</template> * <template #content>Content</template> * <template #footer>Footer</template> * </dialog-page> * ` * } */ export default { i18n: { messages }, }; </script> <style lang="scss" scoped src="./dialog-page.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" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export interface BentoTagProps { /** * The label text content of the Tag. */ label?: string; /** * The number of characters to limit the number of displayed characters for the tag text, truncating it with an ellipsis of three dots "...". */ truncate?: number; /** * The variant variant of tag */ variant?: BentoTagVariant | `${BentoTagVariant}`; }
|
|
1
|
+
export type BentoTagVariant = 'grey' | 'blue' | 'green' | 'orange' | 'red' | 'white'; /** * @deprecated Since v2.0.0. Use string literal instead. */ export const BentoTagVariant: Record<string, BentoTagVariant> = { GREY: 'grey', BLUE: 'blue', GREEN: 'green', ORANGE: 'orange', RED: 'red', WHITE: 'white', }; export interface BentoTagProps { /** * The label text content of the Tag. */ label?: string; /** * The number of characters to limit the number of displayed characters for the tag text, truncating it with an ellipsis of three dots "...". */ truncate?: number; /** * The variant variant of tag */ variant?: BentoTagVariant | `${BentoTagVariant}`; }
|