@adyen/bento-mcp 0.6.0 → 0.7.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 +11 -0
- package/README.md +7 -6
- package/bin/add-skill.js +12 -1
- package/dist/assets/components/anchor-scroller/anchor-scroller.vue +1 -1
- package/dist/assets/components/anchor-scroller/components/anchor-scroller-list/anchor-scroller-list.types.ts +1 -1
- package/dist/assets/components/anchor-scroller/components/anchor-scroller-list/anchor-scroller-list.vue +1 -1
- package/dist/assets/components/button/button.docs.mdx +24 -0
- package/dist/assets/components/button/button.vue +1 -1
- package/dist/assets/components/button/components/button-actions/button-actions.docs.mdx +14 -1
- package/dist/assets/components/date-range-picker/date-range-picker.vue +1 -1
- package/dist/assets/components/draggable/components/draggable-handle/draggable-handle.vue +1 -1
- package/dist/assets/components/dropdown/components/dropdown-options-container/dropdown-options-container.vue +1 -1
- package/dist/assets/components/dropdown/dropdown.docs.mdx +58 -0
- package/dist/assets/components/dropdown/dropdown.stories.ts +1 -1
- package/dist/assets/components/dropdown/dropdown.vue +1 -1
- package/dist/assets/components/filter-bar/components/range-filter/range-filter.types.ts +1 -0
- package/dist/assets/components/filter-bar/components/range-filter/range-filter.vue +1 -1
- package/dist/assets/components/filter-bar/filter-bar.docs.mdx +74 -1
- package/dist/assets/components/filter-bar/filter-bar.stories.ts +1 -1
- package/dist/assets/components/filter-bar/filter-bar.types.ts +1 -1
- package/dist/assets/components/filter-bar/filter-bar.vue +1 -1
- package/dist/assets/components/header/header.docs.mdx +8 -0
- package/dist/assets/components/header-with-views/header-with-views.docs.mdx +12 -0
- package/dist/assets/components/input-field/input-field.vue +1 -1
- package/dist/assets/components/internal/listbox/components/listbox-lazy-load/listbox-lazy-load.types.ts +1 -0
- package/dist/assets/components/internal/listbox/components/listbox-lazy-load/listbox-lazy-load.vue +1 -0
- package/dist/assets/components/internal/listbox/components/listbox-multi-select/components/listbox-multi-select-options/components/listbox-multi-select-category/listbox-multi-select-category.vue +1 -1
- package/dist/assets/components/internal/listbox/components/listbox-multi-select/components/listbox-multi-select-options/components/listbox-multi-select-nested-lazy-load/listbox-multi-select-nested-lazy-load.types.ts +1 -0
- package/dist/assets/components/internal/listbox/components/listbox-multi-select/components/listbox-multi-select-options/components/listbox-multi-select-nested-lazy-load/listbox-multi-select-nested-lazy-load.vue +1 -0
- package/dist/assets/components/internal/listbox/components/listbox-multi-select/components/listbox-multi-select-options/listbox-multi-select-options.vue +1 -1
- package/dist/assets/components/internal/listbox/components/listbox-multi-select/listbox-multi-select.vue +1 -1
- package/dist/assets/components/internal/listbox/components/listbox-single-select/listbox-single-select.vue +1 -1
- package/dist/assets/components/internal/listbox/listbox.vue +1 -1
- package/dist/assets/components/table-of-contents/components/table-of-contents-list/table-of-contents-list.types.ts +1 -0
- package/dist/assets/components/table-of-contents/components/table-of-contents-list/table-of-contents-list.vue +1 -0
- package/dist/assets/components/table-of-contents/table-of-contents.docs.mdx +34 -0
- package/dist/assets/components/table-of-contents/table-of-contents.stories.ts +1 -0
- package/dist/assets/components/table-of-contents/table-of-contents.types.ts +1 -0
- package/dist/assets/components/table-of-contents/table-of-contents.vue +1 -0
- package/dist/assets/components.json +1 -0
- package/dist/assets/composables/use-numeric-input/use-numeric-input.types.ts +1 -0
- package/dist/assets/composables/use-section-navigation-keyboard/use-section-navigation-keyboard.types.ts +1 -0
- package/dist/assets/deprecations.md +136 -136
- package/dist/assets/index.ts +1 -1
- package/dist/assets/usage.json +80 -79
- package/dist/assets/utils/ts/scroll-to-section/scroll-to-section.types.ts +1 -0
- package/dist/main.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.0 (2026-06-23)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- updated skill adding logic to include droid ([a8d725483](https://github.com/Adyen/bento/commit/a8d725483))
|
|
8
|
+
|
|
9
|
+
### ❤️ Thank You
|
|
10
|
+
|
|
11
|
+
- gerald
|
|
12
|
+
|
|
13
|
+
|
|
3
14
|
## 0.6.0 (2026-06-09)
|
|
4
15
|
|
|
5
16
|
### Features
|
package/README.md
CHANGED
|
@@ -34,11 +34,12 @@ To configure the MCP server, add the following to your client's settings file:
|
|
|
34
34
|
The location of this file may vary depending on your client and setup. For project-level and global configuration, you
|
|
35
35
|
can typically use:
|
|
36
36
|
|
|
37
|
-
| Client
|
|
38
|
-
|
|
|
39
|
-
| Claude
|
|
40
|
-
|
|
|
41
|
-
|
|
|
37
|
+
| Client | Local Path | Global Path |
|
|
38
|
+
| ------------- | ----------------------------- | ------------------------------------- |
|
|
39
|
+
| Claude | `.claude/settings.local.json` | `~/.claude/settings.json` |
|
|
40
|
+
| Droid/Factory | `.factory/mcp.json` | `~/.factory/mcp.json` |
|
|
41
|
+
| Gemini | `.gemini/settings.json` | `~/.gemini/settings.json` |
|
|
42
|
+
| Windsurf | `-` | `~/.codeium/windsurf/mcp_config.json` |
|
|
42
43
|
|
|
43
44
|
Once configured, you can invoke the tool by its name (`bento`).
|
|
44
45
|
|
|
@@ -51,7 +52,7 @@ The Bento AI Skill provides a "Verify & Interpret" implementation workflow. To a
|
|
|
51
52
|
Run the following command in your project root:
|
|
52
53
|
|
|
53
54
|
```bash
|
|
54
|
-
|
|
55
|
+
npm install -D @adyen/bento-mcp && npx bento-add-skill
|
|
55
56
|
```
|
|
56
57
|
|
|
57
58
|
#### For Other Agents (Claude, Cursor, etc.)
|
package/bin/add-skill.js
CHANGED
|
@@ -30,7 +30,18 @@ try {
|
|
|
30
30
|
console.log(`ℹ️ Gemini CLI: Skipping local installation (ensure you are in your project root)`);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
// 2.
|
|
33
|
+
// 2. Install for Droid (Local)
|
|
34
|
+
const droidPath = join(process.cwd(), '.factory/skills/writing-bento-components/SKILL.md');
|
|
35
|
+
try {
|
|
36
|
+
mkdirSync(dirname(droidPath), { recursive: true });
|
|
37
|
+
copyFileSync(skillSource, droidPath);
|
|
38
|
+
console.log(`✅ Droid: Added to ${droidPath}`);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
// Silently continue if Droid install fails (e.g. not in a project root)
|
|
41
|
+
console.log(`ℹ️ Droid: Skipping local installation (ensure you are in your project root)`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 3. Support for other agents via @codemcp/agentskills
|
|
34
45
|
console.log('\n🌟 Multi-Agent Support (Claude, Cursor, etc.)');
|
|
35
46
|
try {
|
|
36
47
|
// Check if agentskills is available
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div v-if="computedItems.length" class="b-anchor-scroller"> <nav ref="navigationRef" class="b-anchor-scroller__navigation" :class="navigationConditionalClasses" :aria-labelledby="navLabelId" > <fixed-scroller condensed centered> <div class="b-anchor-scroller__navigation-panel"> <bento-typography :id="navLabelId" el="span" class="b-anchor-scroller__navigation-panel-label" stronger > {{ t('jumpTo') }} </bento-typography> <anchor-scroller-list :items="computedItems" :active-index="currentActiveIndex" :scroll-offset="bottom" /> </div> </fixed-scroller> </nav> <slot /> </div> </template> <script setup lang="ts"> import { computed, ref, toRef, watch } from 'vue'; import { refDebounced, useElementBounding } from '@vueuse/core'; import BentoTypography from '@/components/typography/typography.vue'; import { FixedScroller } from '@/internal'; import { type BentoAnchorScrollerItem, type BentoAnchorScrollerProps } from './anchor-scroller.types'; import { AnchorScrollerList } from './components/anchor-scroller-list'; import { useI18n } from '@/utils/ts/i18n'; import { generateUid } from '@/core/utils/ts'; import { unrefElement } from '@/directives/click-outside/utils'; import messages from './messages.json'; import { useScrollSpy } from '
|
|
1
|
+
<template> <div v-if="computedItems.length" class="b-anchor-scroller"> <nav ref="navigationRef" class="b-anchor-scroller__navigation" :class="navigationConditionalClasses" :aria-labelledby="navLabelId" > <fixed-scroller condensed centered> <div class="b-anchor-scroller__navigation-panel"> <bento-typography :id="navLabelId" el="span" class="b-anchor-scroller__navigation-panel-label" stronger > {{ t('jumpTo') }} </bento-typography> <anchor-scroller-list :items="computedItems" :active-index="currentActiveIndex" :scroll-offset="bottom" /> </div> </fixed-scroller> </nav> <slot /> </div> </template> <script setup lang="ts"> import { computed, ref, toRef, watch } from 'vue'; import { refDebounced, useElementBounding } from '@vueuse/core'; import BentoTypography from '@/components/typography/typography.vue'; import { FixedScroller } from '@/internal'; import { type BentoAnchorScrollerItem, type BentoAnchorScrollerProps } from './anchor-scroller.types'; import { AnchorScrollerList } from './components/anchor-scroller-list'; import { useI18n } from '@/utils/ts/i18n'; import { generateUid } from '@/core/utils/ts'; import { unrefElement } from '@/directives/click-outside/utils'; import messages from './messages.json'; import { useScrollSpy } from '@/composables/use-scroll-spy'; import { printDevelopmentWarning } from '@/utils/ts/print-development-warning'; type MessageSchema = (typeof messages)['en-US']; const { t } = useI18n<{ message: MessageSchema }>({ messages }); const props = withDefaults(defineProps<BentoAnchorScrollerProps>(), { activeIndex: undefined, }); const emit = defineEmits<{ /** * Emits update event when the active index is changed */ (e: 'update:active-index', value: number): void; }>(); const navigationRef = ref(null); const computedItems = computed<Array<BentoAnchorScrollerItem>>(() => props.items.map(item => ({ ...item }))); // Skip and cache only items that are enabled const enabledItems = computed(() => props.items .map((item, originalIndex) => ({ element: unrefElement(item.elementRef), originalIndex, disabled: item.disabled, })) .filter( (item): item is { element: Element; originalIndex: number; disabled: boolean } => !item.disabled && !!item.element ) ); const { top, bottom } = useElementBounding(navigationRef); // Extract strictly the elements for the ScrollSpy to watch const scrollToTargets = computed(() => enabledItems.value.map(c => c.element)); const navigationConditionalClasses = computed(() => ({ 'b-anchor-scroller__navigation--pinned': top.value <= 1, })); const { activeIndex: relativeActiveIndex } = useScrollSpy(scrollToTargets, { default: toRef(props, 'activeIndex'), }); const debouncedActiveIndex = refDebounced(relativeActiveIndex, 200); const currentActiveIndex = computed(() => { const candidate = enabledItems.value[debouncedActiveIndex.value]; return candidate ? candidate.originalIndex : 0; }); watch(currentActiveIndex, val => { if (Number.isFinite(val)) { emit('update:active-index', val); } }); watch( () => props.activeIndex, val => { if (Number.isFinite(val) && computedItems.value[val].disabled) { printDevelopmentWarning(`[bento-anchor-scroller] activeIndex should not be a disabled item`); } } ); const navLabelId = generateUid('bento-anchor-scroller-label'); </script> <script lang="ts"> /** * A navigation component that tracks page scroll and allows jumping to sections. * * @example * import { BentoAnchorScroller } from '@adyen/bento-vue2'; * import { ref } from 'vue'; * * export default { * components: { BentoAnchorScroller }, * template: ` * <bento-anchor-scroller :items="items"> * <section ref="section1">Section 1</section> * <section ref="section2">Section 2</section> * </bento-anchor-scroller> * `, * setup() { * const section1 = ref(null); * const section2 = ref(null); * const items = ref([ * { title: 'Section 1', elementRef: section1 }, * { title: 'Section 2', elementRef: section2 }, * ]); * return { * items, * section1, * section2, * } * } * } */ export default { i18n: { messages }, name: 'bento-anchor-scroller', }; </script> <style lang="scss" scoped src="./anchor-scroller.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type SectionNavigationItem } from '@/types/section-navigation'; export interface AnchorScrollerListItem extends SectionNavigationItem { disabled?: boolean; } export interface AnchorScrollerListProps { activeIndex?: number; items: Array<AnchorScrollerListItem>; scrollOffset?: number; }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <ul class="b-anchor-scroller-list" @keydown="onKeydownNavigation"> <li v-for="({ title, disabled, elementRef }, index) in items" :key="index" class="b-anchor-scroller-list__item"> <bento-button ref="
|
|
1
|
+
<template> <ul class="b-anchor-scroller-list" @keydown="onKeydownNavigation"> <li v-for="({ title, disabled, elementRef }, index) in items" :key="index" class="b-anchor-scroller-list__item"> <bento-button ref="navigationItemRefs" :aria-current="activeIndex === index ? 'location' : null" class="b-anchor-scroller-list__item-button" :class="{ 'b-anchor-scroller-list__item-button--active': activeIndex === index, 'b-anchor-scroller-list__item-button--disabled': disabled, }" :aria-disabled="disabled" :tabindex="isFocusable(index) ? null : '-1'" variant="tertiary-with-background" @click="onScrollToSection(elementRef, disabled)" @keydown.space.native.prevent="onScrollToSection(elementRef, disabled)" @keydown.enter.native.prevent="onScrollToSection(elementRef, disabled)" > {{ title }} </bento-button> </li> </ul> </template> <script setup lang="ts"> import BentoButton from '@/components/button/button.vue'; import { useSectionNavigationKeyboard } from '@/composables/use-section-navigation-keyboard'; import { scrollToSection, type ScrollToSectionTarget } from '@/utils/ts/scroll-to-section'; import { type AnchorScrollerListProps } from './anchor-scroller-list.types'; import { computed, toRef } from 'vue'; const props = withDefaults(defineProps<AnchorScrollerListProps>(), { activeIndex: undefined, scrollOffset: undefined, }); const navigationItems = computed(() => props.items); const { isFocusable, navigationItemRefs, onKeydownNavigation } = useSectionNavigationKeyboard(navigationItems, { isDisabled: item => item.disabled, initialIndex: toRef(props, 'activeIndex'), }); const onScrollToSection = ( elementRef: AnchorScrollerListProps['items'][number]['elementRef'], disabled?: boolean ) => scrollToSection(elementRef as ScrollToSectionTarget, { disabled, scrollOffset: props.scrollOffset, warningPrefix: 'bento-anchor-scroller', }); </script> <style lang="scss" scoped src="./anchor-scroller-list.scss" />
|
|
@@ -121,6 +121,30 @@ page. The following keys trigger the following actions:
|
|
|
121
121
|
| `Space` | Activates the button, triggering the click event. |
|
|
122
122
|
| `Enter` | Activates the button, triggering the click event. |
|
|
123
123
|
|
|
124
|
+
### Accessible Disabled State (`aria-disabled`)
|
|
125
|
+
|
|
126
|
+
By default, a button with the `disabled` prop uses the native HTML `disabled` attribute, which removes it from the tab
|
|
127
|
+
order and prevents all pointer events. This can be problematic when the button is used as a target for components like
|
|
128
|
+
`bento-popover` or `bento-tooltip`, because the popover or tooltip cannot be triggered on a non-interactive element.
|
|
129
|
+
|
|
130
|
+
To keep a disabled button **focusable and hoverable** while still appearing and behaving as disabled, use the
|
|
131
|
+
`aria-disabled` attribute instead of the `disabled` prop:
|
|
132
|
+
|
|
133
|
+
```html
|
|
134
|
+
<!-- Native disabled: not focusable, no hover events -->
|
|
135
|
+
<bento-button disabled>Submit</bento-button>
|
|
136
|
+
|
|
137
|
+
<!-- Accessible disabled: focusable, hoverable, clicks suppressed -->
|
|
138
|
+
<bento-button aria-disabled="true">Submit</bento-button>
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
When `aria-disabled` is set, the button will:
|
|
142
|
+
|
|
143
|
+
- Render `aria-disabled="true"` instead of the native `disabled` attribute.
|
|
144
|
+
- Apply the same disabled visual styling (`b-button--disabled`).
|
|
145
|
+
- Suppress click events (clicks are intercepted and stopped).
|
|
146
|
+
- Remain in the tab order and respond to hover, allowing popovers and tooltips to activate.
|
|
147
|
+
|
|
124
148
|
### Roles, States, and Properties
|
|
125
149
|
|
|
126
150
|
Adhering to the Adhering to the [WAI-ARIA 1.2 standard](https://www.w3.org/TR/wai-aria-1.2/) we follow the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <component :is="componentType" v-bind="componentProps" class="b-button" :class="{ ...conditionalClasses, ...getIconOnlyClass() }" @click.capture="onClickButton" > <span v-if="hasSlot('iconLeft')" class="b-button__icon-left" data-testid="icon-button-left" :aria-hidden="hasSlot('default') ? 'true' : null" > <slot name="iconLeft" /> </span> <bento-typography v-if="hasSlot('default')" el="span" variant="body" stronger class="b-button__label"> <slot /> </bento-typography> <span v-if="hasSlot('iconRight')" class="b-button__icon-right" data-testid="icon-button-right" :aria-hidden="hasSlot('default') ? 'true' : null" > <slot name="iconRight" /> </span> </component> </template> <script setup lang="ts"> import { computed, type PropType, useAttrs, useSlots } from 'vue'; import { BentoButtonPropClass, BentoButtonVariant } from './button.types'; import { BentoTypography } from '@/components/typography'; import { useHasSlot } from '@/composables/use-has-slot/use-has-slot'; import { type Booleanish } from '../../types/prop-types'; import type { RouterLinkProps } from 'vue-router/types/router.d.ts'; import type { AnchorHTMLAttributes, ButtonHTMLAttributes } from 'vue/types/jsx.d.ts'; import { BentoLink, BentoLinkVariant } from '@/components/link'; // TODO: replace all this code when migrated to Vue 3 is complete as this is the only // current way to extend the `router-link` component in Vue 2 and Vue Router 3 export interface LinkProps extends RouterLinkProps, AnchorHTMLAttributes { args?: { props: RouterLinkProps }; /** * Set the rendered link to a regular external anchor link. */ external: boolean; } const props = defineProps({ /** * Reduces sizing of button */ condensed: { type: Boolean, default: false }, /** * Displays button as critical */ critical: { type: Boolean, default: false }, /** * Disables button functionality */ disabled: { type: Boolean, default: false }, /** * Sets color set to inverse */ inverse: { type: Boolean, default: false }, /** * Object containing props passed on to the BentoLink component. @see BentoLink documentation for which properties you can pass. */ link: { type: Object, default: undefined }, /** * The default behavior of the button. * Possible values are: 'button', 'reset', 'submit' */ type: { type: String as PropType<ButtonHTMLAttributes['type']>, default: 'button', }, /** * Types of flavour of Button * @values primary, secondary, tertiary, tertiary-with-background */ variant: { type: String as PropType<BentoButtonVariant | `${BentoButtonVariant}`>, default: BentoButtonVariant.PRIMARY, validator: (value: BentoButtonVariant) => Object.values(BentoButtonVariant).includes(value), }, }); const emit = defineEmits<{ /** * Emitted when the element has lost focus */ (e: 'blur'); /** * Emitted when the button is clicked */ (e: 'click', event: MouseEvent); /** * Emitted when the element has received focus */ (e: 'focus'); /** * Emitted when the "ESC" key is pressed */ (e: 'keydown.esc'); }>(); const attrs = useAttrs(); const slots = useSlots(); const hasSlot = useHasSlot(slots); const componentType = computed(() => { if (props?.link !== undefined) { /** * Display a "span" with role "link" and "aria-disabled=true" * if the link is disabled. * @link https://www.scottohara.me/blog/2021/05/28/disabled-links.html */ return
|
|
1
|
+
<template> <component :is="componentType" v-bind="componentProps" class="b-button" :class="{ ...conditionalClasses, ...getIconOnlyClass() }" @click.capture="onClickButton" > <span v-if="hasSlot('iconLeft')" class="b-button__icon-left" data-testid="icon-button-left" :aria-hidden="hasSlot('default') ? 'true' : null" > <slot name="iconLeft" /> </span> <bento-typography v-if="hasSlot('default')" el="span" variant="body" stronger class="b-button__label"> <slot /> </bento-typography> <span v-if="hasSlot('iconRight')" class="b-button__icon-right" data-testid="icon-button-right" :aria-hidden="hasSlot('default') ? 'true' : null" > <slot name="iconRight" /> </span> </component> </template> <script setup lang="ts"> import { computed, type PropType, useAttrs, useSlots } from 'vue'; import { BentoButtonPropClass, BentoButtonVariant } from './button.types'; import { BentoTypography } from '@/components/typography'; import { useHasSlot } from '@/composables/use-has-slot/use-has-slot'; import { type Booleanish } from '../../types/prop-types'; import type { RouterLinkProps } from 'vue-router/types/router.d.ts'; import type { AnchorHTMLAttributes, ButtonHTMLAttributes } from 'vue/types/jsx.d.ts'; import { BentoLink, BentoLinkVariant } from '@/components/link'; // TODO: replace all this code when migrated to Vue 3 is complete as this is the only // current way to extend the `router-link` component in Vue 2 and Vue Router 3 export interface LinkProps extends RouterLinkProps, AnchorHTMLAttributes { args?: { props: RouterLinkProps }; /** * Set the rendered link to a regular external anchor link. */ external: boolean; } const props = defineProps({ /** * Reduces sizing of button */ condensed: { type: Boolean, default: false }, /** * Displays button as critical */ critical: { type: Boolean, default: false }, /** * Disables button functionality */ disabled: { type: Boolean, default: false }, /** * Sets color set to inverse */ inverse: { type: Boolean, default: false }, /** * Object containing props passed on to the BentoLink component. @see BentoLink documentation for which properties you can pass. */ link: { type: Object, default: undefined }, /** * The default behavior of the button. * Possible values are: 'button', 'reset', 'submit' */ type: { type: String as PropType<ButtonHTMLAttributes['type']>, default: 'button', }, /** * Types of flavour of Button * @values primary, secondary, tertiary, tertiary-with-background */ variant: { type: String as PropType<BentoButtonVariant | `${BentoButtonVariant}`>, default: BentoButtonVariant.PRIMARY, validator: (value: BentoButtonVariant) => Object.values(BentoButtonVariant).includes(value), }, }); const emit = defineEmits<{ /** * Emitted when the element has lost focus */ (e: 'blur'); /** * Emitted when the button is clicked */ (e: 'click', event: MouseEvent); /** * Emitted when the element has received focus */ (e: 'focus'); /** * Emitted when the "ESC" key is pressed */ (e: 'keydown.esc'); }>(); const attrs = useAttrs(); const slots = useSlots(); const hasSlot = useHasSlot(slots); const isAriaDisabled = computed( () => attrs['aria-disabled'] === 'true' || attrs['aria-disabled'] === '' || attrs['aria-disabled'] === true ); const isDisabled = computed(() => props.disabled || isAriaDisabled.value); const componentType = computed(() => { if (props?.link !== undefined) { /** * Display a "span" with role "link" and "aria-disabled=true" * if the link is disabled. * @link https://www.scottohara.me/blog/2021/05/28/disabled-links.html */ return isDisabled.value ? 'span' : BentoLink; } return 'button'; }); const ariaAttributes = computed(() => ({ 'aria-controls': attrs['aria-controls']?.toString() as string, 'aria-expanded': attrs['aria-expanded']?.toString() as Booleanish, })); const componentProps = computed(() => componentType.value === 'button' ? { ...ariaAttributes.value, ...(isAriaDisabled.value ? { 'aria-disabled': 'true' } : { disabled: props.disabled }), type: props.type, } : { ...props.link, disableTypography: true, variant: BentoLinkVariant.DEFAULT, isButton: true, /** * When the button is a disabled link, it's rendered as a `<span>`. * These attributes ensure it's still announced as a disabled link by screen readers. */ 'aria-disabled': isDisabled.value ? 'true' : undefined, role: isDisabled.value ? 'link' : undefined, } ); const variantClass = computed(() => ({ [`b-button--${props.variant}${props.critical ? `-${BentoButtonPropClass.DESTRUCTIVE}` : ''}`]: true, })); const conditionalClasses = computed(() => ({ ...variantClass.value, [`b-button--${BentoButtonPropClass.CONDENSED}`]: props.condensed, [`b-button--${BentoButtonPropClass.DISABLED}`]: isDisabled.value, [`b-button--${BentoButtonPropClass.INVERSE}`]: props.inverse, })); const getIconOnlyClass = () => ({ [`b-button--${BentoButtonPropClass.ICON_ONLY}`]: !hasSlot('default'), }); const onClickButton = (e: MouseEvent) => { if (isDisabled.value) { e.preventDefault(); e.stopPropagation(); } else { emit('click', e); } }; </script> <script lang="ts"> /** * Button component for all the various types of bento buttons. * * @example * import { BentoButton } from '@adyen/bento-vue2'; * import { CheckMarkIcon } from '@adyen/ui-assets-icons-16'; * * export default { * components: { BentoButton }, * template: ` * <bento-button * disabled * variant="secondary" * > * <template #iconLeft> * <check-mark svg-title="checked" /> * </template> * * Button text * * <template #iconRight> * <check-mark svg-title="checked" /> * </template> * </bento-button> * `, * } */ export default { name: 'bento-button', }; </script> <style lang="scss" scoped src="./button.scss" />
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Canvas, Meta, Source } from '@storybook/blocks';
|
|
2
2
|
import * as ButtonActionsStories from './button-actions.stories';
|
|
3
|
+
import ButtonActionsDomEventsExample from './__tests__/button-actions-dom-events-example.vue?raw';
|
|
3
4
|
|
|
4
5
|
<Meta of={ButtonActionsStories} />
|
|
5
6
|
|
|
@@ -40,7 +41,7 @@ configured:
|
|
|
40
41
|
language="typescript"
|
|
41
42
|
code={`
|
|
42
43
|
interface BentoButtonActionObject {
|
|
43
|
-
|
|
44
|
+
|
|
44
45
|
// Shared properties
|
|
45
46
|
title: string;
|
|
46
47
|
condensed?: boolean;
|
|
@@ -91,6 +92,18 @@ configured:
|
|
|
91
92
|
- `variant` (optional): the button's variant background color.
|
|
92
93
|
- See [menu](../?path=/story/components-menu--default) for the menu properties
|
|
93
94
|
|
|
95
|
+
#### Additional DOM events on action buttons
|
|
96
|
+
|
|
97
|
+
Use the action's `event` property for click handling. To listen to other button DOM events, such as `mouseenter` or
|
|
98
|
+
`mouseleave`, pass `elementRef` on the action object and attach listeners to the ref — for example, using
|
|
99
|
+
`useEventListener` from `@vueuse/core`. Note that `elementRef` provides a component instance, so when using standard
|
|
100
|
+
event listeners you need to access `.$el` to get the underlying DOM element.
|
|
101
|
+
|
|
102
|
+
<Source dark language="html" code={ButtonActionsDomEventsExample} />
|
|
103
|
+
|
|
104
|
+
When `bento-button-actions` is used inside another component, such as `bento-header` or `bento-header-with-views`, pass
|
|
105
|
+
the same `elementRef` on the action object.
|
|
106
|
+
|
|
94
107
|
#### Styling Guidelines
|
|
95
108
|
|
|
96
109
|
As a developer, you should rely on these patterns of `bento-button-actions` styling:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<!-- TODO: Remove aria props as these are part of JSX elements /vue/types/jsx.d.ts --> <!-- eslint-disable vue/attribute-hyphenation --> <template> <div v-bento-click-outside-directive="closeDateRangePicker" class="b-date-range-picker"> <!-- Label --> <field-label v-if="label" :id="labelId" :label="label" :tooltip-text="tooltipText" :optional="optional" :required="required" /> <!-- Input --> <div @keydown.enter="onEnterPressedOverInput"> <dropdown-input-default ref="inputContainerRef" always-combobox-is-input :ariaControls="datePickerContainerId" :ariaExpanded="isDateRangePickerOpen" :aria-labelledby="dropdownInputAriaLabelledBy" :aria-describedby="ariaDescribedBy" :aria-required="required" :disabled="disabled" :is-invalid="!!errorMessage || shouldDisplayError" :open="isDateRangePickerOpen" :display-value="displayedDateValue" :readonly="isReadOnly" @open="openDateRangePicker" @clear="clearDateRangePickerValue" @close="closeDateRangePicker" /> </div> <!-- Error Messages --> <error-message v-if="shouldDisplayError" :id="dateErrorId" :error-message="t('invalidDateFormat')" class="b-date-range-picker__error-message" /> <error-message v-if="!!errorMessage" :id="errorId" :error-message="errorMessage" class="b-date-range-picker__error-message" /> <!-- Description --> <bento-typography v-if="description" :id="descriptionId" class="b-date-range-picker__description" :class="descriptionConditionalClasses" el="span" > {{ description }} </bento-typography> <!-- Calendar container --> <bento-popover v-if="inputContainerRef" :id="datePickerContainerId" class="b-date-range-picker__container" :open="isDateRangePickerOpen" :target-element="inputContainerRef" :aria-label="popoverAriaLabel" position="bottom-start" without-space fit-content trap-all overflow-visible @keydown.esc.native="closeDateRangePicker" > <date-range-picker-calendar :allow-time-input="withTimeInput" :quick-select-ranges="adjustedQuickSelectRanges" :value="internalModelValue" :first-day-of-week="resolvedFirstDayOfWeek" :granularities="granularities" :is-date-disabled="isDateDisabled" :max-range="maxRange" :min="min" :max="max" :number-of-months="computedNumberOfMonths" :show-end-date-on-open="showEndDateOnOpen" :date-form-data="displayedTextInputValue" :variant="variant" :year-range="yearRange" @custom-range="onRangeSelectorInput" @input="onDateRangePickerInput" @error="onDateRangePickerError" @form-date="onDateRangePickerFormDateInputUpdate" > <template v-if="hasActions" #actions> <bento-divider /> <div class="b-date-range-picker__footer"> <bento-button-actions :actions="rangePickerButtonActions" :layout="BentoButtonActionsLayout.SPACE_BETWEEN" /> </div> </template> </date-range-picker-calendar> </bento-popover> </div> </template> <script setup lang="ts"> import { computed, type HTMLAttributes, onMounted, ref, toRaw, toRef, useAttrs, watch } from 'vue'; import { useBreakpoints } from '@vueuse/core'; // Components import { BentoButtonActions, BentoButtonActionsLayout } from '../button'; import { BentoDivider } from '@/components/divider'; import { BentoPopover } from '@/components/popover'; import { BentoTypography } from '../typography'; import { DateRangePickerCalendar } from './components/date-range-picker-calendar'; import { DropdownInputDefault } from '@/components/dropdown/components'; import { ErrorMessage, FieldLabel } from '@/internal'; // Utils import { generateUid } from '@/core/utils/ts'; import { useI18n } from '@/utils/ts/i18n'; import { dateToTimeInputString } from '@/utils/ts/format-date'; import { useDateInputFormatter } from '@/composables/use-date-input-formatter/use-date-input-formatter'; // Composables import { useDateRangePickerCalendarText } from './composables/use-date-range-picker-calendar-text'; import { useFirstDayOfWeek, useFormLayoutFieldLoading } from '@/composables'; // Directives import { BentoClickOutsideDirective as vBentoClickOutsideDirective } from '@/directives'; // Types import { type BentoDateRangePickerEmits, type BentoDateRangePickerProps, type BentoDateRangePickerValue, } from './date-range-picker.types'; import { type DateRangePickerCalendarFormData, type DateRangePickerCalendarRangeSelectorItem, } from './components/date-range-picker-calendar/date-range-picker-calendar.types'; import { deprecate } from '@/utils/ts/deprecate'; import messages from './messages.json'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; type MessageSchema = (typeof messages)['en-US']; const emit = defineEmits<BentoDateRangePickerEmits>(); const props = withDefaults(defineProps<BentoDateRangePickerProps>(), { description: undefined, disabled: false, errorMessage: null, firstDayOfWeek: undefined, hasActions: false, isDateDisabled: undefined, label: undefined, max: null, maxRange: null, min: null, modelValue: null, numberOfMonths: undefined, optional: false, placeholder: null, quickSelectRanges: undefined, readonly: false, required: false, showEndDateOnOpen: false, tooltipText: null, value: null, variant: undefined, // Default will be set by @/internal/calendar yearRange: undefined, }); const attrs: HTMLAttributes = useAttrs(); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const { dateToInputDateString } = useDateInputFormatter(); const datePickerContainerId = generateUid('bento-date-range-picker-container'); const labelId = generateUid('bento-date-range-picker-label'); const descriptionId = generateUid('bento-date-range-picker-description'); const errorId = generateUid('bento-date-range-picker-error'); const dateErrorId = generateUid('bento-date-range-picker-date-error'); const popoverAriaLabel = computed(() => props.label || t('dateRangePicker', { name: datePickerContainerId })); const { resolvedFirstDayOfWeek } = useFirstDayOfWeek(toRef(props, 'firstDayOfWeek')); const breakpoints = useBreakpoints({ none: 0, one: 710, // width of date range picker with one panes two: 1030, // width of date range picker with two panes three: 1290, // width of date range picker with three panes }); const computedNumberOfMonths = computed(() => { const activeBreakpoint = breakpoints.active().value; const breakpointToCalendarPanesMap = { none: 0, one: 1, two: 2, }; if (activeBreakpoint in breakpointToCalendarPanesMap) { const maxPanes = breakpointToCalendarPanesMap[activeBreakpoint as keyof typeof breakpointToCalendarPanesMap]; // If numberOfMonths is not provided, use the max panes for the breakpoint. // Otherwise, use the smaller of the two values. return props.numberOfMonths === undefined ? maxPanes : Math.min(props.numberOfMonths, maxPanes); } // Default case for 'three' and larger breakpoints, respect user-provided numberOfMonths. return props.numberOfMonths; }); const adjustedQuickSelectRanges = computed<Array<DateRangePickerCalendarRangeSelectorItem>>(() => { return ( props?.quickSelectRanges && props.quickSelectRanges.map((range: DateRangePickerCalendarRangeSelectorItem) => { const timeDiff = !range.data.endDate ? new Date(Date.now()).getTime() - range.data.startDate.getTime() : undefined; return { ...range, data: { ...range.data, // Adding timeDifference to calculate an up-to-date endDate on range selection timeDifference: timeDiff, }, }; }) ); }); const dropdownInputAriaLabelledBy = computed(() => (props.label ? labelId : (attrs?.['aria-labelledby'] ?? null))); const isDateIncorrect = ref(false); const inputContainerRef = ref(null); const isDateRangePickerOpen = ref(false); const { isReadOnly } = useFormLayoutFieldLoading(toRef(props, 'readonly')); const { emitValue } = useFormFieldEmits(emit); watch( () => props.disabled, () => { // Close date picker popover if it's opened when it's disabled if (props.disabled && isDateRangePickerOpen.value) { isDateRangePickerOpen.value = false; } } ); const internalModelValue = computed(() => props.modelValue || props.value); const shouldDisplayError = computed(() => { if (props.hasActions) { return !isApplyDisabled.value && isDateIncorrect.value; } return isDateIncorrect.value; }); const descriptionConditionalClasses = computed(() => ({ 'b-date-range-picker__description--error': !!props.errorMessage || shouldDisplayError.value, })); const isMonthVariant = computed(() => props.variant === 'month'); const withTimeInput = computed(() => props.allowTimeInput && props.variant !== 'month'); const { dateDisplayValue, textInputDisplayValue, formatTextInputDisplayValue } = useDateRangePickerCalendarText({ datePickerValue: internalModelValue, isDateIncorrect, quickSelectRanges: adjustedQuickSelectRanges, isMonthVariant, withTimeInput, placeholder: toRef(props, 'placeholder'), }); const ariaDescribedBy = computed( () => `${props.description ? descriptionId : ''} ${props.errorMessage ? errorId : ''} ${ isDateIncorrect.value ? dateErrorId : '' }`.trim() || null ); const closeDateRangePicker = () => { isDateRangePickerOpen.value = false; }; const openDateRangePicker = () => { isDateRangePickerOpen.value = true; formatTextInputDisplayValue(); }; /* * Used when `hasActions` is set to true */ const localDateDisplayValue = ref(dateDisplayValue.value); const localTextInputDisplayValue = ref<DateRangePickerCalendarFormData>( // textInputDisplayValue is a reactive object structuredClone(toRaw(textInputDisplayValue)) ); const selectedDate = ref<BentoDateRangePickerValue>(props.modelValue ?? props.value); const isApplyDisabled = ref(true); const displayedTextInputValue = computed(() => props.hasActions ? localTextInputDisplayValue.value : textInputDisplayValue ); const displayedDateValue = computed(() => props.hasActions ? localDateDisplayValue.value : dateDisplayValue.value ); onMounted(() => { if (!props.hasActions) { deprecate( 'BentoDateRangePicker "hasActions" property', `Set the BentoDateRangePicker "hasActions" property true which will display the "Apply" and "Cancel" buttons. This will be the default behavior in the v2 and "hasActions" will be removed.`, '2.0.0' ); } if (props.value) { deprecate( 'BentoDateRangePicker "value" property', 'The use of "value" prop in "BentoDateRangePicker" is no longer supported. Use the "v-model" or "model-value" property instead.', '2.0.0' ); } if ( props.granularities && props.variant !== 'month' && props.granularities.find(({ type }) => type === 'quarterly') ) { throw new Error('Quarterly granularity is only supported by BentoDateRangePicker with variant: "month"'); } }); // On update for the value we need to update all the locally stored data watch([() => props.value, () => props.modelValue], () => { localDateDisplayValue.value = dateDisplayValue.value; localTextInputDisplayValue.value = structuredClone(toRaw(textInputDisplayValue)); }); const rangePickerButtonActions = computed(() => [ { title: t('apply'), event: () => { emitValue(selectedDate.value); closeDateRangePicker(); isApplyDisabled.value = true; }, disabled: isApplyDisabled.value, }, { title: t('cancel'), event: closeDateRangePicker, }, ]); watch(selectedDate, date => { // skip emit until Apply button is clicked if (props.hasActions) { isApplyDisabled.value = isDateIncorrect.value; return; } emitValue(date); }); const onDateRangePickerInput = (selectedRange: BentoDateRangePickerValue) => { // Reset the errors when the date is correct isDateIncorrect.value = false; textInputDisplayValue.startDate = dateToInputDateString(selectedRange.startDate); textInputDisplayValue.endDate = dateToInputDateString(selectedRange.endDate); textInputDisplayValue.startTime = dateToTimeInputString(selectedRange.startDate); textInputDisplayValue.endTime = dateToTimeInputString(selectedRange.endDate); selectedDate.value = selectedRange; }; const clearDateRangePickerValue = async () => { if (isDateIncorrect.value) { isDateIncorrect.value = false; } selectedDate.value = null; }; const onEnterPressedOverInput = () => { // open the datepicker if (!isDateRangePickerOpen.value) { isDateRangePickerOpen.value = true; return; } // Close the date picker if the date is correct and it is open if (!isDateIncorrect.value) { isDateRangePickerOpen.value = false; } }; const onDateRangePickerFormDateInputUpdate = (dateFormDate: DateRangePickerCalendarFormData) => { isDateIncorrect.value = false; if (dateFormDate.startTime) { textInputDisplayValue.startTime = dateFormDate.startTime; } if (dateFormDate.endTime) { textInputDisplayValue.endTime = dateFormDate.endTime; } }; const onDateRangePickerError = dateTextInput => { isDateIncorrect.value = true; textInputDisplayValue.startDate = dateTextInput.startDate; textInputDisplayValue.endDate = dateTextInput.endDate; // Remove current date range selection selectedDate.value = { startDate: null, endDate: null }; }; const onRangeSelectorInput = (quickSelectRanges?: BentoDateRangePickerValue) => { if (quickSelectRanges) { textInputDisplayValue.startDate = dateToInputDateString(quickSelectRanges.startDate); textInputDisplayValue.endDate = dateToInputDateString(quickSelectRanges.endDate); textInputDisplayValue.startTime = dateToTimeInputString(quickSelectRanges.startDate); textInputDisplayValue.endTime = dateToTimeInputString(quickSelectRanges.endDate); selectedDate.value = quickSelectRanges; } }; </script> <script lang="ts"> /** * Date range picker selector. * * @example * import { BentoDateRangePicker } from '@adyen/bento-vue2'; * * export default { * components: { BentoDateRangePicker }, * template: ` * <bento-date-range-picker * label="Label" * description="Supporting text" * required * optional * disabled * @input="onDateChanged" * v-model="selectedDate" * /> * `, * setup() { * const selectedDate = ref(new Date()); // reactive({ startDate: new Date(), endDate: new Date() }) * return { * selectedDate, * } * } * } */ export default { i18n: { messages }, name: 'bento-date-range-picker', model: { prop: 'modelValue' }, }; </script> <style lang="scss" scoped src="./date-range-picker.scss" />
|
|
1
|
+
<!-- TODO: Remove aria props as these are part of JSX elements /vue/types/jsx.d.ts --> <!-- eslint-disable vue/attribute-hyphenation --> <template> <div v-bento-click-outside-directive="closeDateRangePicker" class="b-date-range-picker"> <!-- Label --> <field-label v-if="label" :id="labelId" :label="label" :tooltip-text="tooltipText" :optional="optional" :required="required" /> <!-- Input --> <div @keydown.enter="onEnterPressedOverInput"> <dropdown-input-default ref="inputContainerRef" always-combobox-is-input :ariaControls="datePickerContainerId" :ariaExpanded="isDateRangePickerOpen" :aria-labelledby="dropdownInputAriaLabelledBy" :aria-describedby="ariaDescribedBy" :aria-required="required" :disabled="disabled" :is-invalid="!!errorMessage || shouldDisplayError" :open="isDateRangePickerOpen" :display-value="displayedDateValue" :readonly="isReadOnly" @open="openDateRangePicker" @clear="clearDateRangePickerValue" @close="closeDateRangePicker" /> </div> <!-- Error Messages --> <error-message v-if="shouldDisplayError" :id="dateErrorId" :error-message="t('invalidDateFormat')" class="b-date-range-picker__error-message" /> <error-message v-if="!!errorMessage" :id="errorId" :error-message="errorMessage" class="b-date-range-picker__error-message" /> <!-- Description --> <bento-typography v-if="description" :id="descriptionId" class="b-date-range-picker__description" :class="descriptionConditionalClasses" el="span" > {{ description }} </bento-typography> <!-- Calendar container --> <bento-popover v-if="inputContainerRef" :id="datePickerContainerId" class="b-date-range-picker__container" :open="isDateRangePickerOpen" :target-element="inputContainerRef" :aria-label="popoverAriaLabel" position="bottom-start" without-space fit-content trap-all overflow-visible :trap-all-options="trapAllOptions" @keydown.esc.native="closeDateRangePicker" > <date-range-picker-calendar :allow-time-input="withTimeInput" :quick-select-ranges="adjustedQuickSelectRanges" :value="dateRangePickerCalendarValue" :first-day-of-week="resolvedFirstDayOfWeek" :granularities="granularities" :is-date-disabled="isDateDisabled" :max-range="maxRange" :min="min" :max="max" :number-of-months="computedNumberOfMonths" :show-end-date-on-open="showEndDateOnOpen" :date-form-data="displayedTextInputValue" :variant="variant" :year-range="yearRange" @custom-range="onRangeSelectorInput" @input="onDateRangePickerInput" @error="onDateRangePickerError" @form-date="onDateRangePickerFormDateInputUpdate" > <template v-if="hasActions" #actions> <bento-divider /> <div class="b-date-range-picker__footer"> <bento-button-actions :actions="rangePickerButtonActions" :layout="BentoButtonActionsLayout.SPACE_BETWEEN" /> </div> </template> </date-range-picker-calendar> </bento-popover> </div> </template> <script setup lang="ts"> import { computed, type HTMLAttributes, onMounted, ref, toRaw, toRef, useAttrs, watch } from 'vue'; import { useBreakpoints } from '@vueuse/core'; // Components import { BentoButtonActions, BentoButtonActionsLayout } from '../button'; import { BentoDivider } from '@/components/divider'; import { BentoPopover } from '@/components/popover'; import { BentoTypography } from '../typography'; import { DateRangePickerCalendar } from './components/date-range-picker-calendar'; import { DropdownInputDefault } from '@/components/dropdown/components'; import { ErrorMessage, FieldLabel } from '@/internal'; // Utils import { generateUid } from '@/core/utils/ts'; import { useI18n } from '@/utils/ts/i18n'; import { dateToTimeInputString } from '@/utils/ts/format-date'; import { useDateInputFormatter } from '@/composables/use-date-input-formatter/use-date-input-formatter'; // Composables import { useDateRangePickerCalendarText } from './composables/use-date-range-picker-calendar-text'; import { useFirstDayOfWeek, useFormLayoutFieldLoading } from '@/composables'; // Directives import { BentoClickOutsideDirective as vBentoClickOutsideDirective } from '@/directives'; // Types import { type BentoDateRangePickerEmits, type BentoDateRangePickerProps, type BentoDateRangePickerValue, } from './date-range-picker.types'; import { type DateRangePickerCalendarFormData, type DateRangePickerCalendarRangeSelectorItem, } from './components/date-range-picker-calendar/date-range-picker-calendar.types'; import { deprecate } from '@/utils/ts/deprecate'; import messages from './messages.json'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; type MessageSchema = (typeof messages)['en-US']; const emit = defineEmits<BentoDateRangePickerEmits>(); const props = withDefaults(defineProps<BentoDateRangePickerProps>(), { description: undefined, disabled: false, errorMessage: null, firstDayOfWeek: undefined, hasActions: false, isDateDisabled: undefined, label: undefined, max: null, maxRange: null, min: null, modelValue: null, numberOfMonths: undefined, optional: false, placeholder: null, quickSelectRanges: undefined, readonly: false, required: false, showEndDateOnOpen: false, tooltipText: null, value: null, variant: undefined, // Default will be set by @/internal/calendar yearRange: undefined, }); const attrs: HTMLAttributes = useAttrs(); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const { dateToInputDateString } = useDateInputFormatter(); const datePickerContainerId = generateUid('bento-date-range-picker-container'); const labelId = generateUid('bento-date-range-picker-label'); const descriptionId = generateUid('bento-date-range-picker-description'); const errorId = generateUid('bento-date-range-picker-error'); const dateErrorId = generateUid('bento-date-range-picker-date-error'); const popoverAriaLabel = computed(() => props.label || t('dateRangePicker', { name: datePickerContainerId })); const { resolvedFirstDayOfWeek } = useFirstDayOfWeek(toRef(props, 'firstDayOfWeek')); const breakpoints = useBreakpoints({ none: 0, one: 710, // width of date range picker with one panes two: 1030, // width of date range picker with two panes three: 1290, // width of date range picker with three panes }); const computedNumberOfMonths = computed(() => { const activeBreakpoint = breakpoints.active().value; const breakpointToCalendarPanesMap = { none: 0, one: 1, two: 2, }; if (activeBreakpoint in breakpointToCalendarPanesMap) { const maxPanes = breakpointToCalendarPanesMap[activeBreakpoint as keyof typeof breakpointToCalendarPanesMap]; // If numberOfMonths is not provided, use the max panes for the breakpoint. // Otherwise, use the smaller of the two values. return props.numberOfMonths === undefined ? maxPanes : Math.min(props.numberOfMonths, maxPanes); } // Default case for 'three' and larger breakpoints, respect user-provided numberOfMonths. return props.numberOfMonths; }); const adjustedQuickSelectRanges = computed<Array<DateRangePickerCalendarRangeSelectorItem>>(() => { return ( props?.quickSelectRanges && props.quickSelectRanges.map((range: DateRangePickerCalendarRangeSelectorItem) => { const timeDiff = !range.data.endDate ? new Date(Date.now()).getTime() - range.data.startDate.getTime() : undefined; return { ...range, data: { ...range.data, // Adding timeDifference to calculate an up-to-date endDate on range selection timeDifference: timeDiff, }, }; }) ); }); const dropdownInputAriaLabelledBy = computed(() => (props.label ? labelId : (attrs?.['aria-labelledby'] ?? null))); const isDateIncorrect = ref(false); const inputContainerRef = ref(null); const isDateRangePickerOpen = ref(false); const { isReadOnly } = useFormLayoutFieldLoading(toRef(props, 'readonly')); const { emitValue } = useFormFieldEmits(emit); watch( () => props.disabled, () => { // Close date picker popover if it's opened when it's disabled if (props.disabled && isDateRangePickerOpen.value) { isDateRangePickerOpen.value = false; } } ); const internalModelValue = computed(() => props.modelValue || props.value); const shouldDisplayError = computed(() => { if (props.hasActions) { return !isApplyDisabled.value && isDateIncorrect.value; } return isDateIncorrect.value; }); const descriptionConditionalClasses = computed(() => ({ 'b-date-range-picker__description--error': !!props.errorMessage || shouldDisplayError.value, })); const isMonthVariant = computed(() => props.variant === 'month'); const withTimeInput = computed(() => props.allowTimeInput && props.variant !== 'month'); const { dateDisplayValue, textInputDisplayValue, formatTextInputDisplayValue } = useDateRangePickerCalendarText({ datePickerValue: internalModelValue, isDateIncorrect, quickSelectRanges: adjustedQuickSelectRanges, isMonthVariant, withTimeInput, placeholder: toRef(props, 'placeholder'), }); const ariaDescribedBy = computed( () => `${props.description ? descriptionId : ''} ${props.errorMessage ? errorId : ''} ${ isDateIncorrect.value ? dateErrorId : '' }`.trim() || null ); const closeDateRangePicker = () => { isDateRangePickerOpen.value = false; }; const openDateRangePicker = () => { isDateRangePickerOpen.value = true; formatTextInputDisplayValue(); }; /* * Used when `hasActions` is set to true */ const localDateDisplayValue = ref(dateDisplayValue.value); const localTextInputDisplayValue = ref<DateRangePickerCalendarFormData>( // textInputDisplayValue is a reactive object structuredClone(toRaw(textInputDisplayValue)) ); const selectedDate = ref<BentoDateRangePickerValue>(props.modelValue ?? props.value); const isApplyDisabled = ref(true); const dateRangePickerCalendarValue = computed(() => props.hasActions ? selectedDate.value : internalModelValue.value ); const displayedTextInputValue = ref<DateRangePickerCalendarFormData>( props.hasActions ? localTextInputDisplayValue.value : textInputDisplayValue ); const displayedDateValue = computed(() => props.hasActions ? localDateDisplayValue.value : dateDisplayValue.value ); const trapAllOptions = { allowOutsideClick: true, }; onMounted(() => { if (!props.hasActions) { deprecate( 'BentoDateRangePicker "hasActions" property', `Set the BentoDateRangePicker "hasActions" property true which will display the "Apply" and "Cancel" buttons. This will be the default behavior in the v2 and "hasActions" will be removed.`, '2.0.0' ); } if (props.value) { deprecate( 'BentoDateRangePicker "value" property', 'The use of "value" prop in "BentoDateRangePicker" is no longer supported. Use the "v-model" or "model-value" property instead.', '2.0.0' ); } if ( props.granularities && props.variant !== 'month' && props.granularities.find(({ type }) => type === 'quarterly') ) { throw new Error('Quarterly granularity is only supported by BentoDateRangePicker with variant: "month"'); } }); // Sync the local draft display state back to the committed model value. // Since `displayedTextInputValue` is a mutable ref, it must point to the new cloned draft object. const syncLocalDisplayStateFromCommittedValue = () => { localDateDisplayValue.value = dateDisplayValue.value; localTextInputDisplayValue.value = structuredClone(toRaw(textInputDisplayValue)); displayedTextInputValue.value = props.hasActions ? localTextInputDisplayValue.value : textInputDisplayValue; }; // On update for the value we need to update all the locally stored data watch([() => props.value, () => props.modelValue], () => { syncLocalDisplayStateFromCommittedValue(); }); const cancelDateRangePicker = () => { // Reset local date range picker state syncLocalDisplayStateFromCommittedValue(); selectedDate.value = internalModelValue.value; isDateIncorrect.value = false; isApplyDisabled.value = true; closeDateRangePicker(); }; const rangePickerButtonActions = computed(() => [ { title: t('apply'), event: () => { emitValue(selectedDate.value); closeDateRangePicker(); isApplyDisabled.value = true; }, disabled: isApplyDisabled.value, }, { title: t('cancel'), event: cancelDateRangePicker, }, ]); watch(selectedDate, date => { // skip emit until Apply button is clicked if (props.hasActions) { isApplyDisabled.value = isDateIncorrect.value || date === internalModelValue.value; return; } emitValue(date); }); const onDateRangePickerInput = (selectedRange: BentoDateRangePickerValue) => { // Reset the errors when the date is correct isDateIncorrect.value = false; displayedTextInputValue.value.startDate = dateToInputDateString(selectedRange.startDate); displayedTextInputValue.value.endDate = dateToInputDateString(selectedRange.endDate); displayedTextInputValue.value.startTime = dateToTimeInputString(selectedRange.startDate); displayedTextInputValue.value.endTime = dateToTimeInputString(selectedRange.endDate); selectedDate.value = selectedRange; }; const clearDateRangePickerValue = async () => { if (isDateIncorrect.value) { isDateIncorrect.value = false; } selectedDate.value = null; }; const onEnterPressedOverInput = () => { // open the datepicker if (!isDateRangePickerOpen.value) { isDateRangePickerOpen.value = true; return; } // Close the date picker if the date is correct and it is open if (!isDateIncorrect.value) { isDateRangePickerOpen.value = false; } }; const onDateRangePickerFormDateInputUpdate = (dateFormDate: DateRangePickerCalendarFormData) => { isDateIncorrect.value = false; if (dateFormDate.startTime) { displayedTextInputValue.value.startTime = dateFormDate.startTime; } if (dateFormDate.endTime) { displayedTextInputValue.value.endTime = dateFormDate.endTime; } }; const onDateRangePickerError = dateTextInput => { isDateIncorrect.value = true; displayedTextInputValue.value.startDate = dateTextInput.startDate; displayedTextInputValue.value.endDate = dateTextInput.endDate; // Remove current date range selection selectedDate.value = { startDate: null, endDate: null }; }; const onRangeSelectorInput = (quickSelectRanges?: BentoDateRangePickerValue) => { if (quickSelectRanges) { displayedTextInputValue.value.startDate = dateToInputDateString(quickSelectRanges.startDate); displayedTextInputValue.value.endDate = dateToInputDateString(quickSelectRanges.endDate); displayedTextInputValue.value.startTime = dateToTimeInputString(quickSelectRanges.startDate); displayedTextInputValue.value.endTime = dateToTimeInputString(quickSelectRanges.endDate); selectedDate.value = quickSelectRanges; } }; </script> <script lang="ts"> /** * Date range picker selector. * * @example * import { BentoDateRangePicker } from '@adyen/bento-vue2'; * * export default { * components: { BentoDateRangePicker }, * template: ` * <bento-date-range-picker * label="Label" * description="Supporting text" * required * optional * disabled * @input="onDateChanged" * v-model="selectedDate" * /> * `, * setup() { * const selectedDate = ref(new Date()); // reactive({ startDate: new Date(), endDate: new Date() }) * return { * selectedDate, * } * } * } */ export default { i18n: { messages }, name: 'bento-date-range-picker', model: { prop: 'modelValue' }, }; </script> <style lang="scss" scoped src="./date-range-picker.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <bento-menu class="b-draggable-handle" :class="computedClasses" :data="menuData" :menu-width="250" :button="buttonComputedProps" > <template #iconLeft> <span class="b-draggable-handle__icon"> <grab-icon aria-hidden="true" /> </span> </template> </bento-menu> </template> <script setup lang="ts"> import { computed } from 'vue'; import { BentoMenu, type BentoMenuItem, type BentoMenuProps } from '@/components/menu'; import { type BentoDraggableHandleProps } from './draggable-handle.types'; import { useI18n } from '@/utils/ts/i18n'; import GrabIcon from '@adyen/ui-assets-icons-16/vue/grab'; import ArrowTopIcon from '@adyen/ui-assets-icons-16/vue/arrow-top'; import ArrowLeftIcon from '@adyen/ui-assets-icons-16/vue/arrow-left'; import ArrowRightIcon from '@adyen/ui-assets-icons-16/vue/arrow-right'; import ArrowBottomIcon from '@adyen/ui-assets-icons-16/vue/arrow-bottom'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const { t } = useI18n<{ message: MessageSchema }>({ messages }); const props = withDefaults(defineProps<BentoDraggableHandleProps>(), { condensed: false, disabled: false, disabledMenus: () => ({}), variant: 'up-and-down', }); const emit = defineEmits<{ /** * Emitted when the "Move to bottom" action is fired. */ (e: 'move-bottom'): void; /** * Emitted when the "Move down" action is fired. */ (e: 'move-down'): void; /** * Emitted when the "Move left" action is fired. (Only in `all-directions` variant) */ (e: 'move-left'): void; /** * Emitted when the "Move right" action is fired. (Only in `all-directions` variant) */ (e: 'move-right'): void; /** * Emitted when the "Move to top" action is fired. */ (e: 'move-top'): void; /** * Emitted when the "Move up" action is fired. */ (e: 'move-up'): void; }>(); const buttonComputedProps = computed( () => ({ 'aria-label': t('reorderLabel', { label: props.label }), condensed: props.condensed, disabled: props.disabled, variant: 'tertiary', }) as BentoMenuProps['button'] ); const computedClasses = computed(() => ({ /** * Disables the handle and prevents the entire * element from being dragged. * Used by `useBentoDraggable` */ 'b-draggable-handle--disabled': props.disabled, })); /** * Renders the menus for the draggable handle. */ const menuData = computed(() => { const MENUS: { [key: string]: BentoMenuItem; } = { up: { text: t('moveUp'), handler: () => emit('move-up'), disabled: props.disabledMenus.up, icon: ArrowTopIcon, }, down: { text: t('moveDown'), handler: () => emit('move-down'), disabled: props.disabledMenus.down, icon: ArrowBottomIcon, }, left: { text: t('moveLeft'), handler: () => emit('move-left'), disabled: props.disabledMenus.left, icon: ArrowLeftIcon, }, right: { text: t('moveRight'), handler: () => emit('move-right'), disabled: props.disabledMenus.right, icon: ArrowRightIcon, }, top: { text: t('moveToTheTop'), handler: () => emit('move-top'), disabled: props.disabledMenus.top, icon: ArrowTopIcon, }, bottom: { text: t('moveToTheBottom'), handler: () => emit('move-bottom'), disabled: props.disabledMenus.bottom, icon: ArrowBottomIcon, }, }; // Render the menus and dividers according to the variant return props.variant === 'up-and-down' ? [ MENUS.up, // Divider added after "Move down" { ...MENUS.down, addDivider: true, }, MENUS.top, MENUS.bottom, ] : [ MENUS.up, MENUS.down, MENUS.left, // Divider added after "Move right" { ...MENUS.right, addDivider: true, }, MENUS.top, MENUS.bottom, ]; }); </script> <script lang="ts"> /** * Bento drag and drop handle component. * * @example * import { BentoDraggableHandle } from '@adyen/bento-vue2'; * * export default { * components: { BentoDraggableHandle }, * template: ` * <bento-draggable-handle * bottom * condensed * down * left * right * top * up * \@move-bottom="moveBottom(index)" * \@move-down="moveDown(index)" * \@move-left="moveLeft(index)" * \@move-right="moveRight(index)" * \@move-top="moveTop(index)" * \@move-up="moveUp(index)" * /> * `, * setup() { * const moveBottom = () => {} * const moveDown = () => {} * const moveLeft = () => {} * const moveRight = () => {} * const moveTop = () => {} * const moveUp = () => {} * } * } */ export default { i18n: { messages }, name: 'bento-draggable-handle', }; </script> <style lang="scss" scoped src="./draggable-handle.scss" />
|
|
1
|
+
<template> <bento-menu ref="menuRef" class="b-draggable-handle" :class="computedClasses" :data="menuData" :menu-width="250" :button="buttonComputedProps" > <template #iconLeft> <span class="b-draggable-handle__icon"> <grab-icon aria-hidden="true" /> </span> </template> </bento-menu> </template> <script setup lang="ts"> import { computed, ref } from 'vue'; import { BentoMenu, type BentoMenuItem, type BentoMenuProps } from '@/components/menu'; import { useEventListener } from '@vueuse/core'; import { type BentoDraggableHandleProps } from './draggable-handle.types'; import { useI18n } from '@/utils/ts/i18n'; import GrabIcon from '@adyen/ui-assets-icons-16/vue/grab'; import ArrowTopIcon from '@adyen/ui-assets-icons-16/vue/arrow-top'; import ArrowLeftIcon from '@adyen/ui-assets-icons-16/vue/arrow-left'; import ArrowRightIcon from '@adyen/ui-assets-icons-16/vue/arrow-right'; import ArrowBottomIcon from '@adyen/ui-assets-icons-16/vue/arrow-bottom'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const { t } = useI18n<{ message: MessageSchema }>({ messages }); const props = withDefaults(defineProps<BentoDraggableHandleProps>(), { condensed: false, disabled: false, disabledMenus: () => ({}), variant: 'up-and-down', }); const emit = defineEmits<{ /** * Emitted when the "Move to bottom" action is fired. */ (e: 'move-bottom'): void; /** * Emitted when the "Move down" action is fired. */ (e: 'move-down'): void; /** * Emitted when the "Move left" action is fired. (Only in `all-directions` variant) */ (e: 'move-left'): void; /** * Emitted when the "Move right" action is fired. (Only in `all-directions` variant) */ (e: 'move-right'): void; /** * Emitted when the "Move to top" action is fired. */ (e: 'move-top'): void; /** * Emitted when the "Move up" action is fired. */ (e: 'move-up'): void; }>(); const menuRef = ref<InstanceType<typeof BentoMenu> | null>(null); const buttonComputedProps = computed( () => ({ 'aria-label': t('reorderLabel', { label: props.label }), condensed: props.condensed, disabled: props.disabled, variant: 'tertiary', }) as BentoMenuProps['button'] ); const computedClasses = computed(() => ({ /** * Disables the handle and prevents the entire * element from being dragged. * Used by `useBentoDraggable` */ 'b-draggable-handle--disabled': props.disabled, })); /** * Renders the menus for the draggable handle. */ const menuData = computed(() => { const MENUS: { [key: string]: BentoMenuItem; } = { up: { text: t('moveUp'), handler: () => emit('move-up'), disabled: props.disabledMenus.up, icon: ArrowTopIcon, }, down: { text: t('moveDown'), handler: () => emit('move-down'), disabled: props.disabledMenus.down, icon: ArrowBottomIcon, }, left: { text: t('moveLeft'), handler: () => emit('move-left'), disabled: props.disabledMenus.left, icon: ArrowLeftIcon, }, right: { text: t('moveRight'), handler: () => emit('move-right'), disabled: props.disabledMenus.right, icon: ArrowRightIcon, }, top: { text: t('moveToTheTop'), handler: () => emit('move-top'), disabled: props.disabledMenus.top, icon: ArrowTopIcon, }, bottom: { text: t('moveToTheBottom'), handler: () => emit('move-bottom'), disabled: props.disabledMenus.bottom, icon: ArrowBottomIcon, }, }; // Render the menus and dividers according to the variant return props.variant === 'up-and-down' ? [ MENUS.up, // Divider added after "Move down" { ...MENUS.down, addDivider: true, }, MENUS.top, MENUS.bottom, ] : [ MENUS.up, MENUS.down, MENUS.left, // Divider added after "Move right" { ...MENUS.right, addDivider: true, }, MENUS.top, MENUS.bottom, ]; }); /** * Closes the menu when a drag is about to start. * * SortableJS clones the dragged element synchronously during `dragstart` to create * the drag ghost. Because Vue batches DOM updates to the next tick, calling * `closeMenu()` alone would leave the popover visible in the cloned ghost. * * To prevent this, the popover DOM element is hidden directly via `style.display` * before SortableJS can capture it. The subsequent `closeMenu(false)` call then * synchronises the Vue reactive state. The `false` argument skips reverting focus * to the menu button, which would interfere with the ongoing drag. */ useEventListener(document, 'dragstart', () => { const ref = menuRef.value?.$refs?.popoverRef; if (ref && !Array.isArray(ref) && '$el' in ref && ref.$el instanceof HTMLElement) { ref.$el.style.display = 'none'; } menuRef.value?.closeMenu(false); }); </script> <script lang="ts"> /** * Bento drag and drop handle component. * * @example * import { BentoDraggableHandle } from '@adyen/bento-vue2'; * * export default { * components: { BentoDraggableHandle }, * template: ` * <bento-draggable-handle * bottom * condensed * down * left * right * top * up * \@move-bottom="moveBottom(index)" * \@move-down="moveDown(index)" * \@move-left="moveLeft(index)" * \@move-right="moveRight(index)" * \@move-top="moveTop(index)" * \@move-up="moveUp(index)" * /> * `, * setup() { * const moveBottom = () => {} * const moveDown = () => {} * const moveLeft = () => {} * const moveRight = () => {} * const moveTop = () => {} * const moveUp = () => {} * } * } */ export default { i18n: { messages }, name: 'bento-draggable-handle', }; </script> <style lang="scss" scoped src="./draggable-handle.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <component :is="popperContainerOrPopoverComponent" v-if="isDropdownOpen" class="b-dropdown-options-container" :class="conditionalClasses" v-bind="popperContainerOrPopoverProps" > <div @keydown.stop @keydown.esc="onEscapeKey"> <bento-listbox :id="id" ref="dropdownOptionsListboxRef" :aria-label="ariaLabel" class="b-dropdown-options-container__listbox" :class="conditionalListboxClasses" :component-loading="componentLoading" :empty-state="emptyState" :is-option-disabled="isOptionDisabled" :items="items" :selected-value="internalSelectedValue" :multiple="multiple" :static-categories="staticCategories" :no-results-message="noResultsMessage" :searching="searching" :loading="loading" :lazy-load-type="lazyLoadType" :has-more-items="hasMoreItems" :virtual-scroll="virtualScroll" v-on="listeners" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </bento-listbox> </div> </component> </template> <script setup lang="ts"> import { computed, defineComponent, type PropType, ref, toRefs, useSlots } from 'vue'; import { BentoListbox } from '@/internal/listbox'; import { type BentoListboxEvent, type BentoListboxIsOptionDisabled, type BentoListboxOptions, } from '@/types/listbox'; import { PopperContainer } from '@/components/popper-container'; import { type BentoDropdownEmptyStateProps, type BentoDropdownEvent, BentoDropdownLazyLoadType, type BentoDropdownVirtulisationOptions, } from '../../dropdown.types'; import { useI18n } from '@/utils/ts/i18n'; import { BentoPopover } from '@/components/popover'; import { useDropdownOptionsListbox } from './composables/use-dropdown-options-listbox'; import { generateUid } from '@/core/utils/ts'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const slots = useSlots(); const emit = defineEmits<{ /** * Emitted when the dropdown is closing. */ (e: BentoDropdownEvent.CLOSE_DROPDOWN); /** * Emitted when the `ENTER` key is pressed */ (e: BentoListboxEvent.ENTER, newSelectedValue: BentoListboxOptions); /** * Emitted when the `ESCAPE` key is pressed */ (e: BentoListboxEvent.ESCAPE); /** * Emitted when an item is selected */ (e: BentoListboxEvent.SELECT, newSelectedValue: BentoListboxOptions); /** * Emitted when: * - `lazyLoad` is `automatic` and the scroll meets the end of the list * - `lazyload` is `button` and the button is clicked */ (e: BentoDropdownEvent.SHOW_MORE); /** * Emitted when the `SPACE` key is pressed */ (e: BentoListboxEvent.SPACE, newSelectedValue: BentoListboxOptions); /** * Emitted when the `TAB` key is pressed */ (e: BentoListboxEvent.TAB, newSelectedValue: BentoListboxOptions); }>(); const props = defineProps({ /** * Defines a string value that labels an interactive element. */ ariaLabel: { type: String, default: null }, /** * Indicates that dropdown items are loading */ componentLoading: { type: Boolean, default: false }, /** * Empty state props used with the inner empty state component which is displayed when no search results are found. */ emptyState: { type: Object as PropType<BentoDropdownEmptyStateProps>, default: undefined, }, /** * Disables dropdown functionality */ disabled: { type: Boolean, default: false }, /** * Indicates whether there are more items to load */ hasMoreItems: { type: Boolean, default: false }, /** * Identifies the listbox whose contents are controlled by the the combobox on which the aria-controls attribute is set. */ id: { type: String, required: true }, /** * Function that allows the options to be disabled * * @type {BentoDropdownIsOptionDisabled} * @param {BentoDropdownOptionItem} option - Dropdown option object * @param {string} option.label - Option's label text * @param {string|number} option.value - Option's value * @param {unknown} option.data - Option's extra data to be passed to the default slot */ isOptionDisabled: { type: Function as PropType<BentoListboxIsOptionDisabled>, default: undefined, }, /** * The option elements to populate the dropdown with. * It must be an array of {@see BentoDropdownOptionItem } * * @property {string} value.label - Text to be displayed in the option * * @property {string,number} value.value - Value of the option */ items: { type: Array as PropType<BentoListboxOptions>, required: true, }, /** * The type of `Lazy Load`. * Type "automatic" will enable infinite scrolling * Type "button" will enable lazy loading with "Show more" button */ lazyLoadType: { type: String as PropType<BentoDropdownLazyLoadType | `${BentoDropdownLazyLoadType}`>, default: BentoDropdownLazyLoadType.AUTOMATIC, validator: (value: BentoDropdownLazyLoadType) => Object.values(BentoDropdownLazyLoadType).includes(value), }, /** * Indicates if new options are lazy loading. */ loading: { type: Boolean, default: false }, /** * If enabled, the user will be able to select multiple items. */ multiple: { type: Boolean, default: false }, /** * Indicates if the dropdown is open so that * the container can be displayed. */ open: { type: Boolean, required: true }, /** * The `input` value. * Providing an empty string or empty array will select no options. * Set to an empty string `''` if you don't want any of the available options to be selected for single select */ selectedValue: { type: Array as PropType<BentoListboxOptions>, default: null, }, /** * Flag that indicates that an external search it's being made * to filter out the items inside the the multi-select listbox * * If enabled, hides the "Select all" checkbox in "multiple" mode. */ searching: BentoListbox.props.searching, /** * If enabled, dropdown will display non-selectable, static categories */ staticCategories: { type: Boolean, default: false }, /** * Reference to the DOM element or Vue component for positioning * the Dropdown container. */ targetElement: { type: PopperContainer.props.targetElement.type, required: true }, /** * Enables virtual scrolling if set to true or by providing an object with itemHeight function. * The itemHeight function is used to calculate the height of rendered item given it's index. */ virtualScroll: { type: [Boolean, Object] as PropType<BentoDropdownVirtulisationOptions>, default: false, }, }); const { selectedValue, multiple } = toRefs(props); const dropdownOptionsListboxRef = ref(null); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const isDropdownOpen = computed(() => props.open && !props.disabled); const noResultsMessage = computed(() => t('noOptionsMatchThisSearch') as string); const conditionalClasses = computed(() => ({ 'b-dropdown-options-container--single': !props?.multiple, })); const conditionalListboxClasses = computed(() => ({ 'b-dropdown-options-container__listbox--single': !props?.multiple, })); const { actions, internalSelectedValue, listeners, onEscapeKey, onOutsideDropdownClick } = useDropdownOptionsListbox(selectedValue, multiple, isDropdownOpen, emit); // Use different components depending on if the dropdown is a single or multi-select const popperContainerOrPopoverComponent = computed(() => (props?.multiple ? BentoPopover : PopperContainer)); const popperContainerOrPopoverProps = computed(() => { const containerMinAndMaxWidth = { 'min-width': `${(props?.targetElement as HTMLElement)?.clientWidth}px`, 'max-width': 'min(500px, 95%)', }; return props?.multiple ? ({ actionsLayout: 'space-between', ariaLabel: props.ariaLabel, actions: actions.value, divider: true, fallbackPosition: ['top-start'], id: generateUid(`dropdown-options-${props.id}`), open: props.open, position: 'bottom-start', style: { width: 'auto', ...containerMinAndMaxWidth, }, targetElement: props.targetElement, } as InstanceType<typeof BentoPopover>['$props']) : ({ fallbackPosition: ['top-start'], offset: [0, 8], position: 'bottom-start', style: { display: isDropdownOpen.value ? 'block' : 'none', ...containerMinAndMaxWidth, }, targetElement: props.targetElement, } as InstanceType<typeof PopperContainer>['$props']); }); defineExpose({ onOutsideDropdownClick, }); </script> <script lang="ts"> /** * Dropdown options container. * It lists all the available options. * * @example * <bento-dropdown-options-container * v-if="inputContainerRef" <!-- Ref to the input to match the width --> * :id="dropdownOptionsContainerId" * :target-element="inputContainerRef" * :aria-label="ariaLabel" * :open="isDropdownOpen" * :disabled="disabled" * :multiple="multiple" * :selected="value" * :isOptionDisabled="option => option.value === 2" * :items="[{ * { label: 'Option 1', value: 1 }, * { label: 'Option 2', value: 2 }, * }]" * @select="onOptionSelected" * /> */ export default defineComponent({ i18n: { messages }, name: 'bento-dropdown-options-container', }); </script> <style lang="scss" scoped src="./dropdown-options-container.scss" />
|
|
1
|
+
<template> <component :is="popperContainerOrPopoverComponent" v-if="isDropdownOpen" class="b-dropdown-options-container" :class="conditionalClasses" v-bind="popperContainerOrPopoverProps" > <div @keydown.stop @keydown.esc="onEscapeKey"> <bento-listbox :id="id" ref="dropdownOptionsListboxRef" :aria-label="ariaLabel" class="b-dropdown-options-container__listbox" :class="conditionalListboxClasses" :component-loading="componentLoading" :empty-state="emptyState" :is-option-disabled="isOptionDisabled" :items="items" :selected-value="internalSelectedValue" :multiple="multiple" :static-categories="staticCategories" :no-results-message="noResultsMessage" :searching="searching" :loading="loading" :lazy-load-type="lazyLoadType" :has-more-items="hasMoreItems" :virtual-scroll="virtualScroll" v-on="listeners" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </bento-listbox> </div> </component> </template> <script setup lang="ts"> import { computed, defineComponent, type PropType, ref, toRefs, useSlots } from 'vue'; import { BentoListbox } from '@/internal/listbox'; import { type BentoListboxEvent, type BentoListboxIsOptionDisabled, type BentoListboxOptions, } from '@/types/listbox'; import { PopperContainer } from '@/components/popper-container'; import { type BentoDropdownEmptyStateProps, type BentoDropdownEvent, BentoDropdownLazyLoadType, type BentoDropdownVirtulisationOptions, } from '../../dropdown.types'; import { useI18n } from '@/utils/ts/i18n'; import { BentoPopover } from '@/components/popover'; import { useDropdownOptionsListbox } from './composables/use-dropdown-options-listbox'; import { generateUid } from '@/core/utils/ts'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const slots = useSlots(); const emit = defineEmits<{ /** * Emitted when the dropdown is closing. */ (e: BentoDropdownEvent.CLOSE_DROPDOWN); /** * Emitted when the `ENTER` key is pressed */ (e: BentoListboxEvent.ENTER, newSelectedValue: BentoListboxOptions); /** * Emitted when the `ESCAPE` key is pressed */ (e: BentoListboxEvent.ESCAPE); /** * Emitted when an item is selected */ (e: BentoListboxEvent.SELECT, newSelectedValue: BentoListboxOptions); /** * Emitted when: * - `lazyLoad` is `automatic` and the scroll meets the end of the list * - `lazyload` is `button` and the button is clicked */ (e: BentoDropdownEvent.SHOW_MORE); /** * Emitted when the `SPACE` key is pressed */ (e: BentoListboxEvent.SPACE, newSelectedValue: BentoListboxOptions); /** * Emitted when the `TAB` key is pressed */ (e: BentoListboxEvent.TAB, newSelectedValue: BentoListboxOptions); }>(); const props = defineProps({ /** * Defines a string value that labels an interactive element. */ ariaLabel: { type: String, default: null }, /** * Indicates that dropdown items are loading */ componentLoading: { type: Boolean, default: false }, /** * Empty state props used with the inner empty state component which is displayed when no search results are found. */ emptyState: { type: Object as PropType<BentoDropdownEmptyStateProps>, default: undefined, }, /** * Disables dropdown functionality */ disabled: { type: Boolean, default: false }, /** * Indicates whether there are more items to load */ hasMoreItems: { type: Boolean, default: false }, /** * Identifies the listbox whose contents are controlled by the the combobox on which the aria-controls attribute is set. */ id: { type: String, required: true }, /** * Function that allows the options to be disabled * * @type {BentoDropdownIsOptionDisabled} * @param {BentoDropdownOptionItem} option - Dropdown option object * @param {string} option.label - Option's label text * @param {string|number} option.value - Option's value * @param {unknown} option.data - Option's extra data to be passed to the default slot */ isOptionDisabled: { type: Function as PropType<BentoListboxIsOptionDisabled>, default: undefined, }, /** * The option elements to populate the dropdown with. * It must be an array of {@see BentoDropdownOptionItem } * * @property {string} value.label - Text to be displayed in the option * * @property {string,number} value.value - Value of the option */ items: { type: Array as PropType<BentoListboxOptions>, required: true, }, /** * The type of `Lazy Load`. * Type "automatic" will enable infinite scrolling * Type "button" will enable lazy loading with "Show more" button */ lazyLoadType: { type: String as PropType<BentoDropdownLazyLoadType | `${BentoDropdownLazyLoadType}`>, default: BentoDropdownLazyLoadType.AUTOMATIC, validator: (value: BentoDropdownLazyLoadType) => Object.values(BentoDropdownLazyLoadType).includes(value), }, /** * Indicates if new options are lazy loading. */ loading: { type: Boolean, default: false }, /** * If enabled, the user will be able to select multiple items. */ multiple: { type: Boolean, default: false }, /** * Indicates if the dropdown is open so that * the container can be displayed. */ open: { type: Boolean, required: true }, /** * The `input` value. * Providing an empty string or empty array will select no options. * Set to an empty string `''` if you don't want any of the available options to be selected for single select */ selectedValue: { type: Array as PropType<BentoListboxOptions>, default: null, }, /** * Flag that indicates that an external search it's being made * to filter out the items inside the the multi-select listbox * * If enabled, hides the "Select all" checkbox in "multiple" mode. */ searching: BentoListbox.props.searching, /** * If enabled, dropdown will display non-selectable, static categories */ staticCategories: { type: Boolean, default: false }, /** * Reference to the DOM element or Vue component for positioning * the Dropdown container. */ targetElement: { type: PopperContainer.props.targetElement.type, required: true }, /** * Enables virtual scrolling if set to true or by providing an object with itemHeight function. * The itemHeight function is used to calculate the height of rendered item given it's index. */ virtualScroll: { type: [Boolean, Object] as PropType<BentoDropdownVirtulisationOptions>, default: false, }, }); const { selectedValue, multiple } = toRefs(props); const dropdownOptionsListboxRef = ref(null); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const isDropdownOpen = computed(() => props.open && !props.disabled); const noResultsMessage = computed(() => t('noOptionsMatchThisSearch') as string); const conditionalClasses = computed(() => ({ 'b-dropdown-options-container--single': !props?.multiple, })); const conditionalListboxClasses = computed(() => ({ 'b-dropdown-options-container__listbox--single': !props?.multiple, })); const { actions, internalSelectedValue, listeners, onEscapeKey, onOutsideDropdownClick } = useDropdownOptionsListbox(selectedValue, multiple, isDropdownOpen, emit); // Use different components depending on if the dropdown is a single or multi-select const popperContainerOrPopoverComponent = computed(() => (props?.multiple ? BentoPopover : PopperContainer)); const popperContainerOrPopoverProps = computed(() => { const containerMinAndMaxWidth = { 'min-width': `${(props?.targetElement as HTMLElement)?.clientWidth}px`, 'max-width': 'min(500px, 95%)', }; return props?.multiple ? ({ actionsLayout: 'space-between', ariaLabel: props.ariaLabel, actions: actions.value, divider: true, fallbackPosition: ['top-start'], id: generateUid(`dropdown-options-${props.id}`), open: props.open, position: 'bottom-start', style: { width: 'auto', ...containerMinAndMaxWidth, }, targetElement: props.targetElement, } as InstanceType<typeof BentoPopover>['$props']) : ({ fallbackPosition: ['top-start'], offset: [0, 8], position: 'bottom-start', style: { display: isDropdownOpen.value ? 'block' : 'none', ...containerMinAndMaxWidth, }, targetElement: props.targetElement, } as InstanceType<typeof PopperContainer>['$props']); }); defineExpose({ onOutsideDropdownClick, dropdownOptionsListboxRef, }); </script> <script lang="ts"> /** * Dropdown options container. * It lists all the available options. * * @example * <bento-dropdown-options-container * v-if="inputContainerRef" <!-- Ref to the input to match the width --> * :id="dropdownOptionsContainerId" * :target-element="inputContainerRef" * :aria-label="ariaLabel" * :open="isDropdownOpen" * :disabled="disabled" * :multiple="multiple" * :selected="value" * :isOptionDisabled="option => option.value === 2" * :items="[{ * { label: 'Option 1', value: 1 }, * { label: 'Option 2', value: 2 }, * }]" * @select="onOptionSelected" * /> */ export default defineComponent({ i18n: { messages }, name: 'bento-dropdown-options-container', }); </script> <style lang="scss" scoped src="./dropdown-options-container.scss" />
|
|
@@ -40,7 +40,9 @@ list of options.
|
|
|
40
40
|
- [External filtering and Lazy loading simultaneously](#external-filtering-and-lazy-loading-simultaneously)
|
|
41
41
|
- [Multi select](#multi-select)
|
|
42
42
|
- [Nested multi-select](#nested-multi-select)
|
|
43
|
+
- [Nested multi-select with lazy loading](#nested-multi-select-with-lazy-loading)
|
|
43
44
|
- [Static categories](#static-categories)
|
|
45
|
+
- [Nested multi-select with lazy loading](#nested-multi-select-with-lazy-loading)
|
|
44
46
|
- [Readonly](#readonly)
|
|
45
47
|
- [Tooltip](#tooltip)
|
|
46
48
|
- [Accessibility](#accessibility)
|
|
@@ -348,6 +350,62 @@ searching for values in `dynamicFiltering` mode.
|
|
|
348
350
|
<Story of={DropdownStories.NestedMultiSelect} />
|
|
349
351
|
</Canvas>
|
|
350
352
|
|
|
353
|
+
### Nested multi-select with lazy loading
|
|
354
|
+
|
|
355
|
+
<Canvas sourceState="hidden">
|
|
356
|
+
<Story of={DropdownStories.NestedMultiSelectWithLazyLoad} />
|
|
357
|
+
</Canvas>
|
|
358
|
+
|
|
359
|
+
The `bento-dropdown` nested multi-select variant supports lazy loading individual child item lists (categories). This is
|
|
360
|
+
highly useful when a category contains a large number of nested options, avoiding the need to load all elements upfront.
|
|
361
|
+
|
|
362
|
+
To enable this feature, configure the `lazyLoad` property on any parent option item that contains a nested list of
|
|
363
|
+
`items`:
|
|
364
|
+
|
|
365
|
+
- **`lazyLoad`**: An object containing the following properties:
|
|
366
|
+
- **`hasMoreItems`**: A boolean indicating whether more child options are available to load. Once all options for
|
|
367
|
+
the group are loaded, set this to `false` to hide the "Show more" button.
|
|
368
|
+
- **`lazyLoadEvent`**: A function triggered when the group's "Show more" button is clicked (or space/enter is
|
|
369
|
+
pressed). It must return a `Promise<void>`. The function receives an object with:
|
|
370
|
+
- `value`: The value of the category option item that triggered the lazy load event.
|
|
371
|
+
- `index`: The index of the category option item.
|
|
372
|
+
|
|
373
|
+
When the `lazyLoadEvent` promise is pending, a loading indicator is displayed. If the promise is rejected, an error
|
|
374
|
+
message is shown with a retry option, allowing the user to click the "Show more" button again.
|
|
375
|
+
|
|
376
|
+
#### Category Selection & Select All Behavior
|
|
377
|
+
|
|
378
|
+
When using nested multi-select with lazy loading, special rules apply to selecting parent categories and how the "select
|
|
379
|
+
all" mechanism operates:
|
|
380
|
+
|
|
381
|
+
- **Required Configuration**: The dropdown **must** have `enableValueLabelPair` set to `true` (or
|
|
382
|
+
`enable-value-label-pair`).
|
|
383
|
+
- **Category Selection**: Selection of a parent category is done using the category option's `"value"` (and `"label"`)
|
|
384
|
+
from the parent `BentoListboxOptionItem`. Selecting a parent category will select all of its child items.
|
|
385
|
+
|
|
386
|
+
<Source
|
|
387
|
+
format={'html'}
|
|
388
|
+
dark
|
|
389
|
+
code={`
|
|
390
|
+
<bento-dropdown
|
|
391
|
+
enable-value-label-pair
|
|
392
|
+
:value="[
|
|
393
|
+
{ label: 'Category 1', value: 'c1' },
|
|
394
|
+
]"
|
|
395
|
+
:items="[
|
|
396
|
+
{
|
|
397
|
+
value: 'c1',
|
|
398
|
+
label: 'Category 1',
|
|
399
|
+
lazyLoad: {
|
|
400
|
+
hasMoreItems: true,
|
|
401
|
+
lazyLoadEvent: ({ value, index }) => void
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
{ value: 100, label: 'Option 100'}
|
|
405
|
+
]"
|
|
406
|
+
/>`}
|
|
407
|
+
/>
|
|
408
|
+
|
|
351
409
|
### Static categories
|
|
352
410
|
|
|
353
411
|
Static categories (not to be confused with the Nested options categories) add an additional level of organization to the
|