@adyen/bento-mcp 0.5.1 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/dist/assets/components/accordion/components/accordion-item.vue +1 -1
- package/dist/assets/components/card/card.vue +1 -1
- package/dist/assets/components/dashed-underline/dashed-underline.vue +1 -1
- package/dist/assets/components/data-grid/components/data-grid-row/data-grid-row.types.ts +1 -1
- package/dist/assets/components/data-grid/components/data-grid-row/data-grid-row.vue +1 -1
- package/dist/assets/components/date-picker/composables/use-date-picker-single-calendar-text.types.ts +1 -1
- package/dist/assets/components/date-picker/date-picker.vue +1 -1
- package/dist/assets/components/date-range-picker/components/date-range-picker-calendar/date-range-picker-calendar.vue +1 -1
- package/dist/assets/components/date-time-picker/date-time-picker.docs.mdx +129 -0
- package/dist/assets/components/date-time-picker/date-time-picker.stories.ts +1 -0
- package/dist/assets/components/date-time-picker/date-time-picker.types.ts +1 -0
- package/dist/assets/components/date-time-picker/date-time-picker.vue +1 -0
- package/dist/assets/components/draggable/draggable.docs.mdx +76 -135
- package/dist/assets/components/draggable/draggable.stories.ts +1 -1
- package/dist/assets/components/draggable/draggable.types.ts +1 -1
- package/dist/assets/components/draggable/draggable.vue +1 -1
- package/dist/assets/components/dropdown/components/dropdown-base-textbox/dropdown-base-textbox.vue +1 -1
- package/dist/assets/components/dropdown/components/dropdown-small-textbox/dropdown-small-textbox.vue +1 -1
- package/dist/assets/components/dropdown/dropdown.vue +1 -1
- package/dist/assets/components/file-uploader/file-uploader.vue +1 -1
- package/dist/assets/components/filter-bar/components/base-filter/base-filter.vue +1 -1
- package/dist/assets/components/input-field/input-field.vue +1 -1
- package/dist/assets/components/internal/calendar/calendar.types.ts +1 -1
- package/dist/assets/components/internal/calendar/calendar.vue +1 -1
- package/dist/assets/components/internal/calendar/components/calendar-month/calendar-month.stories.ts +1 -1
- package/dist/assets/components/internal/calendar/components/calendar-month/calendar-month.types.ts +1 -1
- package/dist/assets/components/internal/calendar/components/calendar-month/calendar-month.vue +1 -1
- package/dist/assets/components/internal/calendar/components/calendar-month/composables/granular-min-max-date.types.ts +1 -1
- package/dist/assets/components/internal/calendar/components/calendar-year/calendar-year.types.ts +1 -1
- package/dist/assets/components/internal/calendar/components/calendar-year/calendar-year.vue +1 -1
- package/dist/assets/components/internal/info-icon-with-popover/info-icon-with-popover.vue +1 -1
- package/dist/assets/components/internal/teleport/teleport.vue +1 -1
- package/dist/assets/components/link/link.vue +1 -1
- package/dist/assets/components/navigation-menu/components/navigation-menu-group/navigation-menu-group.stories.ts +1 -1
- package/dist/assets/components/navigation-menu/components/navigation-menu-group/navigation-menu-group.vue +1 -1
- package/dist/assets/components/navigation-menu/components/navigation-menu-item/navigation-menu-item.stories.ts +1 -1
- package/dist/assets/components/navigation-menu/components/navigation-menu-item/navigation-menu-item.vue +1 -1
- package/dist/assets/components/navigation-menu/navigation-menu.docs.mdx +7 -43
- package/dist/assets/components/navigation-menu/navigation-menu.stories.ts +1 -1
- package/dist/assets/components/navigation-menu/navigation-menu.types.ts +1 -1
- package/dist/assets/components/navigation-menu/navigation-menu.vue +1 -1
- package/dist/assets/components/popover/popover.docs.mdx +74 -15
- package/dist/assets/components/popover/popover.stories.ts +1 -1
- package/dist/assets/components/secondary-nav/components/secondary-nav-category/secondary-nav-category.vue +1 -1
- package/dist/assets/components/secondary-nav/components/secondary-nav-item/secondary-nav-item.vue +1 -1
- package/dist/assets/components/secondary-nav/secondary-nav.stories.ts +1 -1
- package/dist/assets/components/secondary-nav/secondary-nav.types.ts +1 -1
- package/dist/assets/components/secondary-nav/secondary-nav.vue +1 -1
- package/dist/assets/components/selection-card/components/selection-card-group/selection-card-group.types.ts +1 -1
- package/dist/assets/components/tabs/components/tab.types.ts +1 -1
- package/dist/assets/components/tabs/components/tab.vue +1 -1
- package/dist/assets/components/tabs/tabs.stories.ts +1 -1
- package/dist/assets/components/tabs/tabs.types.ts +1 -1
- package/dist/assets/components/tabs/tabs.vue +1 -1
- package/dist/assets/components.json +2 -0
- package/dist/assets/composables/use-bento-delayed-hover/use-bento-delayed-hover.docs.mdx +73 -0
- package/dist/assets/composables/use-bento-delayed-hover/use-bento-delayed-hover.stories.ts +1 -0
- package/dist/assets/index.ts +1 -1
- package/dist/assets/usage.json +12 -10
- package/dist/main.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { Canvas, Meta, Source } from '@storybook/blocks';
|
|
2
|
+
import * as DateTimePickerStories from './date-time-picker.stories';
|
|
3
|
+
|
|
4
|
+
<Meta of={DateTimePickerStories} />
|
|
5
|
+
|
|
6
|
+
# Date time picker
|
|
7
|
+
|
|
8
|
+
The date time picker combines a `bento-date-picker` (single date mode) with a `bento-input-time` to let users select
|
|
9
|
+
both a date and a time in one compound field. Its `v-model` value is a single `Date | null`; when set, the `Date`
|
|
10
|
+
contains both the selected day and the time from the time input.
|
|
11
|
+
|
|
12
|
+
<Canvas of={DateTimePickerStories.Default} />
|
|
13
|
+
|
|
14
|
+
## Use Cases
|
|
15
|
+
|
|
16
|
+
Use a `bento-date-time-picker` if:
|
|
17
|
+
|
|
18
|
+
- You need users to provide both a date and a time, such as scheduling an event or setting a deadline with an exact
|
|
19
|
+
time.
|
|
20
|
+
|
|
21
|
+
### Do not use
|
|
22
|
+
|
|
23
|
+
- When only a date is needed — use `bento-date-picker` instead.
|
|
24
|
+
- When only a time is needed — use `bento-input-time` instead.
|
|
25
|
+
|
|
26
|
+
## Behaviour
|
|
27
|
+
|
|
28
|
+
### Date input
|
|
29
|
+
|
|
30
|
+
The date portion uses `bento-date-picker` in its default (day) mode. Users can type a date in `YYYY-MM-DD` format or
|
|
31
|
+
open the calendar popover to select one. If `dateProps.description` is provided, it is appended before the date format
|
|
32
|
+
hint, e.g. `Supporting text YYYY-MM-DD`.
|
|
33
|
+
|
|
34
|
+
### Time input
|
|
35
|
+
|
|
36
|
+
The time portion uses `bento-input-time`. As the user types, colons are inserted automatically to produce a well-formed
|
|
37
|
+
`HH:MM:SS` string. Partial or invalid time strings do not update `v-model`; the component emits `input:error` instead.
|
|
38
|
+
|
|
39
|
+
### Model value
|
|
40
|
+
|
|
41
|
+
<Source
|
|
42
|
+
dark
|
|
43
|
+
language="html"
|
|
44
|
+
code={`
|
|
45
|
+
<bento-date-time-picker :model-value="scheduledAt" @update:model-value="newValue => scheduledAt = newValue" />
|
|
46
|
+
`}
|
|
47
|
+
/>
|
|
48
|
+
|
|
49
|
+
<Source
|
|
50
|
+
dark
|
|
51
|
+
language="typescript"
|
|
52
|
+
code={`
|
|
53
|
+
const scheduledAt = ref<Date | null>(new Date(2026, 4, 1, 10, 30, 0));
|
|
54
|
+
`}
|
|
55
|
+
/>
|
|
56
|
+
|
|
57
|
+
## Modifiers
|
|
58
|
+
|
|
59
|
+
### Labels
|
|
60
|
+
|
|
61
|
+
Use `dateProps.label` and `timeProps.label` to set the accessible labels for each sub-field independently.
|
|
62
|
+
|
|
63
|
+
<Source
|
|
64
|
+
dark
|
|
65
|
+
language="html"
|
|
66
|
+
code={`
|
|
67
|
+
<bento-date-time-picker :date-props="{ label: 'Start date' }" :time-props="{ label: 'Start time' }" />
|
|
68
|
+
`}
|
|
69
|
+
/>
|
|
70
|
+
|
|
71
|
+
### Error message
|
|
72
|
+
|
|
73
|
+
Use `errorMessage` to display a shared error below both inputs and apply error styling to the compound field.
|
|
74
|
+
|
|
75
|
+
<Source
|
|
76
|
+
dark
|
|
77
|
+
language="html"
|
|
78
|
+
code={`
|
|
79
|
+
<bento-date-time-picker error-message="The selected date and time must be in the future" />
|
|
80
|
+
`}
|
|
81
|
+
/>
|
|
82
|
+
|
|
83
|
+
Per-side errors can still be passed with `dateProps.errorMessage` or `timeProps.errorMessage`.
|
|
84
|
+
|
|
85
|
+
### Disabled
|
|
86
|
+
|
|
87
|
+
Use `disabled` to disable both inputs. `dateProps.disabled` or `timeProps.disabled` override the shared value for that
|
|
88
|
+
side.
|
|
89
|
+
|
|
90
|
+
### Readonly
|
|
91
|
+
|
|
92
|
+
Use `readonly` to make both inputs readonly. `dateProps.readonly` or `timeProps.readonly` override the shared value for
|
|
93
|
+
that side.
|
|
94
|
+
|
|
95
|
+
### Required
|
|
96
|
+
|
|
97
|
+
Use `required` to mark both inputs required. `dateProps.required` or `timeProps.required` override the shared value for
|
|
98
|
+
that side.
|
|
99
|
+
|
|
100
|
+
### Min and max
|
|
101
|
+
|
|
102
|
+
Use `min` and `max` to validate the combined `Date`. The date portion is also forwarded to the date picker for calendar
|
|
103
|
+
bounds. Use `isDateDisabled` to disable dates with custom logic instead of, or in addition to, `min` and `max`.
|
|
104
|
+
|
|
105
|
+
### Pass-through props
|
|
106
|
+
|
|
107
|
+
Use `dateProps` and `timeProps` for props that are specific to only one field, such as `label`, `description`,
|
|
108
|
+
`placeholder`, `optional`, or `tooltipText`. If a shared wrapper prop and a per-side prop are both set, the per-side
|
|
109
|
+
prop wins for that field. `dateProps.description` is appended to the automatic date format hint instead of replacing it.
|
|
110
|
+
|
|
111
|
+
## Accessibility
|
|
112
|
+
|
|
113
|
+
### Keyboard interaction
|
|
114
|
+
|
|
115
|
+
Both sub-components follow the keyboard interaction patterns of their respective components:
|
|
116
|
+
|
|
117
|
+
- The date picker is opened with `Space` or `Enter` and navigated with arrow keys.
|
|
118
|
+
- The time input filters non-numeric keystrokes automatically.
|
|
119
|
+
|
|
120
|
+
### Roles, states and properties
|
|
121
|
+
|
|
122
|
+
| ARIA attribute | is automatic? | notes |
|
|
123
|
+
| ------------------ | ---------------------------------------------- | ---------------------------------------------------------------------- |
|
|
124
|
+
| `role="group"` | Yes | Applied to the root wrapper |
|
|
125
|
+
| `label` | Yes, via `dateProps.label` / `timeProps.label` | Each input has its own accessible label |
|
|
126
|
+
| `required` | Yes, via `required` or per-side props | Forwarded to both inputs |
|
|
127
|
+
| `disabled` | Yes, via `disabled` or per-side props | Forwarded to both inputs |
|
|
128
|
+
| `aria-invalid` | Yes | Applied to the group and invalid inputs |
|
|
129
|
+
| `aria-describedby` | Yes | Preserves external ids and adds the shared group error id when present |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import { action } from '@storybook/addon-actions'; import { isVue2 } from 'vue-demi'; import { ref } from 'vue'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import BentoDateTimePicker from './date-time-picker.vue'; import type { Meta, StoryObj } from '@storybook/vue'; import type { BentoDateTimePickerValue } from './date-time-picker.types'; import defaultCode from './__tests__/date-time-picker-default-example.vue?raw'; const meta: Meta = { title: 'Date time picker', component: BentoDateTimePicker, }; export default meta; type Story = StoryObj<typeof BentoDateTimePicker>; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoDateTimePicker }, props: Object.keys(argTypes), template: ` <div style="max-width: 600px"> <bento-date-time-picker v-bind="args" :model-value="modelValue" @update:model-value="onValueChanged" @input:error="onInputError" @input:valid="onInputValid" /> </div> `, setup(props) { const modelValue = ref<BentoDateTimePickerValue>(props.modelValue ?? null); const onValueChanged = (newValue: BentoDateTimePickerValue) => { modelValue.value = newValue; action('update:model-value')(newValue); }; return { args: isVue2 ? props : _args, modelValue, onInputError: action('input:error'), onInputValid: action('input:valid'), onValueChanged, }; }, }), args: { dateProps: { label: 'Date', description: 'Supporting text', required: true, }, timeProps: { label: 'Time', }, modelValue: new Date(2026, 4, 1, 10, 30, 0), }, parameters: storybookDocsParameter(defaultCode), }; export const WithSharedError: Story = { ...Default, args: { ...Default.args, errorMessage: 'The selected date and time must be in the future', }, }; export const Disabled: Story = { ...Default, args: { ...Default.args, disabled: true, }, };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import type { BentoDatePickerIsDateDisabled, BentoDatePickerProps } from '@/components/date-picker'; import type { BentoInputTimeProps } from '@/components/input-time'; export type BentoDateTimePickerValue = Date | null; export interface BentoDateTimePickerEmits { /** * Emitted when the component's model changes */ (e: 'update:model-value', value: BentoDateTimePickerValue): void; /** * Emitted when the combined date and time value passes validation. */ (e: 'input:valid'): void; /** * Emitted when the combined date and time value fails validation. */ (e: 'input:error'): void; } export interface BentoDateTimePickerProps { /** * The combined date and time value. * @default null */ modelValue?: BentoDateTimePickerValue; /** * If true, disables both fields unless overridden by `dateProps` or `timeProps`. * @default false */ disabled?: boolean; /** * If set, displays a shared error message and applies error styling to both fields. * @default null */ errorMessage?: string | null; /** * Indicate if a date should be disabled or not. */ isDateDisabled?: BentoDatePickerIsDateDisabled; /** * Sets a maximum date-time value. * @default null */ max?: Date | null; /** * Sets a minimum date-time value. * @default null */ min?: Date | null; /** * If true, makes both fields readonly unless overridden by `dateProps` or `timeProps`. * @default false */ readonly?: boolean; /** * If true, marks both fields required unless overridden by `dateProps` or `timeProps`. * @default false */ required?: boolean; /** * Props passed directly to the inner `bento-date-picker`. * Accepts all `BentoDatePickerProps` except `modelValue` and `value`. * @default {} */ dateProps?: Omit<BentoDatePickerProps, 'modelValue' | 'value'>; /** * Props passed directly to the inner `bento-input-time`. * Accepts all `BentoInputTimeProps` except `modelValue`. * @default {} */ timeProps?: Omit<BentoInputTimeProps, 'modelValue'>; }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<template> <div class="b-date-time-picker"> <bento-form-layout-group class="b-date-time-picker__inputs" layout="50-50" :error-message="props.errorMessage"> <bento-date-picker class="b-date-time-picker__date-input" v-bind="resolvedDateProps" :model-value="dateValue" @update:model-value="onDateChanged" /> <bento-input-time class="b-date-time-picker__time-input" v-bind="resolvedTimeProps" :model-value="timeValue" @update:model-value="onTimeChanged" @input:valid="onTimeValid" @input:error="onTimeError" /> </bento-form-layout-group> </div> </template> <script setup lang="ts"> import { computed, ref, watch } from 'vue'; import { setMilliseconds } from 'date-fns/setMilliseconds'; import { startOfDay } from 'date-fns/startOfDay'; // Components import { BentoDatePicker } from '@/components/date-picker'; import { BentoFormLayoutGroup } from '@/components/form-layout'; import { BentoInputTime } from '@/components/input-time'; // Utils import { useDateInputFormatter, useTouchDevice } from '@/composables'; import { useI18n } from '@/utils/ts/i18n'; import { dateToTimeInputString, setTimeInDateObject } from '@/utils/ts/format-date'; import { isValidTimeString } from '@/utils/ts/time-input'; // Types import type { BentoDateTimePickerEmits, BentoDateTimePickerProps, BentoDateTimePickerValue, } from './date-time-picker.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const props = withDefaults(defineProps<BentoDateTimePickerProps>(), { dateProps: () => ({}), disabled: false, errorMessage: null, isDateDisabled: undefined, max: null, min: null, modelValue: null, readonly: false, required: false, timeProps: () => ({}), }); const emit = defineEmits<BentoDateTimePickerEmits>(); const currentTimeText = ref(''); const { dateFormat } = useDateInputFormatter(); const { isTouchDevice } = useTouchDevice(); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const dateValue = computed(() => props.modelValue ?? null); const timeValue = computed(() => currentTimeText.value); const resolvedIsDateDisabled = computed(() => props.dateProps?.isDateDisabled ?? props.isDateDisabled); const dateDescription = computed(() => { if (isTouchDevice.value) { return props.dateProps?.description; } else { return props.dateProps?.description ? `${props.dateProps.description} ${t(dateFormat.value)}` : t(dateFormat.value); } }); const isMergedDateInBounds = (date: Date) => { if (resolvedIsDateDisabled.value?.(startOfDay(date))) { return false; } if (props.min && date < setMilliseconds(props.min, 0)) { return false; } if (props.max && date > setMilliseconds(props.max, 0)) { return false; } return true; }; const resolvedDateProps = computed(() => ({ disabled: props.disabled, isDateDisabled: props.isDateDisabled, max: props.max, min: props.min, readonly: props.readonly, required: props.required, errorMessage: props.errorMessage, ...props.dateProps, description: dateDescription.value, })); const resolvedTimeProps = computed(() => ({ disabled: props.disabled, errorMessage: props.errorMessage, readonly: props.readonly, required: props.required, ...props.timeProps, })); watch( () => props.modelValue, newValue => { currentTimeText.value = newValue ? dateToTimeInputString(newValue) : ''; }, { immediate: true } ); const validateBounds = (value: BentoDateTimePickerValue) => !value || isMergedDateInBounds(value); const emitValidationState = (isValid: boolean) => { if (isValid) { emit('input:valid'); } else { emit('input:error'); } }; const emitModelValue = (value: BentoDateTimePickerValue) => { emit('update:model-value', value); }; const emitValidatedValue = (value: BentoDateTimePickerValue) => { emitModelValue(value); emitValidationState(validateBounds(value)); }; const emitInvalidModelValue = (value: BentoDateTimePickerValue) => { emitModelValue(value); emitValidationState(false); }; const onDateChanged = (date: Date | null) => { if (!date) { emitValidatedValue(null); return; } const time = currentTimeText.value; if (time && !isValidTimeString(time)) { emitInvalidModelValue(null); return; } emitValidatedValue(setTimeInDateObject(date, time || '00:00:00')); }; const onTimeChanged = (time: string) => { currentTimeText.value = time; if (!time) { emitValidatedValue(null); return; } if (!isValidTimeString(time)) { emit('input:error'); return; } if (!props.modelValue) { emitValidatedValue(null); return; } emitValidatedValue(setTimeInDateObject(props.modelValue, time)); }; const onTimeValid = () => { emitValidationState(validateBounds(props.modelValue)); }; const onTimeError = () => { emit('input:error'); }; </script> <script lang="ts"> /** * Date time picker that combines a date picker (single date) with a time input. * * @example * import { BentoDateTimePicker } from '@adyen/bento-vue2'; * * export default { * components: { BentoDateTimePicker }, * template: ` * <bento-date-time-picker * :date-props="{ label: 'Date' }" * :time-props="{ label: 'Time' }" * required * :model-value="dateTimeValue" * @update:model-value="newValue => dateTimeValue = newValue" * /> * ` * } */ export default { i18n: { messages }, name: 'bento-date-time-picker', inheritAttrs: false, model: { prop: 'modelValue', event: 'update:model-value' }, }; </script> <style lang="scss" scoped src="./date-time-picker.scss" />
|
|
@@ -6,33 +6,23 @@ import * as DraggableStories from './draggable.stories';
|
|
|
6
6
|
# Draggable
|
|
7
7
|
|
|
8
8
|
The `bento-draggable` component allows you to create lists of items that can be reordered by dragging and dropping. It
|
|
9
|
-
is a wrapper around
|
|
9
|
+
is a wrapper around [SortableJS](https://github.com/SortableJS/Sortable).
|
|
10
10
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
For accessibility and to provide a handle for dragging, each draggable item should include a `bento-draggable-handle`
|
|
17
|
-
component. This handle provides a grab icon and a menu with actions for reordering items using a keyboard.
|
|
13
|
+
Provide a list of `items` (or use `v-model`) and render each item via the `default` slot. Each item should include a
|
|
14
|
+
`bento-draggable-handle` for both drag-and-drop and keyboard reordering.
|
|
18
15
|
|
|
19
16
|
<Canvas withSource="closed">
|
|
20
17
|
<Story of={DraggableStories.Default} />
|
|
21
18
|
</Canvas>
|
|
22
19
|
|
|
23
|
-
## Customization
|
|
24
|
-
|
|
25
|
-
You can render any custom component or HTML structure within the default slot. The slot provides the `item` and its
|
|
26
|
-
`index` from the `items` array, allowing you to build complex draggable elements.
|
|
27
|
-
|
|
28
20
|
## Nested Draggable Lists
|
|
29
21
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
`group` name in their `options` prop.
|
|
22
|
+
Recursively render `bento-draggable` components for tree-like structures. All instances must share the same `group` name
|
|
23
|
+
in their `options` prop to allow dragging between levels.
|
|
33
24
|
|
|
34
|
-
The component
|
|
35
|
-
items within the default slot, as shown in this example.
|
|
25
|
+
> The component does not handle `children` directly — render nested items yourself within the default slot.
|
|
36
26
|
|
|
37
27
|
<Canvas withSource="closed">
|
|
38
28
|
<Story of={DraggableStories.Nested} />
|
|
@@ -40,137 +30,88 @@ items within the default slot, as shown in this example.
|
|
|
40
30
|
|
|
41
31
|
## Grid Layout
|
|
42
32
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
For the `bento-draggable-handle` to display all movement options (including left and right), set its `variant` prop to
|
|
47
|
-
`all-directions`.
|
|
33
|
+
Style the draggable container with CSS Flexbox or Grid to create draggable grids. Set the handle's `variant` to
|
|
34
|
+
`all-directions` to display left/right movement options.
|
|
48
35
|
|
|
49
36
|
<Canvas withSource="closed">
|
|
50
37
|
<Story of={DraggableStories.Grid} />
|
|
51
38
|
</Canvas>
|
|
52
39
|
|
|
53
|
-
##
|
|
54
|
-
|
|
55
|
-
You can get a `ref` to the Draggable component to access the underlying SortableJS instance and other properties.
|
|
56
|
-
|
|
57
|
-
<Source
|
|
58
|
-
dark
|
|
59
|
-
language="html"
|
|
60
|
-
code={`
|
|
61
|
-
<template>
|
|
62
|
-
<bento-draggable ref="draggableRef" :items="items" />
|
|
63
|
-
</template>
|
|
64
|
-
|
|
65
|
-
<script setup lang="ts">
|
|
66
|
-
import { ref, onMounted } from 'vue';
|
|
40
|
+
## Multi-Draggable (Board)
|
|
67
41
|
|
|
68
|
-
|
|
42
|
+
Multiple `bento-draggable` instances can share a SortableJS `group` to allow items to be dragged between lists (e.g. a
|
|
43
|
+
Kanban board). This requires additional setup because `bento-draggable` only manages its own list — cross-list transfers
|
|
44
|
+
must be handled manually.
|
|
69
45
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
// Example: get the order of items
|
|
75
|
-
const order = sortableInstance.toArray();
|
|
76
|
-
console.log(order);
|
|
77
|
-
});
|
|
78
|
-
</script>
|
|
79
|
-
|
|
80
|
-
`} />
|
|
81
|
-
|
|
82
|
-
The following methods and properties are available on the component's `ref`:
|
|
46
|
+
<Canvas withSource="closed">
|
|
47
|
+
<Story of={DraggableStories.Board} />
|
|
48
|
+
</Canvas>
|
|
83
49
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
50
|
+
When building a multi-draggable layout, keep in mind:
|
|
51
|
+
|
|
52
|
+
- **Provide a custom `onSort`:** When a custom `onSort` is passed via `options`, the component's built-in auto-sort is
|
|
53
|
+
bypassed. You are responsible for updating the reactive list yourself to prevent double DOM reorders (SortableJS
|
|
54
|
+
mutates the DOM, then Vue would re-render from the model).
|
|
55
|
+
- **Handle `onAdd` / `onRemove` yourself:** SortableJS fires `onAdd` on the receiving list and `onRemove` on the source
|
|
56
|
+
list, but neither event carries the item data. Track the dragged item manually via `onStart` and splice it into the
|
|
57
|
+
target list in `onAdd`.
|
|
58
|
+
- **`moveLeft` / `moveRight` require custom implementation:** The built-in `moveLeft` and `moveRight` slot helpers only
|
|
59
|
+
move items within the same list (they are aliases of `moveUp` / `moveDown`). For cross-list keyboard movement,
|
|
60
|
+
override `disabledMenus` to enable the direction, then bind the handle's `@move-right` or `@move-left` to your own
|
|
61
|
+
transfer function.
|
|
62
|
+
- **Route announcements to the receiving list's ref:** Each `bento-draggable` instance has its own `announce` method
|
|
63
|
+
exposed via `ref`. When moving an item to another list, call `announce` on the **target** instance so the announcement
|
|
64
|
+
appears in the correct live region.
|
|
65
|
+
|
|
66
|
+
## Properties
|
|
67
|
+
|
|
68
|
+
| Property | Type | Default | Description |
|
|
69
|
+
| ----------------- | --------------------------- | ------- | -------------------------------------------------------------------------------------------------------- |
|
|
70
|
+
| `items` | `Array<BentoDraggableItem>` | `[]` | List of items to render. Supports `v-model`. |
|
|
71
|
+
| `el` | `string` | `div` | HTML tag for the container element. |
|
|
72
|
+
| `options` | `BentoDraggableOptions` | `{}` | Options passed to SortableJS. [See SortableJS docs](https://github.com/SortableJS/Sortable#readme). |
|
|
73
|
+
| `announcement` | `string \| 'disabled'` | `''` | Custom announcement text override. Set to `'disabled'` to suppress announcements. |
|
|
74
|
+
| `disableAutoSort` | `boolean` | `false` | Prevents automatic list reordering on sort. Use when providing a custom `onSort` callback via `options`. |
|
|
75
|
+
|
|
76
|
+
## Slots
|
|
77
|
+
|
|
78
|
+
The `default` slot provides the following properties:
|
|
79
|
+
|
|
80
|
+
| Name | Type | Description |
|
|
81
|
+
| --------------- | ------------------------- | ------------------------------------------------------------------------------ |
|
|
82
|
+
| `item` | `BentoDraggableItem` | The current item from the `items` array. |
|
|
83
|
+
| `index` | `number` | The index of the current item. |
|
|
84
|
+
| `moveUp` | `() => void` | Move the item one position up. Includes announcement. |
|
|
85
|
+
| `moveDown` | `() => void` | Move the item one position down. Includes announcement. |
|
|
86
|
+
| `moveLeft` | `() => void` | Alias for `moveUp`. Override for cross-list movement. |
|
|
87
|
+
| `moveRight` | `() => void` | Alias for `moveDown`. Override for cross-list movement. |
|
|
88
|
+
| `moveTop` | `() => void` | Move the item to the first position. Includes announcement. |
|
|
89
|
+
| `moveBottom` | `() => void` | Move the item to the last position. Includes announcement. |
|
|
90
|
+
| `disabledMenus` | `Record<string, boolean>` | Pre-calculated disabled state for each direction based on the item's position. |
|
|
91
|
+
|
|
92
|
+
## Exposed
|
|
93
|
+
|
|
94
|
+
Access the following via a template `ref` on the component:
|
|
95
|
+
|
|
96
|
+
| Name | Type | Description |
|
|
97
|
+
| ------------------ | ------------------------------------ | ---------------------------------------------------------------- |
|
|
98
|
+
| `isDragging` | `Ref<boolean>` | `true` while an item is being dragged. |
|
|
99
|
+
| `containerRef` | `Ref<HTMLElement>` | The container DOM element. |
|
|
100
|
+
| `draggableElement` | `Sortable` | The SortableJS instance (`toArray()`, `sort()`, `destroy()`, …). |
|
|
101
|
+
| `announce` | `(message: string) => void` | Trigger a screen reader announcement on this instance. |
|
|
102
|
+
| `moveItem` | `(from: number, to: number) => void` | Move an item between indices and emit the updated list. |
|
|
103
|
+
| `focus` | `(index: number) => void` | Focus the draggable handle at the given index. |
|
|
90
104
|
|
|
91
|
-
|
|
105
|
+
## Accessibility
|
|
92
106
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
| `option(name, value)` | Get or set a SortableJS option. |
|
|
96
|
-
| `destroy()` | Removes the sortable functionality completely. |
|
|
97
|
-
| `toArray()` | Returns an array of the `data-id` attributes of the items in their current order. |
|
|
98
|
-
| `sort(order)` | Sorts the items according to a specified array of `data-id`s. |
|
|
107
|
+
The `bento-draggable-handle` provides keyboard-accessible controls ("Move up", "Move down", "Move to top", "Move to
|
|
108
|
+
bottom") and an optional `all-directions` variant that adds "Move left" and "Move right".
|
|
99
109
|
|
|
100
|
-
|
|
110
|
+
`bento-draggable` **automatically** announces reordering actions to screen reader users via a built-in live region
|
|
111
|
+
(`role="status"`). Announcements fire both after drag-and-drop and after keyboard-initiated moves.
|
|
101
112
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
"Move down", "Move to top", and "Move to bottom" actions.
|
|
105
|
-
|
|
106
|
-
### Screen Reader Announcements
|
|
107
|
-
|
|
108
|
-
To provide a fully accessible experience, it is **strongly recommended** to announce reordering or movement actions to
|
|
109
|
-
screen reader users. The `bento-draggable` component exposes an `announce` function that can be used to provide these
|
|
110
|
-
updates.
|
|
111
|
-
|
|
112
|
-
**Note:** The component does not automatically announce movements. It is your responsibility to call this function
|
|
113
|
-
whenever the list order changes or an item is moved:
|
|
114
|
-
|
|
115
|
-
- **After dragging with the mouse:** When an item is dropped and the list is reordered within the same list, or
|
|
116
|
-
moved/cloned to another list.
|
|
117
|
-
- **After using keyboard controls:** When a user selects a reordering action from the `bento-draggable-handle` menu.
|
|
118
|
-
|
|
119
|
-
These announcements help users who rely on assistive technologies to understand the result of their actions and the
|
|
120
|
-
current state of the list.
|
|
121
|
-
|
|
122
|
-
<Source
|
|
123
|
-
dark
|
|
124
|
-
language="html"
|
|
125
|
-
code={`
|
|
126
|
-
<template>
|
|
127
|
-
<bento-draggable
|
|
128
|
-
ref="draggableRef"
|
|
129
|
-
:items="items"
|
|
130
|
-
:options="{ onSort }"
|
|
131
|
-
>
|
|
132
|
-
<template #default="{ item, index }">
|
|
133
|
-
<bento-draggable-handle
|
|
134
|
-
:label="item.label"
|
|
135
|
-
@move-up="moveUp(index)"
|
|
136
|
-
@move-down="moveDown(index)"
|
|
137
|
-
/>
|
|
138
|
-
<span>{{ item.label }}</span>
|
|
139
|
-
</template>
|
|
140
|
-
</bento-draggable>
|
|
141
|
-
</template>
|
|
142
|
-
|
|
143
|
-
<script setup lang="ts">
|
|
144
|
-
import { ref } from 'vue';
|
|
145
|
-
|
|
146
|
-
const draggableRef = ref(null);
|
|
147
|
-
const items = ref([
|
|
148
|
-
{ id: '1', label: 'Item 1' },
|
|
149
|
-
{ id: '2', label: 'Item 2' },
|
|
150
|
-
{ id: '3', label: 'Item 3' },
|
|
151
|
-
]);
|
|
152
|
-
|
|
153
|
-
const announceMove = (itemLabel: string, fromIndex: number, toIndex: number) => {
|
|
154
|
-
draggableRef.value?.announce(
|
|
155
|
-
\`Moved \${itemLabel} from position \${fromIndex + 1} to position \${toIndex + 1} of \${items.value.length}.\`
|
|
156
|
-
);
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
const onSort = (event) => {
|
|
160
|
-
const { oldIndex, newIndex } = event;
|
|
161
|
-
const item = items.value[oldIndex];
|
|
162
|
-
// ... update the items list ...
|
|
163
|
-
announceMove(item.label, oldIndex, newIndex);
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
const moveUp = (index) => {
|
|
167
|
-
const item = items.value[index];
|
|
168
|
-
// ... update the items list ...
|
|
169
|
-
announceMove(item.label, index, index - 1);
|
|
170
|
-
};
|
|
171
|
-
</script>
|
|
172
|
-
|
|
173
|
-
`} />
|
|
113
|
+
Use the `announcement` prop to override the default message, or set it to `'disabled'` to suppress announcements when
|
|
114
|
+
you handle them externally (e.g. via the exposed `announce` method in a multi-draggable setup).
|
|
174
115
|
|
|
175
116
|
## Resources
|
|
176
117
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from '@storybook/vue'; import { type BentoDraggableItems, type BentoDraggableOptions, type BentoDraggableProps, type BentoDraggableSortableEvent, } from './draggable.types'; import { action } from '@storybook/addon-actions'; import { isVue2 } from 'vue-demi'; import { ref } from 'vue'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import { BentoDraggableHandle } from './components/draggable-handle'; import { BentoTypography } from '../typography'; import { BentoButton } from '@/components/button'; import BinIcon from '@adyen/ui-assets-icons-16/vue/bin'; import BentoDraggable from './draggable.vue'; import BentoDraggableDefaultExample from './__tests__/bento-draggable-default-example.vue?raw'; import BentoDraggableNestedExample from './__tests__/bento-draggable-nested-example.vue?raw'; import BentoDraggableGridExample from './__tests__/bento-draggable-grid-example.vue?raw'; import './draggable.stories.scss?module'; const meta: Meta = { title: 'Draggable', component: BentoDraggable, }; export default meta; type Story = StoryObj<typeof BentoDraggable>; const ITEMS: BentoDraggableItems = [ { id: '1', label: 'Item 1' }, { id: '2', label: 'Item 2' }, { id: '3', label: 'Item 3' }, { id: '4', label: 'Item 4' }, { id: '5', label: 'Item 5' }, { id: '6', label: 'Item 6' }, ]; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoDraggable, BentoDraggableHandle, BentoTypography }, props: Object.keys(argTypes), template: ` <bento-draggable v-bind="args" ref="draggableElementRef" :items="itemsList" class="b-draggable-default__container" :options="options" > <template #default="{item, index}"> <div class="b-draggable-default__item" :key="item.id"> <div :style="{ display: 'flex', alignItems: 'center'}"> <bento-draggable-handle :label="item.label" condensed :disabled="item.label === 'Item 2'" :disabled-menus="{ up: index === 0, top: index === 0, down: index === itemsList.length - 1, bottom: index === itemsList.length - 1, }" variant="up-and-down" @move-up="() => moveUp(index)" @move-down="() => moveDown(index)" @move-top="() => moveTop(index)" @move-bottom="() => moveBottom(index)" /> <bento-typography>{{ item.label }}</bento-typography> </div> </div> </template> </bento-draggable> `, setup(props) { const args = isVue2 ? props : _args; const itemsList = ref(args.items); const draggableElementRef = ref<typeof BentoDraggable>(null); const shiftList = (indexOrigin: number, indexTarget: number) => { if (indexOrigin === -1 || indexTarget === -1) { return Array.from(itemsList.value); } const newList = Array.from(itemsList.value as BentoDraggableItems); const [removed] = newList.splice(indexOrigin, 1); newList.splice(indexTarget, 0, removed); const itemLabel = removed.label; draggableElementRef.value.announce( `You've moved ${itemLabel} from position ${indexOrigin + 1} to position ${indexTarget + 1} of ${itemsList.value.length}.` ); return newList; }; const moveUp = (index: number) => { itemsList.value = shiftList(index, index - 1); action('move-up')(itemsList.value); }; const moveDown = (index: number) => { itemsList.value = shiftList(index, index + 1); action('move-down')(itemsList.value); }; const moveTop = (index: number) => { itemsList.value = shiftList(index, 0); action('move-top')(itemsList.value); }; const moveBottom = (index: number) => { itemsList.value = shiftList(index, itemsList.value.length - 1); action('move-bottom')(itemsList.value); }; const onSort = (event: BentoDraggableSortableEvent) => { itemsList.value = shiftList(event.oldIndex, event.newIndex); action('sort')(event, itemsList.value); }; const options: BentoDraggableOptions = { onAdd: action('add'), onChange: () => action('change'), onChoose: action('choose'), onClone: action('clone'), onEnd: action('end'), onFilter: action('filter'), onMove: action('move'), onRemove: action('remove'), onStart: action('start'), onSort, onUnchoose: action('unchoose'), onUpdate: action('update'), }; return { // Values args: isVue2 ? props : _args, itemsList, draggableElementRef, // Events options, moveUp, moveDown, moveTop, moveBottom, }; }, }), args: { items: ITEMS, options: { group: 'nested', animation: 150, multiDrag: true, } as BentoDraggableProps, }, parameters: storybookDocsParameter(BentoDraggableDefaultExample), }; const NESTED_ITEMS: BentoDraggableItems = [ { id: '1.1', label: 'Item 1.1', children: [ { id: '2.1', label: 'Item 2.1' }, { id: '2.2', label: 'Item 2.2', children: [ { id: '3.1', label: 'Item 3.1' }, { id: '3.2', label: 'Item 3.2' }, ], }, { id: '2.3', label: 'Item 2.3' }, ], }, { id: '1.2', label: 'Item 1.2' }, { id: '1.3', label: 'Item 1.3' }, ]; export const Nested: Story = { render: (_args, { argTypes }) => ({ components: { BentoDraggable, BentoDraggableHandle, BentoTypography }, props: Object.keys(argTypes), template: ` <bento-draggable v-bind="args" class="b-draggable-default__container" v-on="listeners" > <template #default="{item, index}"> <div class="b-draggable-default__item" :key="item.id"> <div :style="{ display: 'flex', alignItems: 'center'}"> <bento-draggable-handle :label="item.label" condensed variant="up-and-down" /> <bento-typography>{{ item.label }}</bento-typography> </div> <!-- Children --> <template v-if="item.children"> <bento-draggable class="b-draggable-default__container" :options="args.options" :items="item.children" v-on="listeners" > <template v-slot:default="{item: childItem, index: childIndex}"> <div class="b-draggable-default__item" :key="childItem.id"> <div :style="{ display: 'flex', alignItems: 'center'}"> <bento-draggable-handle :label="childItem.label" condensed variant="up-and-down" /> <bento-typography>{{ childItem.label }}</bento-typography> </div> </div> </template> </bento-draggable> </template> </div> </template> </bento-draggable> `, setup(props) { const clickedAction = (eventName: string) => (event: BentoDraggableSortableEvent) => action(eventName)(event); const listeners = { add: clickedAction('add'), change: clickedAction('change'), choose: clickedAction('choose'), clone: clickedAction('clone'), end: clickedAction('end'), filter: clickedAction('filter'), move: clickedAction('move'), remove: clickedAction('remove'), start: clickedAction('start'), sort: clickedAction('sort'), unchoose: clickedAction('unchoose'), update: clickedAction('update'), }; return { // Values args: isVue2 ? props : _args, // Events listeners, }; }, }), args: { items: NESTED_ITEMS, options: { group: 'nested', animation: 150, }, } as BentoDraggableProps, parameters: storybookDocsParameter(BentoDraggableNestedExample), }; export const Grid: Story = { render: (_args, { argTypes }) => ({ components: { BentoButton, BentoDraggable, BentoDraggableHandle, BentoTypography, BinIcon }, props: Object.keys(argTypes), template: ` <bento-draggable v-bind="args" :style="{ padding: 'var(--b-spacer-050)', width: '670px', display: 'flex', flexDirection: 'row', flexWrap: 'wrap', gap: 'var(--b-spacer-050)', }" v-on="listeners" > <template #default="{item, index}"> <div :key="item.id" :style="{ width: '160px' }"> <div :style="{ display: 'flex', background: 'var(--b-color-background-tertiary)', borderTopLeftRadius: 'var(--b-border-radius-m)', borderTopRightRadius: 'var(--b-border-radius-m)', justifyContent: 'space-between', padding: 'var(--b-spacer-010) var(--b-spacer-020)', }" > <bento-draggable-handle :label="item.label" :disabled-menus="{ up: index <= 3, top: index <= 3, down: index > 3, bottom: index > 3, left: index === 0 || index === 4, right: index === 3 || index === 5, }" condensed variant="all-directions" /> <bento-button condensed variant="secondary"> <template #iconLeft> <bin-icon svg-title="remove" /> </template> </bento-button> </div> <div :style="{ backgroundColor: 'var(--b-color-background-secondary)', display: 'flex', flexDirection: 'column', borderBottomLeftRadius: 'var(--b-border-radius-m)', borderBottomRightRadius: 'var(--b-border-radius-m)', gap: 'var(--b-spacer-060)', padding: 'var(--b-spacer-040)', }" > <bento-typography>Card {{ index + 1 }}</bento-typography> </div> </div> </template> </bento-draggable> `, setup(props) { const clickedAction = (eventName: string) => (event: BentoDraggableSortableEvent) => action(eventName)(event); const listeners = { add: clickedAction('add'), change: clickedAction('change'), choose: clickedAction('choose'), clone: clickedAction('clone'), end: clickedAction('end'), filter: clickedAction('filter'), move: clickedAction('move'), remove: clickedAction('remove'), start: clickedAction('start'), sort: clickedAction('sort'), unchoose: clickedAction('unchoose'), update: clickedAction('update'), }; return { // Values args: isVue2 ? props : _args, // Events listeners, }; }, }), args: { items: ITEMS, options: { group: 'nested', animation: 150, } as BentoDraggableProps, }, parameters: storybookDocsParameter(BentoDraggableGridExample), };
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue'; import { type BentoDraggableItems, type BentoDraggableOptions, type BentoDraggableProps } from './draggable.types'; import { action } from '@storybook/addon-actions'; import { isVue2 } from 'vue-demi'; import { ref } from 'vue'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import { BentoDraggableHandle } from './components/draggable-handle'; import { BentoTypography } from '../typography'; import { BentoButton } from '@/components/button'; import BinIcon from '@adyen/ui-assets-icons-16/vue/bin'; import BentoDraggable from './draggable.vue'; import BentoDraggableDefaultExample from './__tests__/bento-draggable-default-example.vue?raw'; import BentoDraggableNestedExample from './__tests__/bento-draggable-nested-example.vue?raw'; import BentoDraggableGridExample from './__tests__/bento-draggable-grid-example.vue?raw'; import BentoDraggableBoardExample from './__tests__/bento-draggable-board-example.vue?raw'; import './draggable.stories.scss?module'; const meta: Meta = { title: 'Draggable', component: BentoDraggable, }; export default meta; type Story = StoryObj<typeof BentoDraggable>; const ITEMS: BentoDraggableItems = [ { id: '1', label: 'Item 1' }, { id: '2', label: 'Item 2' }, { id: '3', label: 'Item 3' }, { id: '4', label: 'Item 4' }, { id: '5', label: 'Item 5' }, { id: '6', label: 'Item 6' }, ]; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoDraggable, BentoDraggableHandle, BentoTypography }, props: Object.keys(argTypes), template: ` <bento-draggable v-bind="args" :items="itemsList" class="b-draggable-default__container" @update:items="onUpdateItems" > <template #default="{item, moveUp, moveDown, moveTop, moveBottom, disabledMenus}"> <div class="b-draggable-default__item" :key="item.id"> <div :style="{ display: 'flex', alignItems: 'center'}"> <bento-draggable-handle :label="item.label" condensed :disabled="item.label === 'Item 2'" :disabled-menus="disabledMenus" variant="up-and-down" @move-up="moveUp" @move-down="moveDown" @move-top="moveTop" @move-bottom="moveBottom" /> <bento-typography>{{ item.label }}</bento-typography> </div> </div> </template> </bento-draggable> `, setup(props) { const args = isVue2 ? props : _args; const itemsList = ref(args.items); const onUpdateItems = (items: BentoDraggableItems) => { itemsList.value = [...items]; action('update:items')(items); }; return { args: isVue2 ? props : _args, itemsList, onUpdateItems, }; }, }), args: { items: ITEMS, options: { group: 'default', }, }, parameters: storybookDocsParameter(BentoDraggableDefaultExample), }; const NESTED_ITEMS: BentoDraggableItems = [ { id: '1.1', label: 'Item 1.1', children: [ { id: '2.1', label: 'Item 2.1' }, { id: '2.2', label: 'Item 2.2', children: [ { id: '3.1', label: 'Item 3.1' }, { id: '3.2', label: 'Item 3.2' }, ], }, { id: '2.3', label: 'Item 2.3' }, ], }, { id: '1.2', label: 'Item 1.2' }, { id: '1.3', label: 'Item 1.3' }, ]; export const Nested: Story = { render: (_args, { argTypes }) => ({ components: { BentoDraggable, BentoDraggableHandle, BentoTypography }, props: Object.keys(argTypes), template: ` <bento-draggable v-bind="args" :items="itemsList" class="b-draggable-nested__container" @update:items="itemsList = $event" > <template #default="{item, moveUp, moveDown, moveTop, moveBottom, disabledMenus}"> <div class="b-draggable-nested__item" :key="item.id"> <div :style="{ display: 'flex', alignItems: 'center'}"> <bento-draggable-handle :label="item.label" condensed variant="up-and-down" :disabled-menus="disabledMenus" @move-up="moveUp" @move-down="moveDown" @move-top="moveTop" @move-bottom="moveBottom" /> <bento-typography>{{ item.label }}</bento-typography> </div> <!-- Children --> <template v-if="item.children"> <bento-draggable class="b-draggable-nested__container" :options="args.options" :items="item.children" @update:items="item.children = $event" > <template v-slot:default="{item: childItem, moveUp: moveUpChild, moveDown: moveDownChild, moveTop: moveTopChild, moveBottom: moveBottomChild, disabledMenus: disabledMenusChild}"> <div class="b-draggable-nested__item" :key="childItem.id"> <div :style="{ display: 'flex', alignItems: 'center'}"> <bento-draggable-handle :label="childItem.label" condensed variant="up-and-down" :disabled-menus="disabledMenusChild" @move-up="moveUpChild" @move-down="moveDownChild" @move-top="moveTopChild" @move-bottom="moveBottomChild" /> <bento-typography>{{ childItem.label }}</bento-typography> </div> </div> </template> </bento-draggable> </template> </div> </template> </bento-draggable> `, setup(props) { const args = isVue2 ? props : _args; const itemsList = ref(args.items); return { args, itemsList, }; }, }), args: { items: NESTED_ITEMS, options: {}, } as BentoDraggableProps, parameters: storybookDocsParameter(BentoDraggableNestedExample), }; export const Grid: Story = { render: (_args, { argTypes }) => ({ components: { BentoButton, BentoDraggable, BentoDraggableHandle, BentoTypography, BinIcon }, props: Object.keys(argTypes), template: ` <bento-draggable ref="draggableRef" v-bind="args" :items="itemsList" @update:items="itemsList = $event" :style="{ padding: 'var(--b-spacer-050)', width: '670px', display: 'flex', flexDirection: 'row', flexWrap: 'wrap', gap: 'var(--b-spacer-050)', }" > <template #default="{item, index, moveTop, moveBottom, moveLeft, moveRight, disabledMenus}"> <div :key="item.id" :style="{ width: '160px' }"> <div :style="{ display: 'flex', background: 'var(--b-color-background-tertiary)', borderTopLeftRadius: 'var(--b-border-radius-m)', borderTopRightRadius: 'var(--b-border-radius-m)', justifyContent: 'space-between', padding: 'var(--b-spacer-010) var(--b-spacer-020)', }" > <bento-draggable-handle :label="item.label" :disabled-menus="{ ...disabledMenus, up: index < COLUMNS, down: index + COLUMNS >= itemsList.length, left: index % COLUMNS === 0, right: index % COLUMNS === COLUMNS - 1 || index === itemsList.length - 1, }" condensed variant="all-directions" @move-up="gridMoveUp(index)" @move-down="gridMoveDown(index)" @move-top="moveTop" @move-bottom="moveBottom" @move-left="moveLeft" @move-right="moveRight" /> <bento-button condensed variant="secondary"> <template #iconLeft> <bin-icon svg-title="remove" /> </template> </bento-button> </div> <div :style="{ backgroundColor: 'var(--b-color-background-secondary)', display: 'flex', flexDirection: 'column', borderBottomLeftRadius: 'var(--b-border-radius-m)', borderBottomRightRadius: 'var(--b-border-radius-m)', gap: 'var(--b-spacer-060)', padding: 'var(--b-spacer-040)', }" > <bento-typography>Card {{ item.id }}</bento-typography> </div> </div> </template> </bento-draggable> `, setup(props) { const args = isVue2 ? props : _args; const itemsList = ref(args.items); const draggableRef = ref(null); const COLUMNS = 4; const gridMoveUp = (index: number) => { draggableRef.value?.moveItem(index, index - COLUMNS); }; const gridMoveDown = (index: number) => { draggableRef.value?.moveItem(index, index + COLUMNS); }; return { args, itemsList, draggableRef, COLUMNS, gridMoveUp, gridMoveDown, }; }, }), args: { items: ITEMS, options: {}, }, parameters: storybookDocsParameter(BentoDraggableGridExample), }; export const Board: Story = { render: (_args, { argTypes }) => ({ components: { BentoDraggable, BentoDraggableHandle, BentoTypography }, props: Object.keys(argTypes), template: ` <div :style="{ display: 'flex', gap: 'var(--b-spacer-100)', alignItems: 'flex-start', padding: 'var(--b-spacer-080)', backgroundColor: 'var(--b-color-background-tertiary)', minHeight: '400px' }"> <!-- TODO Column --> <div :style="{ flex: 1, background: 'var(--b-color-background-secondary)', padding: 'var(--b-spacer-040)', borderRadius: 'var(--b-border-radius-m)', boxShadow: 'var(--b-shadow-small)' }"> <bento-typography variant="title" :style="{ marginBottom: 'var(--b-spacer-040)', padding: 'var(--b-spacer-020) var(--b-spacer-040)', display: 'block' }"> TODO </bento-typography> <bento-draggable ref="todoDraggableRef" :items="todoItems" :options="toDoOptions" @update:items="todoItems = $event" class="b-draggable-board__container" style="min-height: 50px;" > <template #default="{ item, index, moveUp, moveDown, moveTop, moveBottom, disabledMenus }"> <div class="b-draggable-board__item" :key="item.id"> <div :style="{ display: 'flex', alignItems: 'center' }"> <bento-draggable-handle :label="item.label" condensed variant="all-directions" :disabled-menus="{ ...disabledMenus, right: false, left: true, }" @move-up="moveUp" @move-down="moveDown" @move-top="moveTop" @move-bottom="moveBottom" @move-right="moveToDone(index)" /> <bento-typography>{{ item.label }}</bento-typography> </div> </div> </template> </bento-draggable> </div> <!-- DONE Column --> <div :style="{ flex: 1, background: 'var(--b-color-background-secondary)', padding: 'var(--b-spacer-040)', borderRadius: 'var(--b-border-radius-m)', boxShadow: 'var(--b-shadow-small)' }"> <bento-typography variant="title" :style="{ marginBottom: 'var(--b-spacer-040)', padding: 'var(--b-spacer-020) var(--b-spacer-040)', display: 'block' }"> DONE </bento-typography> <bento-draggable ref="doneDraggableRef" :items="doneItems" :options="doneOptions" @update:items="doneItems = $event" class="b-draggable-board__container" style="min-height: 50px;" > <template #default="{ item, index, moveUp, moveDown, moveTop, moveBottom, moveLeft, disabledMenus }"> <div class="b-draggable-board__item" :key="item.id"> <div :style="{ display: 'flex', alignItems: 'center' }"> <bento-draggable-handle :label="item.label" condensed variant="all-directions" :disabled-menus="{ ...disabledMenus, left: false, }" @move-up="moveUp" @move-down="moveDown" @move-top="moveTop" @move-bottom="moveBottom" @move-left="moveToTodo(index)" /> <bento-typography>{{ item.label }}</bento-typography> </div> </div> </template> </bento-draggable> </div> </div> `, setup() { const todoItems = ref<BentoDraggableItems>([ { id: '1', label: 'Improve documentation' }, { id: '2', label: 'Fix navigation bug' }, { id: '3', label: 'Add unit tests' }, ]); const doneItems = ref<BentoDraggableItems>([{ id: '4', label: 'Initial release' }]); const todoDraggableRef = ref<InstanceType<typeof BentoDraggable> | null>(null); const doneDraggableRef = ref<InstanceType<typeof BentoDraggable> | null>(null); // Since we're moving items between completely separate refs, we track the item being dragged // manually to facilitate transfers between the todoItems and doneItems lists. let draggedItem: BentoDraggableItems[number] | null = null; const moveToDone = (index: number) => { const [item] = todoItems.value.splice(index, 1); doneItems.value.push(item); doneDraggableRef.value?.announce( `${item.label} moved to Done, position ${doneItems.value.length} of ${doneItems.value.length}` ); action('move-to-done')(item, { todo: todoItems.value, done: doneItems.value }); }; const moveToTodo = (index: number) => { const [item] = doneItems.value.splice(index, 1); todoItems.value.push(item); todoDraggableRef.value?.announce( `${item.label} moved to Todo, position ${todoItems.value.length} of ${todoItems.value.length}` ); action('move-to-todo')(item, { todo: todoItems.value, done: doneItems.value }); }; const toDoOptions: BentoDraggableOptions = { group: 'board', animation: 150, onStart: event => { draggedItem = todoItems.value[event.oldIndex!]; }, onAdd: event => { todoItems.value.splice(event.newIndex!, 0, draggedItem!); todoDraggableRef.value?.announce( `${draggedItem!.label} added to Todo, position ${event.newIndex! + 1} of ${todoItems.value.length}` ); }, onRemove: event => { todoItems.value.splice(event.oldIndex!, 1); }, onSort: event => { const { oldIndex, newIndex, from, to } = event; if (from === to && oldIndex !== undefined && newIndex !== undefined) { const [item] = todoItems.value.splice(oldIndex, 1); todoItems.value.splice(newIndex, 0, item); todoDraggableRef.value?.announce( `${item.label} moved to position ${newIndex + 1} of ${todoItems.value.length} in Todo` ); } }, }; const doneOptions: BentoDraggableOptions = { group: 'board', animation: 150, onStart: event => { draggedItem = doneItems.value[event.oldIndex!]; }, onAdd: event => { doneItems.value.splice(event.newIndex!, 0, draggedItem!); doneDraggableRef.value?.announce( `${draggedItem!.label} added to Done, position ${event.newIndex! + 1} of ${doneItems.value.length}` ); }, onRemove: event => { doneItems.value.splice(event.oldIndex!, 1); }, onSort: event => { const { oldIndex, newIndex, from, to } = event; if (from === to && oldIndex !== undefined && newIndex !== undefined) { const [item] = doneItems.value.splice(oldIndex, 1); doneItems.value.splice(newIndex, 0, item); doneDraggableRef.value?.announce( `${item.label} moved to position ${newIndex + 1} of ${doneItems.value.length} in Done` ); action('sort-done')(event, doneItems.value); } }, }; return { todoItems, doneItems, toDoOptions, doneOptions, moveToDone, moveToTodo, todoDraggableRef, doneDraggableRef, }; }, }), parameters: storybookDocsParameter(BentoDraggableBoardExample), };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import { type SortableOptions } from 'sortablejs'; import { type MoveEvent, type SortableEvent } from 'sortablejs'; /** * Event instances generated by each of the events * emitted by the draggable component. */ export type BentoDraggableSortableEvent = SortableEvent; /** * Event emitted by the draggable move event */ export type BentoDraggableMoveEvent = MoveEvent; /** * Options to be passed to SortableJS. * @see {@link https://github.com/SortableJS/Sortable#readme | SortableJS documentation} for all the details of the options available */ export type BentoDraggableOptions = Omit<SortableOptions, 'handle' | 'filter'>; /** * Events to hook on to the draggable component through the options */ export type BentoDraggableEvents = Pick< BentoDraggableOptions, | 'onUnchoose' | 'onChoose' | 'onStart' | 'onEnd' | 'onAdd' | 'onUpdate' | 'onSort' | 'onRemove' | 'onFilter' | 'onMove' | 'onClone' | 'onChange' >; /** * Item data of the items to be rendered as draggable elements */ export interface BentoDraggableItem { label: string; [x: string]: any; children?: Array<BentoDraggableItem>; } /** * List of items to be rendered as draggable elements */ export type BentoDraggableItems = Array<BentoDraggableItem>; export interface BentoDraggableProps { /** * Element that will contain the draggable elements. * @default 'div' */ el?: keyof HTMLElementTagNameMap; /** * List of items to be rendered as draggable elements * @default [] */ items?: Array<BentoDraggableItem>; /** * Options to be passed to SortableJS. * @see {@link https://github.com/SortableJS/Sortable#readme | SortableJS documentation} for all the details of the options available * @default {} */ options?: BentoDraggableOptions; }
|
|
1
|
+
import { type SortableOptions } from 'sortablejs'; import { type MoveEvent, type SortableEvent } from 'sortablejs'; /** * Event instances generated by each of the events * emitted by the draggable component. */ export type BentoDraggableSortableEvent = SortableEvent; /** * Event emitted by the draggable move event */ export type BentoDraggableMoveEvent = MoveEvent; /** * Options to be passed to SortableJS. * @see {@link https://github.com/SortableJS/Sortable#readme | SortableJS documentation} for all the details of the options available */ export type BentoDraggableOptions = Omit<SortableOptions, 'handle' | 'filter'>; /** * Events to hook on to the draggable component through the options */ export type BentoDraggableEvents = Pick< BentoDraggableOptions, | 'onUnchoose' | 'onChoose' | 'onStart' | 'onEnd' | 'onAdd' | 'onUpdate' | 'onSort' | 'onRemove' | 'onFilter' | 'onMove' | 'onClone' | 'onChange' >; /** * Item data of the items to be rendered as draggable elements */ export interface BentoDraggableItem { label: string; [x: string]: any; children?: Array<BentoDraggableItem>; } /** * List of items to be rendered as draggable elements */ export type BentoDraggableItems = Array<BentoDraggableItem>; /** * Events emitted by the draggable component */ export interface BentoDraggableEmits { /** * Emitted when the items list is updated */ (e: 'update:items', items: BentoDraggableItems): void; } export interface BentoDraggableProps { /** * Custom announcement message override. When set, this value is used instead of the * default reorder announcement. Set to `'disabled'` to suppress announcements entirely. */ announcement?: string | 'disabled'; /** * When enabled, the component will not automatically reorder items on sort. * Use this when providing a custom `onSort` callback via options. * @default false */ disableAutoSort?: boolean; /** * Element that will contain the draggable elements. * @default 'div' */ el?: keyof HTMLElementTagNameMap; /** * List of items to be rendered as draggable elements * @default [] */ items?: Array<BentoDraggableItem>; /** * Options to be passed to SortableJS. * @see {@link https://github.com/SortableJS/Sortable#readme | SortableJS documentation} for all the details of the options available * @default {} */ options?: BentoDraggableOptions; }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <component :is="el" ref="containerRef"> <slot v-for="(item, index) of items" :item="item" :index="index" name="default"></slot> </component> </template> <script setup lang="ts"> import { computed,
|
|
1
|
+
<template> <component :is="el" ref="containerRef"> <slot v-for="(item, index) of items" :item="item" :index="index" :move-up="() => moveItemUp(index)" :move-right="() => moveItemRight(index)" :move-down="() => moveItemDown(index)" :move-left="() => moveItemLeft(index)" :move-top="() => moveItemTop(index)" :move-bottom="() => moveItemBottom(index)" :disabled-menus="disabledMenuItems(index)" name="default" ></slot> <div class="b-draggable__announcement" role="status" aria-live="polite"> {{ announcementMessage }} </div> </component> </template> <script setup lang="ts"> import { computed, nextTick, ref, watch } from 'vue'; import type { BentoDraggableEmits, BentoDraggableItems, BentoDraggableOptions, BentoDraggableProps, BentoDraggableSortableEvent, } from './draggable.types'; import { useBentoDraggable } from './composables/use-bento-draggable/use-bento-draggable'; import { useDraggableAnnouncement } from './composables/use-draggable-announcement'; import { useTrackKeyboardUser } from './composables/use-track-keyboard-user'; import { useI18n } from '@/utils/ts/i18n'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const props = withDefaults(defineProps<BentoDraggableProps>(), { el: 'div', items: () => [], options: () => ({}), announcement: '', disableAutoSort: false, }); const emit = defineEmits<BentoDraggableEmits>(); /** * Reference to the container of the draggable elements. */ const containerRef = ref<HTMLElement | null>(null); /** * Indicates if there is an element being dragged inside the container. */ const isDragging = ref<boolean>(false); /** * Internal copy of items used to track position changes for focus restoration. */ const internalItems = ref<BentoDraggableItems>([]); /** * Tracks the item being moved via keyboard so focus can be restored to the correct element. */ const lastMovedItem = ref<BentoDraggableItems[number] | null>(null); const computedOptions = computed<BentoDraggableOptions>(() => { if (!containerRef.value) { return {}; } return { ...props.options, onStart: (event: BentoDraggableSortableEvent) => { isDragging.value = true; if (props.options?.onStart) { props.options?.onStart(event); } }, onEnd: (event: BentoDraggableSortableEvent) => { // This is a hack to move the event to the end of the event queue. // cf this issue: https://github.com/SortableJS/Sortable/issues/1184 setTimeout(() => { isDragging.value = false; if (props.options?.onEnd) { props.options?.onEnd(event); } }); }, onSort: (event: BentoDraggableSortableEvent) => { // Always execute the user's custom onSort callback if provided if (props.options?.onSort) { props.options.onSort(event); return; } // Automatically sync the v-model unless the user explicitly disabled it if (!props.disableAutoSort) { const { oldIndex, newIndex, from, to } = event; if ( from === to && from === containerRef.value && oldIndex !== undefined && newIndex !== undefined ) { moveItem(oldIndex, newIndex); } } }, }; }); /** * Handle reordering announcements for screen readers. */ const { announcementMessage, announce } = useDraggableAnnouncement(); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const { draggableElement } = useBentoDraggable(containerRef, computedOptions); // When moving items between sibling `bento-draggable`, the item may be removed, causing the DOM to be destroyed and temporarily lose focus // This is an alternative to query element `:focus-within` as the `:focus-within` will be gone until the other `bento-draggable` adds it into the list const { isKeyboardUser } = useTrackKeyboardUser(); /** * Focuses the draggable handle button at the given index. * Waits for the next tick to ensure the DOM has been updated before focusing. * * @param index - The index of the item whose handle should receive focus. */ const focus = async (index: number) => { await nextTick(); if (index !== -1 && containerRef.value) { const itemElement = containerRef.value.children[index] as HTMLElement; const button = itemElement?.querySelector<HTMLButtonElement>('.b-draggable-handle button'); await nextTick(); button?.focus(); } }; /** * Moves an item from one position to another within the list, * emits the updated list, and announces the change for screen readers. * * @param indexOrigin - The current index of the item to move. * @param indexTarget - The desired index to move the item to. */ const moveItem = async (indexOrigin: number, indexTarget: number) => { if ( indexOrigin < 0 || indexTarget < 0 || indexOrigin >= props.items.length || indexTarget >= props.items.length || indexOrigin === indexTarget ) { return; } const newList = Array.from(props.items as BentoDraggableItems); const [removed] = newList.splice(indexOrigin, 1); newList.splice(indexTarget, 0, removed); lastMovedItem.value = removed; // Occasionally, there may be race conditions where the index is out-of-date due to the fast drag-and-drop or // other async operations like onEnd > setTimeout. // Filtering out null/undefined values ensures the list remains valid. emit('update:items', newList.filter(Boolean)); const itemLabel = removed.label; const message = props?.announcement || t('youHaveMovedFromPositionToPosition', { itemLabel, indexOrigin: indexOrigin + 1, indexTarget: indexTarget + 1, total: props.items.length, }); if (props.announcement !== 'disabled') { announce(message); } }; const moveItemUp = (index: number) => moveItem(index, index - 1); const moveItemDown = (index: number) => moveItem(index, index + 1); const moveItemLeft = (index: number) => moveItem(index, index - 1); const moveItemRight = (index: number) => moveItem(index, index + 1); const moveItemTop = (index: number) => moveItem(index, 0); const moveItemBottom = (index: number) => moveItem(index, props.items.length - 1); /** * Returns a pre-calculated disabled menus object for the current item. */ const disabledMenuItems = (index: number) => { return { up: index === 0, top: index === 0, down: index === props.items.length - 1, bottom: index === props.items.length - 1, left: true, right: true, }; }; /** * Watch for items changes and restore focus automatically */ watch( () => props.items, async newItems => { if (!newItems) { return; } if (isKeyboardUser.value && newItems.length > 0 && internalItems.value.length > 0) { if (lastMovedItem.value) { // Within-list reorder: focus the moved item at its new position const movedItemIndex = newItems.indexOf(lastMovedItem.value); if (movedItemIndex !== -1) { await focus(movedItemIndex); } lastMovedItem.value = null; } else if (newItems.length > internalItems.value.length) { // Cross-list addition: focus the newly added item const newItemIndex = newItems.findIndex(item => !internalItems.value.includes(item)); if (newItemIndex !== -1) { await focus(newItemIndex); } } } internalItems.value = [...newItems]; }, { deep: true, immediate: true } ); defineExpose({ /** * A ref to the container DOM element. */ containerRef, /** * The underlying SortableJS instance, which provides methods like `toArray()`, `sort()`, and `destroy()`. */ draggableElement, /** * A boolean ref that is `true` when an item is being dragged. */ isDragging, /** * Announces a message to screen reader users via the live region. */ announce, /** * Moves an item from one index to another and emits the updated list. */ moveItem, /** * Programmatically focuses the draggable handle button at the given index. */ focus, }); </script> <script lang="ts"> /** * Drag and drop container that transforms a list of items into draggable elements. * The library is based on SortableJS. * @see {@link https://github.com/SortableJS/Sortable#readme | SortableJS documentation} * * @example * import { BentoDraggable, BentoDraggableHandle, type BentoDraggableEvents } from '@adyen/bento-vue2'; * * export default { * components: { BentoDraggable, BentoDraggableHandle }, * template: ` * <bento-draggable * v-model="items" * condensed * :options="{ * group: 'nested', * animation: 150 * }" * > * <template #default="{ item, index, moveUp, moveDown }"> * <div :key="item.id"> * <bento-draggable-handle * :label="item.label" * @move-up="moveUp" * @move-down="moveDown" * /> * </div> * </template> * </bento-draggable> * `, * setup () { * const items = ref([ * { id: '1', label: 'Item 1' }, * { id: '2', label: 'Item 2' } * ]); * return { * items, * } * } * } */ export default { name: 'bento-draggable', model: { prop: 'items', event: 'update:items' }, i18n: { messages }, }; </script> <style lang="scss" scoped src="./draggable.scss" />
|
package/dist/assets/components/dropdown/components/dropdown-base-textbox/dropdown-base-textbox.vue
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div :class="$attrs.class" data-testid="textbox-wrapper" @click="onToggleDropdown" @keydown.escape="onCloseDropdown" > <bento-typography class="b-dropdown-base-textbox" :class="conditionalClasses" el="div" data-testid="base-textbox" > <input v-if="shouldShowInput" v-bind="ariaAttributes" :id="textboxId" ref="textbox" class="b-dropdown-base-textbox__input" role="combobox" type="text" aria-autocomplete="list" size="1" :value="open ? value : displayValue" :disabled="disabled ? true : null" :readonly="readonly" :required="showRequiredAttribute" @keydown="$emit(DropdownBaseTextboxEvent.KEYDOWN, $event)" @keydown.enter="onToggleDropdown" @input="onInputEvent" /> <div v-else :id="textboxId" v-bind="ariaAttributes" ref="textbox" class="b-dropdown-base-textbox__input" :class="comboboxConditionalClasses" :aria-readonly="readonly ? 'true' : null" role="combobox" :tabindex="disabled ? -1 : 0" > <template v-if="shouldShowSingleSelectSlot"> <slot v-if="hasSlot('display-value')" v-bind="selectedValueItem" name="display-value"></slot> <slot v-else v-bind="selectedValueItem"> {{ displayValue }} </slot> </template> <template v-else-if="shouldShowMultipleSlot"> <div class="b-dropdown-base-textbox__input-multiple-selected"> <div v-for="singleItem of selectedValueItems" :key="singleItem.value"> <div class="b-dropdown-base-textbox__input-selected-element"> <slot v-bind="singleItem"> {{ displayValue }} </slot> </div> </div> </div> </template> <template v-else>{{ displayValue }}</template> </div> <span v-if="additionalItemsSelected" class="b-dropdown-base-textbox__additional-items"> {{ additionalItemsSelected }} </span> <span v-if="hasSlot('icon') && !isFiltering" class="b-dropdown-base-textbox__icon" aria-hidden="true"> <slot name="icon"></slot> </span> <span v-if="isFiltering" class="b-dropdown-base-textbox__clear-search" :tabindex="disabled ? -1 : 0" data-testid="clear-search-button" @click.stop="onClearSearch" @keydown.enter.stop="onClearSearch" @keyup.space="onClearSearch" > <cross-circle-fill-small-icon v-if="size === BentoDropdownSize.SMALL" :svg-title="t('clearSearch')" /> <cross-circle-fill-icon v-else :svg-title="t('clearSearch')" /> </span> </bento-typography> </div> </template> <script setup lang="ts"> import { computed, defineComponent, nextTick, type PropType, ref, useAttrs, useSlots, watch } from 'vue'; import { generateUid } from '@/core/utils/ts'; import { useI18n } from '@/utils/ts/i18n'; import { useHasSlot } from '@/composables'; import { BentoTypography } from '@/components/typography'; import { type Booleanish } from '@/types/prop-types'; import { type BentoListboxOptionItem } from '@/types/listbox'; import type { HTMLAttributes } from 'vue/types/jsx.d.ts'; import { DropdownBaseTextboxEvent, DropdownBaseTextboxState } from './dropdown-base-textbox.types'; import CrossCircleFillIcon from '@adyen/ui-assets-icons-16/vue/cross-circle-fill'; import CrossCircleFillSmallIcon from '@adyen/ui-assets-icons-16/vue/cross-circle-fill-small'; import { BentoDropdownSize } from '@/components/dropdown/dropdown.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const { t } = useI18n<{ message: MessageSchema }>({ messages }); const attrs = useAttrs(); const slots = useSlots(); const hasSlot = useHasSlot(slots); const props = defineProps({ /** * The number of additional items that have been selected in the dropdown. */ additionalItemsSelected: { type: [Number, String] as PropType<number | string>, default: 0 }, /** * If the combobox DOM element should be an `input` element. */ alwaysComboboxIsInput: { type: Boolean, default: false, }, /** * Sets the aria-controls attribute on the textbox. This should correspond to the popover's id that the textbox triggers. */ ariaControls: { type: String, required: true }, /** * Indicates whether the content the textbox triggers is visible or not. Needs to be set accordingly when the textbox is used and changed every time the popover is shown/hidden. */ ariaExpanded: { type: [String, Boolean] as PropType<Booleanish>, required: true, }, /** * If no label element is used then the aria-label is be set should be set. */ ariaLabel: { type: String, default: null }, /** * Corresponds to the label's id that labels the textbox element. */ ariaLabelledby: { type: String, default: null }, /** * Disables any textbox functionality */ disabled: { type: Boolean, default: false }, /** * Selected option's label */ displayValue: { type: String, default: null }, /** * Sets the possibility to type into the textbox. */ dynamicFiltering: { type: Boolean, default: false }, /** * Marks the textbox as invalid, rendering an error state */ isInvalid: { 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 respective event can be emitted */ open: { type: Boolean, default: false }, /** * Sets the dropdown to readonly state */ readonly: { type: Boolean, default: false }, /** * The input value */ value: { type: String, default: '' }, /** * The selected value's list box item. */ selectedValueItem: { type: [Object, Array] as PropType<BentoListboxOptionItem | Array<BentoListboxOptionItem>>, default: undefined, }, /** * Toggles whether to use the slot content select items shown in the textbox in multiple mode. */ showSlotContentInMultiple: { type: Boolean, default: false }, /** * Dropdown size */ size: { type: String as PropType<BentoDropdownSize | `${BentoDropdownSize}`>, default: null, }, }); const emit = defineEmits<{ /** * Emitted when the "clear" button is clicked */ (e: DropdownBaseTextboxEvent.CLEAR); /** * Emitted on click, space, enter and esc user events. It will close the dropdown. */ (e: DropdownBaseTextboxEvent.CLOSE); /** * Emitted when `dynamicFiltering` is enabled and a search input is entered */ (e: DropdownBaseTextboxEvent.INPUT, searchValue: string); /** * Emitted when any key is pressed and the focus is on the input field */ (e: DropdownBaseTextboxEvent.KEYDOWN, event: KeyboardEvent); /** * Emitted on click, space and enter user events. It will open the dropdown. */ (e: DropdownBaseTextboxEvent.OPEN); }>(); const textbox = ref(null); const textboxId = generateUid('textbox'); const conditionalClasses = computed(() => ({ [`b-dropdown-base-textbox--${DropdownBaseTextboxState.DISABLED}`]: props.disabled, [`b-dropdown-base-textbox--${DropdownBaseTextboxState.READONLY}`]: props.readonly, [`b-dropdown-base-textbox--${DropdownBaseTextboxState.HAS_ITEMS}`]: !!props.additionalItemsSelected, })); const showRequiredAttribute = computed(() => shouldShowInput.value && attrs['aria-required'] ? ('true' as Booleanish) : null ); const comboboxConditionalClasses = computed(() => ({ [`b-dropdown-base-textbox__input--dynamic-filtering`]: props.dynamicFiltering, })); const isFiltering = computed(() => props.dynamicFiltering && !!props.value && props.open); const ariaAttributes = computed( () => ({ 'aria-haspopup': 'listbox', 'aria-controls': props.ariaControls, 'aria-describedby': attrs['aria-describedby']?.toString() as string, 'aria-expanded': props.ariaExpanded ? 'true' : 'false', 'aria-labelledby': props.ariaLabelledby, 'aria-label': props.ariaLabelledby ? null : props.ariaLabel, 'aria-invalid': props.isInvalid, 'aria-disabled': props.disabled ? true : null, 'aria-required': attrs['aria-required'] as Booleanish, }) as HTMLAttributes ); // Cast selectedValueItem if it is an array of selected items const selectedValueItems = computed(() => props.multiple ? (props.selectedValueItem as Array<BentoListboxOptionItem>) : [] ); const shouldShowInput = computed(() => props.dynamicFiltering && (props.open || props.alwaysComboboxIsInput)); const shouldShowSingleSelectSlot = computed(() => props.selectedValueItem && !props.multiple); const shouldShowMultipleSlot = computed( () => props.multiple && props.selectedValueItem && selectedValueItems.value.length > 0 && props.showSlotContentInMultiple ); watch( () => shouldShowInput.value, () => { if (shouldShowInput.value && !props.alwaysComboboxIsInput) { nextTick(() => { textbox.value.focus(); }); } } ); const onOpenDropdown = async () => { if (!props.disabled && !props.readonly) { emit(DropdownBaseTextboxEvent.OPEN); // wait for popover to open before setting focus await nextTick(); textbox.value.focus(); } }; const onCloseDropdown = () => { if (props.open) { textbox.value.focus(); emit(DropdownBaseTextboxEvent.CLOSE); } }; const onToggleDropdown = (event: Event) => { const isDynamicFilteringInputClickEvent = props.open && (event.target as HTMLDivElement).tagName === 'INPUT'; if (isDynamicFilteringInputClickEvent) { /** * When there's a click even on the input during filtering * and the popover is open, do not toggle the popover. * Allow for click events inside the input field */ return null; } return props.open ? onCloseDropdown() : onOpenDropdown(); }; const onClearSearch = () => { emit(DropdownBaseTextboxEvent.CLEAR); emit(DropdownBaseTextboxEvent.INPUT, ''); textbox.value.focus(); }; const onInputEvent = (event: Event) => { if (!props.readonly && !props.disabled) { emit(DropdownBaseTextboxEvent.INPUT, (event.target as HTMLInputElement).value); } else { event.preventDefault(); event.stopPropagation(); } }; const focus = () => { textbox.value.focus(); }; defineExpose({ focus, }); </script> <script lang="ts"> /** * Only for internal use, implements the logic of the textbox element to be implemented in the combobox. */ export default defineComponent({ i18n: { messages }, name: 'dropdown-base-textbox', inheritAttrs: false, }); </script> <style lang="scss" scoped src="./dropdown-base-textbox.scss" />
|
|
1
|
+
<template> <div :class="$attrs.class" data-testid="textbox-wrapper" @click="onToggleDropdown" @keydown.escape="onCloseDropdown" > <bento-typography class="b-dropdown-base-textbox" :class="conditionalClasses" :variant="isUnderlineStyle ? 'title' : 'body'" el="div" data-testid="base-textbox" > <input v-if="shouldShowInput" v-bind="ariaAttributes" :id="textboxId" ref="textbox" class="b-dropdown-base-textbox__input" role="combobox" type="text" aria-autocomplete="list" size="1" :value="open ? value : displayValue" :disabled="disabled ? true : null" :readonly="readonly" :required="showRequiredAttribute" @keydown="$emit(DropdownBaseTextboxEvent.KEYDOWN, $event)" @keydown.enter="onToggleDropdown" @input="onInputEvent" /> <div v-else :id="textboxId" v-bind="ariaAttributes" ref="textbox" class="b-dropdown-base-textbox__input" :class="comboboxConditionalClasses" :aria-readonly="readonly ? 'true' : null" role="combobox" :tabindex="disabled ? -1 : 0" > <template v-if="shouldShowSingleSelectSlot"> <slot v-if="hasSlot('display-value')" v-bind="selectedValueItem" name="display-value"></slot> <slot v-else v-bind="selectedValueItem"> {{ displayValue }} </slot> </template> <template v-else-if="shouldShowMultipleSlot"> <div class="b-dropdown-base-textbox__input-multiple-selected"> <div v-for="singleItem of selectedValueItems" :key="singleItem.value"> <div class="b-dropdown-base-textbox__input-selected-element"> <slot v-bind="singleItem"> {{ displayValue }} </slot> </div> </div> </div> </template> <template v-else>{{ displayValue }}</template> </div> <span v-if="additionalItemsSelected" class="b-dropdown-base-textbox__additional-items"> {{ additionalItemsSelected }} </span> <span v-if="hasSlot('icon') && !isFiltering" class="b-dropdown-base-textbox__icon" aria-hidden="true"> <slot name="icon"></slot> </span> <span v-if="isFiltering" class="b-dropdown-base-textbox__clear-search" :tabindex="disabled ? -1 : 0" data-testid="clear-search-button" @click.stop="onClearSearch" @keydown.enter.stop="onClearSearch" @keyup.space="onClearSearch" > <cross-circle-fill-small-icon v-if="size === BentoDropdownSize.SMALL" :svg-title="t('clearSearch')" /> <cross-circle-fill-icon v-else :svg-title="t('clearSearch')" /> </span> </bento-typography> </div> </template> <script setup lang="ts"> import { computed, defineComponent, inject, nextTick, type PropType, ref, useAttrs, useSlots, watch } from 'vue'; import { generateUid } from '@/core/utils/ts'; import { useI18n } from '@/utils/ts/i18n'; import { useHasSlot } from '@/composables'; import { BentoTypography } from '@/components/typography'; import { type Booleanish } from '@/types/prop-types'; import { type BentoListboxOptionItem } from '@/types/listbox'; import type { HTMLAttributes } from 'vue/types/jsx.d.ts'; import { DropdownBaseTextboxEvent, DropdownBaseTextboxState } from './dropdown-base-textbox.types'; import CrossCircleFillIcon from '@adyen/ui-assets-icons-16/vue/cross-circle-fill'; import CrossCircleFillSmallIcon from '@adyen/ui-assets-icons-16/vue/cross-circle-fill-small'; import { BentoDropdownSize } from '@/components/dropdown/dropdown.types'; import { DROPDOWN_UNDERLINE_STYLE_INJECTION_KEY } from '../../dropdown.keys'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const { t } = useI18n<{ message: MessageSchema }>({ messages }); const attrs = useAttrs(); const slots = useSlots(); const hasSlot = useHasSlot(slots); const props = defineProps({ /** * The number of additional items that have been selected in the dropdown. */ additionalItemsSelected: { type: [Number, String] as PropType<number | string>, default: 0 }, /** * If the combobox DOM element should be an `input` element. */ alwaysComboboxIsInput: { type: Boolean, default: false, }, /** * Sets the aria-controls attribute on the textbox. This should correspond to the popover's id that the textbox triggers. */ ariaControls: { type: String, required: true }, /** * Indicates whether the content the textbox triggers is visible or not. Needs to be set accordingly when the textbox is used and changed every time the popover is shown/hidden. */ ariaExpanded: { type: [String, Boolean] as PropType<Booleanish>, required: true, }, /** * If no label element is used then the aria-label is be set should be set. */ ariaLabel: { type: String, default: null }, /** * Corresponds to the label's id that labels the textbox element. */ ariaLabelledby: { type: String, default: null }, /** * Disables any textbox functionality */ disabled: { type: Boolean, default: false }, /** * Selected option's label */ displayValue: { type: String, default: null }, /** * Sets the possibility to type into the textbox. */ dynamicFiltering: { type: Boolean, default: false }, /** * Marks the textbox as invalid, rendering an error state */ isInvalid: { 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 respective event can be emitted */ open: { type: Boolean, default: false }, /** * Sets the dropdown to readonly state */ readonly: { type: Boolean, default: false }, /** * The input value */ value: { type: String, default: '' }, /** * The selected value's list box item. */ selectedValueItem: { type: [Object, Array] as PropType<BentoListboxOptionItem | Array<BentoListboxOptionItem>>, default: undefined, }, /** * Toggles whether to use the slot content select items shown in the textbox in multiple mode. */ showSlotContentInMultiple: { type: Boolean, default: false }, /** * Dropdown size */ size: { type: String as PropType<BentoDropdownSize | `${BentoDropdownSize}`>, default: null, }, }); const emit = defineEmits<{ /** * Emitted when the "clear" button is clicked */ (e: DropdownBaseTextboxEvent.CLEAR); /** * Emitted on click, space, enter and esc user events. It will close the dropdown. */ (e: DropdownBaseTextboxEvent.CLOSE); /** * Emitted when `dynamicFiltering` is enabled and a search input is entered */ (e: DropdownBaseTextboxEvent.INPUT, searchValue: string); /** * Emitted when any key is pressed and the focus is on the input field */ (e: DropdownBaseTextboxEvent.KEYDOWN, event: KeyboardEvent); /** * Emitted on click, space and enter user events. It will open the dropdown. */ (e: DropdownBaseTextboxEvent.OPEN); }>(); const textbox = ref(null); const textboxId = generateUid('textbox'); // Inject underline style key to change the dropdown layout const isUnderlineStyle = inject(DROPDOWN_UNDERLINE_STYLE_INJECTION_KEY, false); const conditionalClasses = computed(() => ({ [`b-dropdown-base-textbox--${DropdownBaseTextboxState.DISABLED}`]: props.disabled, [`b-dropdown-base-textbox--${DropdownBaseTextboxState.READONLY}`]: props.readonly, [`b-dropdown-base-textbox--${DropdownBaseTextboxState.HAS_ITEMS}`]: !!props.additionalItemsSelected, 'b-dropdown-base-textbox--underline': isUnderlineStyle, })); const showRequiredAttribute = computed(() => shouldShowInput.value && attrs['aria-required'] ? ('true' as Booleanish) : null ); const comboboxConditionalClasses = computed(() => ({ [`b-dropdown-base-textbox__input--dynamic-filtering`]: props.dynamicFiltering, })); const isFiltering = computed(() => props.dynamicFiltering && !!props.value && props.open); const ariaAttributes = computed( () => ({ 'aria-haspopup': 'listbox', 'aria-controls': props.ariaControls, 'aria-describedby': attrs['aria-describedby']?.toString() as string, 'aria-expanded': props.ariaExpanded ? 'true' : 'false', 'aria-labelledby': props.ariaLabelledby, 'aria-label': props.ariaLabelledby ? null : props.ariaLabel, 'aria-invalid': props.isInvalid, 'aria-disabled': props.disabled ? true : null, 'aria-required': attrs['aria-required'] as Booleanish, }) as HTMLAttributes ); // Cast selectedValueItem if it is an array of selected items const selectedValueItems = computed(() => props.multiple ? (props.selectedValueItem as Array<BentoListboxOptionItem>) : [] ); const shouldShowInput = computed(() => props.dynamicFiltering && (props.open || props.alwaysComboboxIsInput)); const shouldShowSingleSelectSlot = computed(() => props.selectedValueItem && !props.multiple); const shouldShowMultipleSlot = computed( () => props.multiple && props.selectedValueItem && selectedValueItems.value.length > 0 && props.showSlotContentInMultiple ); watch( () => shouldShowInput.value, () => { if (shouldShowInput.value && !props.alwaysComboboxIsInput) { nextTick(() => { textbox.value.focus(); }); } } ); const onOpenDropdown = async () => { if (!props.disabled && !props.readonly) { emit(DropdownBaseTextboxEvent.OPEN); // wait for popover to open before setting focus await nextTick(); textbox.value.focus(); } }; const onCloseDropdown = () => { if (props.open) { textbox.value.focus(); emit(DropdownBaseTextboxEvent.CLOSE); } }; const onToggleDropdown = (event: Event) => { const isDynamicFilteringInputClickEvent = props.open && (event.target as HTMLDivElement).tagName === 'INPUT'; if (isDynamicFilteringInputClickEvent) { /** * When there's a click even on the input during filtering * and the popover is open, do not toggle the popover. * Allow for click events inside the input field */ return null; } return props.open ? onCloseDropdown() : onOpenDropdown(); }; const onClearSearch = () => { emit(DropdownBaseTextboxEvent.CLEAR); emit(DropdownBaseTextboxEvent.INPUT, ''); textbox.value.focus(); }; const onInputEvent = (event: Event) => { if (!props.readonly && !props.disabled) { emit(DropdownBaseTextboxEvent.INPUT, (event.target as HTMLInputElement).value); } else { event.preventDefault(); event.stopPropagation(); } }; const focus = () => { textbox.value.focus(); }; defineExpose({ focus, }); </script> <script lang="ts"> /** * Only for internal use, implements the logic of the textbox element to be implemented in the combobox. */ export default defineComponent({ i18n: { messages }, name: 'dropdown-base-textbox', inheritAttrs: false, }); </script> <style lang="scss" scoped src="./dropdown-base-textbox.scss" />
|
package/dist/assets/components/dropdown/components/dropdown-small-textbox/dropdown-small-textbox.vue
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div class="b-dropdown-small-textbox" :class="conditionalClasses"> <dropdown-base-textbox ref="baseTextbox" v-bind="{ ...$attrs, ...$props }" class="b-dropdown-small-textbox__textbox" :size="BentoDropdownSize.SMALL" v-on="listeners" > <template #icon> <div class="b-dropdown-small-textbox__chevron"> <chevron-up-small-icon v-show="isDropdownOpen" svg-title="opened" /> <chevron-down-small-icon v-show="!isDropdownOpen" svg-title="closed" /> </div> </template> </dropdown-base-textbox> </div> </template> <script setup lang="ts"> import { computed, defineComponent, type PropType, ref } from 'vue'; import { DropdownBaseTextboxEvent, DropdownBaseTextboxState, } from '@/components/dropdown/components/dropdown-base-textbox/dropdown-base-textbox.types'; import { BentoDropdownSize } from '@/components/dropdown/dropdown.types'; import { type Booleanish } from '@/types/prop-types'; import DropdownBaseTextbox from '@/components/dropdown/components/dropdown-base-textbox/dropdown-base-textbox.vue'; import ChevronDownSmallIcon from '@adyen/ui-assets-icons-16/vue/chevron-down-small'; import ChevronUpSmallIcon from '@adyen/ui-assets-icons-16/vue/chevron-up-small'; import { stopEventPropagation } from '@/utils/ts/events'; const emit = defineEmits<{ /** * Emitted on clearing the input. */ (e: 'clear'); /** * Emitted on click, space and enter user events. It will open the dropdown. */ (e: 'close'); /** * Emitted on input event. */ (e: 'input', newInput: string); /** * Emitted on keydown event. */ (e: 'keydown', keydownEvent: KeyboardEvent); /** * Emitted on click, space, enter and esc user events. It will close the dropdown. */ (e: 'open'); }>(); const props = defineProps({ /** * The number of additional items that have been selected in the dropdown. */ additionalItemsSelected: { type: [Number, String] as PropType<number | string>, default: 0 }, /** * Sets the aria-controls attribute on the textbox. This should correspond to the popover's id that the textbox triggers. */ ariaControls: { type: String, required: true }, /** * Indicates whether the content the textbox triggers is visible or not. Needs to be set accordingly when the textbox is used and changed every time the popover is shown/hidden. */ ariaExpanded: { type: [String, Boolean] as PropType<Booleanish>, required: true, }, /** * If no label element is used then the aria-label is be set should be set. */ ariaLabel: { type: String, default: null }, /** * Corresponds to the label's id that labels the textbox element. */ ariaLabelledby: { type: String, default: null }, /** * Disables any textbox functionality */ disabled: { type: Boolean, default: false }, /** * Selected option's label */ displayValue: { type: String, default: null }, /** * Sets the possibility to type into the textbox */ dynamicFiltering: { type: Boolean, default: false }, /** * Marks the textbox as invalid, rendering an error state */ isInvalid: { type: Boolean, default: false }, /** * Indicates if the dropdown is open so that * the respective event can be emitted */ open: { type: Boolean, default: false }, /** * Sets the dropdown to readonly state */ readonly: { type: Boolean, default: false }, /** * The input value */ value: { type: String, default: '' }, }); const baseTextbox = ref(null); const conditionalClasses = computed(() => ({ [`b-dropdown-small-textbox--${DropdownBaseTextboxState.DISABLED}`]: props.disabled, [`b-dropdown-small-textbox--${DropdownBaseTextboxState.READONLY}`]: props.readonly, [`b-dropdown-small-textbox--${DropdownBaseTextboxState.OPENED}`]: props.open, })); const isDropdownOpen = computed(() => props.open && !props.disabled && !props.readonly); const onInput = (newInput: string) => { emit('input', newInput); }; const onKeyDown = (e: KeyboardEvent) => { emit('keydown', e); }; const openDropdown = (e: Event) => { stopEventPropagation(e); emit('open'); }; const closeDropdown = (e: Event) => { stopEventPropagation(e); emit('close'); }; const clearValue = (e: Event) => { stopEventPropagation(e); emit('clear'); }; const listeners = { [DropdownBaseTextboxEvent.INPUT]: onInput, [DropdownBaseTextboxEvent.OPEN]: openDropdown, [DropdownBaseTextboxEvent.CLOSE]: closeDropdown, [DropdownBaseTextboxEvent.CLEAR]: clearValue, [DropdownBaseTextboxEvent.KEYDOWN]: onKeyDown, }; const focus = () => { baseTextbox.value.focus(); }; defineExpose({ focus, }); </script> <script lang="ts"> /** * For internal use only, textbox component in its small variant used in the dropdown component. */ export default defineComponent({ name: 'dropdown-small-textbox', inheritAttrs: false, }); </script> <style lang="scss" scoped src="./dropdown-small-textbox.scss" />
|
|
1
|
+
<template> <div class="b-dropdown-small-textbox" :class="conditionalClasses"> <dropdown-base-textbox ref="baseTextbox" v-bind="{ ...$attrs, ...$props }" class="b-dropdown-small-textbox__textbox" :size="BentoDropdownSize.SMALL" v-on="listeners" > <template #icon> <div class="b-dropdown-small-textbox__chevron"> <chevron-up-small-icon v-show="isDropdownOpen" svg-title="opened" /> <chevron-down-small-icon v-show="!isDropdownOpen" svg-title="closed" /> </div> </template> </dropdown-base-textbox> </div> </template> <script setup lang="ts"> import { computed, defineComponent, inject, type PropType, ref } from 'vue'; import { DropdownBaseTextboxEvent, DropdownBaseTextboxState, } from '@/components/dropdown/components/dropdown-base-textbox/dropdown-base-textbox.types'; import { BentoDropdownSize } from '@/components/dropdown/dropdown.types'; import { type Booleanish } from '@/types/prop-types'; import DropdownBaseTextbox from '@/components/dropdown/components/dropdown-base-textbox/dropdown-base-textbox.vue'; import ChevronDownSmallIcon from '@adyen/ui-assets-icons-16/vue/chevron-down-small'; import ChevronUpSmallIcon from '@adyen/ui-assets-icons-16/vue/chevron-up-small'; import { stopEventPropagation } from '@/utils/ts/events'; import { DROPDOWN_UNDERLINE_STYLE_INJECTION_KEY } from '../../dropdown.keys'; const emit = defineEmits<{ /** * Emitted on clearing the input. */ (e: 'clear'); /** * Emitted on click, space and enter user events. It will open the dropdown. */ (e: 'close'); /** * Emitted on input event. */ (e: 'input', newInput: string); /** * Emitted on keydown event. */ (e: 'keydown', keydownEvent: KeyboardEvent); /** * Emitted on click, space, enter and esc user events. It will close the dropdown. */ (e: 'open'); }>(); const props = defineProps({ /** * The number of additional items that have been selected in the dropdown. */ additionalItemsSelected: { type: [Number, String] as PropType<number | string>, default: 0 }, /** * Sets the aria-controls attribute on the textbox. This should correspond to the popover's id that the textbox triggers. */ ariaControls: { type: String, required: true }, /** * Indicates whether the content the textbox triggers is visible or not. Needs to be set accordingly when the textbox is used and changed every time the popover is shown/hidden. */ ariaExpanded: { type: [String, Boolean] as PropType<Booleanish>, required: true, }, /** * If no label element is used then the aria-label is be set should be set. */ ariaLabel: { type: String, default: null }, /** * Corresponds to the label's id that labels the textbox element. */ ariaLabelledby: { type: String, default: null }, /** * Disables any textbox functionality */ disabled: { type: Boolean, default: false }, /** * Selected option's label */ displayValue: { type: String, default: null }, /** * Sets the possibility to type into the textbox */ dynamicFiltering: { type: Boolean, default: false }, /** * Marks the textbox as invalid, rendering an error state */ isInvalid: { type: Boolean, default: false }, /** * Indicates if the dropdown is open so that * the respective event can be emitted */ open: { type: Boolean, default: false }, /** * Sets the dropdown to readonly state */ readonly: { type: Boolean, default: false }, /** * The input value */ value: { type: String, default: '' }, }); const baseTextbox = ref(null); // Inject underline style key to change the dropdown layout const isUnderlineStyle = inject(DROPDOWN_UNDERLINE_STYLE_INJECTION_KEY, false); const conditionalClasses = computed(() => ({ [`b-dropdown-small-textbox--${DropdownBaseTextboxState.DISABLED}`]: props.disabled, [`b-dropdown-small-textbox--${DropdownBaseTextboxState.READONLY}`]: props.readonly, [`b-dropdown-small-textbox--${DropdownBaseTextboxState.OPENED}`]: props.open, 'b-dropdown-small-textbox--underline': isUnderlineStyle, })); const isDropdownOpen = computed(() => props.open && !props.disabled && !props.readonly); const onInput = (newInput: string) => { emit('input', newInput); }; const onKeyDown = (e: KeyboardEvent) => { emit('keydown', e); }; const openDropdown = (e: Event) => { stopEventPropagation(e); emit('open'); }; const closeDropdown = (e: Event) => { stopEventPropagation(e); emit('close'); }; const clearValue = (e: Event) => { stopEventPropagation(e); emit('clear'); }; const listeners = { [DropdownBaseTextboxEvent.INPUT]: onInput, [DropdownBaseTextboxEvent.OPEN]: openDropdown, [DropdownBaseTextboxEvent.CLOSE]: closeDropdown, [DropdownBaseTextboxEvent.CLEAR]: clearValue, [DropdownBaseTextboxEvent.KEYDOWN]: onKeyDown, }; const focus = () => { baseTextbox.value.focus(); }; defineExpose({ focus, }); </script> <script lang="ts"> /** * For internal use only, textbox component in its small variant used in the dropdown component. */ export default defineComponent({ name: 'dropdown-small-textbox', inheritAttrs: false, }); </script> <style lang="scss" scoped src="./dropdown-small-textbox.scss" />
|