@adyen/bento-mcp 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/dist/assets/components/alert/alert.types.ts +1 -1
- package/dist/assets/components/button/button.types.ts +1 -1
- package/dist/assets/components/button/components/button-actions/button-actions.types.ts +1 -1
- package/dist/assets/components/card/card.types.ts +1 -1
- package/dist/assets/components/checkbox/checkbox.vue +1 -1
- package/dist/assets/components/checkbox/components/checkbox-group/checkbox-group.types.ts +1 -1
- package/dist/assets/components/checkbox/components/checkbox-group/checkbox-group.vue +1 -1
- package/dist/assets/components/code-snippet/code-snippet.types.ts +1 -1
- package/dist/assets/components/country/country.types.ts +1 -1
- package/dist/assets/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.types.ts +1 -1
- package/dist/assets/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.vue +1 -1
- package/dist/assets/components/data-grid/components/data-grid-row/data-grid-row.vue +1 -1
- package/dist/assets/components/data-grid/data-grid.docs.mdx +1 -1
- package/dist/assets/components/data-grid/data-grid.vue +1 -1
- package/dist/assets/components/date-picker/date-picker.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-range-picker/date-range-picker.types.ts +1 -1
- package/dist/assets/components/draggable/components/draggable-handle/draggable-handle.stories.ts +1 -0
- package/dist/assets/components/draggable/components/draggable-handle/draggable-handle.types.ts +1 -0
- package/dist/assets/components/draggable/components/draggable-handle/draggable-handle.vue +1 -0
- package/dist/assets/components/draggable/draggable.docs.mdx +178 -0
- package/dist/assets/components/draggable/draggable.stories.ts +1 -0
- package/dist/assets/components/draggable/draggable.types.ts +1 -0
- package/dist/assets/components/draggable/draggable.vue +1 -0
- package/dist/assets/components/dropdown/components/variants/dropdown-avatar/dropdown-avatar.vue +1 -1
- package/dist/assets/components/dropdown/components/variants/dropdown-country/dropdown-country.vue +1 -1
- package/dist/assets/components/dropdown/components/variants/dropdown-currency/dropdown-currency.vue +1 -1
- package/dist/assets/components/dropdown/components/variants/dropdown-payment-method/dropdown-payment-method.vue +1 -1
- package/dist/assets/components/dropdown/components/variants/dropdown-tag/dropdown-tag.vue +1 -1
- package/dist/assets/components/dropdown/composables/use-keyboard-navigation.types.ts +1 -1
- package/dist/assets/components/dropdown/dropdown.stories.ts +1 -1
- package/dist/assets/components/dropdown/dropdown.types.ts +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/boolean-filter/boolean-filter.vue +1 -1
- package/dist/assets/components/filter-bar/components/date-filter/date-filter.vue +1 -1
- package/dist/assets/components/filter-bar/components/input-filter/input-filter.types.ts +1 -1
- package/dist/assets/components/filter-bar/filter-bar.docs.mdx +1 -0
- package/dist/assets/components/filter-bar/filter-bar.stories.ts +1 -1
- package/dist/assets/components/filter-bar/filter-bar.types.ts +1 -1
- package/dist/assets/components/filter-bar/filter-bar.vue +1 -1
- package/dist/assets/components/form-layout/components/form-layout-group/form-layout-group.vue +1 -1
- package/dist/assets/components/form-layout/form-layout.docs.mdx +33 -20
- package/dist/assets/components/form-layout/form-layout.stories.ts +1 -1
- package/dist/assets/components/header/header.types.ts +1 -1
- package/dist/assets/components/input-field/input-field.types.ts +1 -1
- package/dist/assets/components/input-field/input-field.vue +1 -1
- package/dist/assets/components/input-field-password/input-field-password.types.ts +1 -1
- package/dist/assets/components/input-field-password/input-field-password.vue +1 -1
- package/dist/assets/components/input-field-phone-number/input-field-phone-number.types.ts +1 -1
- package/dist/assets/components/input-field-phone-number/input-field-phone-number.vue +1 -1
- package/dist/assets/components/input-time/input-time.vue +1 -1
- package/dist/assets/components/internal/calendar/calendar.types.ts +1 -1
- package/dist/assets/components/internal/calendar/composables/use-range-pane.types.ts +1 -0
- package/dist/assets/components/internal/listbox/components/listbox-multi-select/listbox-multi-select.vue +1 -1
- package/dist/assets/components/internal/listbox/components/listbox-single-select/listbox-single-select.vue +1 -1
- package/dist/assets/components/internal/listbox/listbox.vue +1 -1
- package/dist/assets/components/internal/teleport/teleport.vue +1 -1
- package/dist/assets/components/link/link.types.ts +1 -1
- package/dist/assets/components/menu/menu.types.ts +1 -1
- package/dist/assets/components/menu/menu.vue +1 -1
- package/dist/assets/components/modal/modal.types.ts +1 -1
- package/dist/assets/components/modal/modal.vue +1 -1
- package/dist/assets/components/pagination/pagination.types.ts +1 -1
- package/dist/assets/components/payment-method/payment-method.types.ts +1 -1
- package/dist/assets/components/popover/popover.types.ts +1 -1
- package/dist/assets/components/popper-container/popper-container.types.ts +1 -1
- package/dist/assets/components/promo-banner/promo-banner.types.ts +1 -1
- package/dist/assets/components/radio-group/radio-group.types.ts +1 -1
- package/dist/assets/components/segmented-control/segmented-control.types.ts +1 -1
- package/dist/assets/components/status/status.types.ts +1 -1
- package/dist/assets/components/summary-grid/components/summary-grid-item/summary-grid-item.types.ts +1 -1
- package/dist/assets/components/summary-grid/summary-grid.types.ts +1 -1
- package/dist/assets/components/tag/tag.types.ts +1 -1
- package/dist/assets/components/textarea/textarea.types.ts +1 -1
- package/dist/assets/components/textarea/textarea.vue +1 -1
- package/dist/assets/components/timeline/timeline.types.ts +1 -1
- package/dist/assets/components/toggle/toggle.types.ts +1 -1
- package/dist/assets/components/toggle/toggle.vue +1 -1
- package/dist/assets/components/typography/typography.types.ts +1 -1
- package/dist/assets/components.json +101 -100
- package/dist/assets/composables/use-bento-theme/install.types.ts +1 -1
- package/dist/assets/composables/use-form-validate/use-form.types.ts +1 -1
- package/dist/assets/deprecations.md +196 -196
- package/dist/assets/icons.json +18 -0
- package/dist/assets/index.ts +1 -1
- package/dist/assets/tailwind/README.md +45 -5
- package/dist/assets/usage.json +103 -0
- package/dist/main.js +53 -3
- package/package.json +2 -3
- package/dist/assets/components/draggable-handle/draggable-handle.stories.ts +0 -1
- package/dist/assets/components/draggable-handle/draggable-handle.types.ts +0 -1
- package/dist/assets/components/draggable-handle/draggable-handle.vue +0 -1
- /package/dist/assets/components/{draggable-handle → draggable/components/draggable-handle}/draggable-handle.docs.mdx +0 -0
package/dist/assets/components/form-layout/components/form-layout-group/form-layout-group.vue
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div class="b-form-layout-group" :class="conditionalClasses"> <slot></slot> </div> </template> <script setup lang="ts"> import { computed, type PropType } from 'vue'; import { BentoLayoutGroupProportions } from './form-layout-group.types'; // Default proportion const DEFAULT_PROPORTION = 100; const props = defineProps({ /** * The layout of the pair group of components. * This will render the components within the width of the proportions. * Only two
|
|
1
|
+
<template> <div class="b-form-layout-group" :class="conditionalClasses"> <slot></slot> <error-message v-if="!!errorMessage" :error-message="errorMessage" class="b-form-layout-group__error-message" /> </div> </template> <script setup lang="ts"> import { computed, type PropType } from 'vue'; import { ErrorMessage } from '@/internal'; import { BentoLayoutGroupProportions } from './form-layout-group.types'; // Default proportion const DEFAULT_PROPORTION = 100; const props = defineProps({ /** * The layout of the pair group of components. * This will render the components within the width of the proportions. * Only two columns are available. * * If only one value is entered, then it will assume there's only one child in the group. * If two values are entered, the first value will be use for the first child and the second * value for the second child. * * If the value entered is not listed in the example * list below it will use 100% by default. * @example * '33-33-33' * '50-50' * '25-75' * '75-25' * '33-66' * '66-33' * '25' * '33' * '50' * '100' */ layout: { type: String as PropType<BentoLayoutGroupProportions>, default: '100', validator: (value: BentoLayoutGroupProportions) => BentoLayoutGroupProportions.includes(value), }, /** * The error message to be displayed for the whole group. * * If set, individual error messages for all components within this group will be hidden. * This is particularly useful for field pairs where a single group error is displayed. */ errorMessage: { type: String, default: null }, }); // Sets DEFAULT_PROPORTION by default if `layout` is not in the list const computedLayout = computed(() => BentoLayoutGroupProportions.includes(props.layout) ? props.layout : DEFAULT_PROPORTION ); const conditionalClasses = computed(() => ({ // Transform into column if the layout is unsupported 'b-form-layout-group--column': !BentoLayoutGroupProportions.includes(props.layout), // Assings the correct layout class [`b-form-layout-group--${computedLayout.value}`]: true, 'b-form-layout-group--group-error-messages': !!props.errorMessage, })); </script> <script lang="ts"> /** * Use to provide horizontal layouts to build forms * with two fields per line. * * These can be used for field combinations, * such as post code and city or title and first name. * * @example * import { BentoFormLayoutGroup } from '@adyen/bento-vue2'; * * export default { * components: { BentoFormLayoutGroup }, * template: ` * <bento-form-layout-group> * <first-element /> * <second-element /> * </bento-form-layout-group> * ` * } */ export default {}; </script> <style lang="scss" scoped src="./form-layout-group.scss" />
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Canvas, Meta, Source } from '@storybook/blocks';
|
|
2
2
|
import * as FormLayoutStories from './form-layout.stories';
|
|
3
|
+
import BentoFormLayoutValidateExample from './__tests__/form-layout-validate-example.vue?raw';
|
|
3
4
|
|
|
4
5
|
<Meta of={FormLayoutStories} />
|
|
5
6
|
|
|
@@ -24,6 +25,7 @@ page-specific guidance, such as multi-page forms and how to place a form in a pa
|
|
|
24
25
|
- [Type](#type)
|
|
25
26
|
- [Group](#group)
|
|
26
27
|
- [Dynamic default slot content](#dynamic-default-slot-content)
|
|
28
|
+
- [Group-level validation](#group-level-validation)
|
|
27
29
|
- [Form actions](#form-actions)
|
|
28
30
|
- [Loading](#loading)
|
|
29
31
|
- [Validation](#validation)
|
|
@@ -186,6 +188,17 @@ slot. If this functionality is needed:
|
|
|
186
188
|
`}
|
|
187
189
|
/>
|
|
188
190
|
|
|
191
|
+
#### Group-level validation
|
|
192
|
+
|
|
193
|
+
To highlight inputs with errors, set the `error-message` prop on each input component. To display the error message at
|
|
194
|
+
the group level, set the `error-message` prop on the `bento-form-layout-group` component. When a group-level error
|
|
195
|
+
message is present, individual field error messages are visually hidden but remain accessible to screen readers via
|
|
196
|
+
`aria-describedby`. For optimal accessibility, ensure all fields within the group use the same error message text.
|
|
197
|
+
|
|
198
|
+
See an example of group-level validation below:
|
|
199
|
+
|
|
200
|
+
<Source dark language="html" code={BentoFormLayoutValidateExample} />
|
|
201
|
+
|
|
189
202
|
### Form actions
|
|
190
203
|
|
|
191
204
|
The form provides two slots for actions:
|
|
@@ -241,8 +254,8 @@ import { z } from 'zod/v4'
|
|
|
241
254
|
import { useForm, toTypedSchema } from '@adyen/bento-vue2/use-form-v4';
|
|
242
255
|
const { values, errors, submitForm, handleReset } = useForm({
|
|
243
256
|
validationSchema: toTypedSchema(
|
|
244
|
-
z.object({
|
|
245
|
-
// Schema here
|
|
257
|
+
z.object({
|
|
258
|
+
// Schema here
|
|
246
259
|
})
|
|
247
260
|
)
|
|
248
261
|
});
|
|
@@ -281,7 +294,7 @@ import { z } from 'zod/v4'
|
|
|
281
294
|
import { useForm, toTypedSchema } from '@adyen/bento-vue2/use-form-v4';
|
|
282
295
|
const { values, errors, submitForm, handleReset } = useForm({
|
|
283
296
|
validationSchema: toTypedSchema(
|
|
284
|
-
z.object({
|
|
297
|
+
z.object({
|
|
285
298
|
name: z.string(),
|
|
286
299
|
lastName: z.string()
|
|
287
300
|
}).refine(data => data.name === data.lastName, {
|
|
@@ -306,7 +319,7 @@ import { useForm, toTypedSchema } from '@adyen/bento-vue2';
|
|
|
306
319
|
const externalValue = computed(() => { ... });
|
|
307
320
|
const { values, errors, submitForm, handleReset } = useForm({
|
|
308
321
|
validationSchema: toTypedSchema(
|
|
309
|
-
z.object({
|
|
322
|
+
z.object({
|
|
310
323
|
percentage: z
|
|
311
324
|
.string({
|
|
312
325
|
errorMap: (data, ctx) => {
|
|
@@ -341,7 +354,7 @@ import { useForm, toTypedSchema } from '@adyen/bento-vue2/use-form-v4';
|
|
|
341
354
|
const externalValue = computed(() => { ... });
|
|
342
355
|
const { values, errors, submitForm, handleReset } = useForm({
|
|
343
356
|
validationSchema: toTypedSchema(
|
|
344
|
-
z.object({
|
|
357
|
+
z.object({
|
|
345
358
|
percentage: z
|
|
346
359
|
.string({
|
|
347
360
|
error: issue => 'Default error';
|
|
@@ -350,7 +363,7 @@ const { values, errors, submitForm, handleReset } = useForm({
|
|
|
350
363
|
error: issue => 'Value must be positive'
|
|
351
364
|
})
|
|
352
365
|
.lte(100, {
|
|
353
|
-
error: issue => \`Field needs to be greater than value \`\${externalValue.value\}\`
|
|
366
|
+
error: issue => \`Field needs to be greater than value \`\${externalValue.value\}\`
|
|
354
367
|
}),
|
|
355
368
|
})
|
|
356
369
|
)
|
|
@@ -370,7 +383,7 @@ Vue2
|
|
|
370
383
|
<Source
|
|
371
384
|
dark
|
|
372
385
|
language="typescript"
|
|
373
|
-
code={`
|
|
386
|
+
code={`
|
|
374
387
|
import { useForm, toTypedSchema } from '@adyen/bento-vue2/use-form-v4';
|
|
375
388
|
import { z } from 'zod/v4'
|
|
376
389
|
const { values, errors, submitForm } = useForm({
|
|
@@ -378,12 +391,12 @@ const { values, errors, submitForm } = useForm({
|
|
|
378
391
|
z.object({
|
|
379
392
|
name: z
|
|
380
393
|
.string({
|
|
381
|
-
error: issue =>
|
|
382
|
-
issue.input === undefined
|
|
383
|
-
? 'This field is required'
|
|
394
|
+
error: issue =>
|
|
395
|
+
issue.input === undefined
|
|
396
|
+
? 'This field is required'
|
|
384
397
|
: 'Not a string',
|
|
385
398
|
})
|
|
386
|
-
.min(3, {
|
|
399
|
+
.min(3, {
|
|
387
400
|
error: issue => {
|
|
388
401
|
if (issue.code === 'too_small') {
|
|
389
402
|
return \`Value must be >\${issue.minimum}\`
|
|
@@ -391,7 +404,7 @@ const { values, errors, submitForm } = useForm({
|
|
|
391
404
|
}
|
|
392
405
|
})
|
|
393
406
|
.startsWith('Jane')
|
|
394
|
-
.max(4, {
|
|
407
|
+
.max(4, {
|
|
395
408
|
error: issue => {
|
|
396
409
|
if (issue.code === 'too_big') {
|
|
397
410
|
return \`Value must be >\${issue.maximum}\`
|
|
@@ -401,9 +414,9 @@ const { values, errors, submitForm } = useForm({
|
|
|
401
414
|
.default('Joe'),
|
|
402
415
|
})
|
|
403
416
|
)
|
|
404
|
-
});
|
|
405
|
-
const handleSubmit = submitForm(() => {
|
|
406
|
-
// Your code here
|
|
417
|
+
});
|
|
418
|
+
const handleSubmit = submitForm(() => {
|
|
419
|
+
// Your code here
|
|
407
420
|
});
|
|
408
421
|
`}
|
|
409
422
|
/>
|
|
@@ -421,12 +434,12 @@ const { values, errors, submitForm } = useForm({
|
|
|
421
434
|
z.object({
|
|
422
435
|
name: z
|
|
423
436
|
.string({
|
|
424
|
-
error: issue =>
|
|
425
|
-
issue.input === undefined
|
|
426
|
-
? 'This field is required'
|
|
437
|
+
error: issue =>
|
|
438
|
+
issue.input === undefined
|
|
439
|
+
? 'This field is required'
|
|
427
440
|
: 'Not a string',
|
|
428
441
|
})
|
|
429
|
-
.min(3, {
|
|
442
|
+
.min(3, {
|
|
430
443
|
error: issue => {
|
|
431
444
|
if (issue.code === 'too_small') {
|
|
432
445
|
return \`Value must be >\${issue.minimum}\`
|
|
@@ -434,7 +447,7 @@ const { values, errors, submitForm } = useForm({
|
|
|
434
447
|
}
|
|
435
448
|
})
|
|
436
449
|
.startsWith('Jane')
|
|
437
|
-
.max(4, {
|
|
450
|
+
.max(4, {
|
|
438
451
|
error: issue => {
|
|
439
452
|
if (issue.code === 'too_big') {
|
|
440
453
|
return \`Value must be >\${issue.maximum}\`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import { computed, ref } from 'vue'; import { isVue2 } from 'vue-demi'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import type { Meta, StoryObj } from '@storybook/vue'; import { BentoButton, BentoLoadingButton, BentoLoadingButtonState } from '@/components/button'; import { BentoInputField } from '@/components/input-field'; import { BentoDropdown } from '@/components/dropdown'; import { BentoDatePicker } from '@/components/date-picker'; import { BentoTextarea } from '@/components/textarea'; import { BentoCheckboxGroup } from '@/components/checkbox'; import { type BentoCheckboxGroupItems } from '@/components/checkbox/components/checkbox-group/checkbox-group.types'; import { type BentoListboxOptions } from '@/types/listbox'; import { BentoRadioGroup } from '@/components/radio-group'; import { BentoSelectionCardGroup } from '@/components/selection-card/components/selection-card-group'; import { BentoFileUploader } from '@/components/file-uploader'; import { type BentoRadioGroupItems } from '@/components/radio-group/radio-group.types'; import { BentoFormLayoutFieldTip, BentoFormLayoutGroup, BentoFormLayoutTitle } from './components'; import { toTypedSchema, useForm } from '@/composables/use-form-validate/use-form'; import { BentoAlert } from '@/components/alert'; import { BentoLink } from '@/components/link'; import BentoFormLayout from './form-layout.vue'; import ChevronLeftIcon from '@adyen/ui-assets-icons-16/vue/chevron-left'; import SaveIcon from '@adyen/ui-assets-icons-16/vue/save'; import { BentoFormLayoutFieldTipTypeOptions } from './components/form-layout-field-tip/form-layout-tip.types'; import * as z from 'zod'; import BentoFormLayoutFieldTipExample from './__tests__/form-layout-field-tip-example.vue?raw'; import BentoFormLayoutDefaultExample from './__tests__/form-layout-default-example.vue?raw'; export default { title: 'Form layout', component: BentoFormLayout, argTypes: { default: { name: 'default', defaultValue: '', description: 'Can include various common form components: `bento-input-field`, `bento-dropdown`, `bento-date-picker`, `bento-checkbox-group`, etc.', table: { type: { summary: 'VNode[]' }, }, control: { disable: true, }, }, 'error-message': { name: 'error-message', defaultValue: '', description: 'Can include any HTMLElement', table: { type: { summary: 'VNode[]' }, }, control: { type: 'text', }, }, }, } as Meta<typeof BentoFormLayout>; type Story = StoryObj<typeof BentoFormLayout>; const DROPDOWN_ITEMS: BentoListboxOptions = [ { label: 'Cat', value: 'cat' }, { label: 'Dog', value: 'dog' }, ]; const CHECKBOX_GROUP_ITEMS: BentoCheckboxGroupItems = [ { value: 1, label: 'Label' }, { value: 2, label: 'Label' }, { value: 3, label: 'Label' }, ]; const RADIO_GROUP_ITEMS: BentoRadioGroupItems = [ { value: 1, label: 'Label' }, { value: 2, label: 'Label' }, { value: 3, label: 'Label' }, ]; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdown, BentoCheckboxGroup, BentoButton, BentoLoadingButton, BentoDatePicker, BentoInputField, BentoFileUploader, BentoFormLayout, BentoFormLayoutFieldTip, BentoFormLayoutGroup, BentoFormLayoutTitle, BentoRadioGroup, BentoTextarea, BentoSelectionCardGroup, }, props: Object.keys(argTypes), template: ` <bento-form-layout v-bind="args"> <template #error-message>{{ errorMessage }}</template> <bento-form-layout-title el="h1"> Section title <template #description> Optional section description </template> </bento-form-layout-title> <bento-input-field>Input field</bento-input-field> <bento-dropdown label="Dropdown" :items="dropdownItems" placeholder="Select an animal" /> <bento-date-picker label="Date picker" /> <bento-textarea label="Textarea" :character-limit="400" /> <bento-checkbox-group label="Checkbox group" :items="checkboxGroupItems" :hide-label="false" /> <bento-radio-group label="Radio group" :items="radioGroupItems" :hide-label="false" /> <bento-selection-card-group label="Selection card group" :items="selectionCardGroupItems" :hide-label="false" variant="radio" /> <bento-file-uploader label="File upload" description="Custom suppporting text" accept=".png,.jpeg,.pdf" :max-size="5242880" /> <bento-form-layout-field-tip> <bento-dropdown label="Dropdown" :items="dropdownItems" placeholder="Select an animal" /> <template #message>This is a small tip that can be used in small spaces</template> </bento-form-layout-field-tip> <bento-form-layout-title el="h2" subsection> Subsection title </bento-form-layout-title> <bento-form-layout-group> <bento-input-field>Input field (100%)</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="33-33-33"> <bento-input-field>Input field (33%)</bento-input-field> <bento-input-field>Input field (33%)</bento-input-field> <bento-input-field>Input field (33%)</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="50-50"> <bento-input-field>Input field (50%)</bento-input-field> <bento-input-field>Input field (50%)</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="25-75"> <bento-input-field>Input field (25%)</bento-input-field> <bento-input-field>Input field (75%)</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="75-25"> <bento-input-field>Input field (75%)</bento-input-field> <bento-input-field>Input field (25%)</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="33-66"> <bento-input-field>Input field (33%)</bento-input-field> <bento-input-field>Input field (66%)</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="66-33"> <bento-input-field>Input field (66%)</bento-input-field> <bento-input-field>Input field (33%)</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="25"> <bento-input-field>Input field (25%)</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="33"> <bento-input-field>Input field (33%)</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="50"> <bento-input-field>Input field (50%)</bento-input-field> </bento-form-layout-group> <template #actions-left> <bento-button variant="secondary"> Action </bento-button> <bento-button variant="secondary"> Action </bento-button> </template> <template #actions-right> <bento-button variant="secondary"> Action </bento-button> <bento-loading-button> Action </bento-loading-button> </template> </bento-form-layout> `, setup(props) { return { args: isVue2 ? props : _args, errorMessage: 'There is an error. Correct the highlighted field.', dropdownItems: [...DROPDOWN_ITEMS], checkboxGroupItems: [...CHECKBOX_GROUP_ITEMS], radioGroupItems: [...RADIO_GROUP_ITEMS], selectionCardGroupItems: [ { title: 'Card title', value: '1', description: 'Description', }, { title: 'Card title', value: '2', description: 'Description', }, { title: 'Card title', value: '3', description: 'Description', }, { title: 'Card title', value: '4', description: 'Description', }, ], }; }, }), parameters: storybookDocsParameter(BentoFormLayoutDefaultExample), }; export const WithTip: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdown, BentoFormLayout, BentoFormLayoutFieldTip, }, props: Object.keys(argTypes), template: ` <bento-form-layout v-bind="args"> <bento-form-layout-field-tip :type="args.type" :id="args.id ? args.id : undefined"> <bento-dropdown label="Dropdown" :items="dropdownItems" placeholder="Select an animal" :aria-describedby="args.id" /> <template #message>This is a small tip that can be used in small spaces</template> </bento-form-layout-field-tip> </bento-form-layout> `, setup(props) { return { args: isVue2 ? props : _args, dropdownItems: [...DROPDOWN_ITEMS], }; }, }), argTypes: { type: { options: BentoFormLayoutFieldTipTypeOptions, description: 'Type of a alert; set a distinctive icon and color. Defaults to highlight.', control: { type: 'select', }, table: { category: 'Form Layout Field Tip props', type: { summary: 'string' } }, }, id: { description: 'Unique identifier to the form field. Used for accessibility.', table: { category: 'Form Layout Field Tip props', type: { summary: 'string' } }, }, }, args: { id: 'dropdown_field_with_tip', type: 'highlight' }, parameters: storybookDocsParameter(BentoFormLayoutFieldTipExample), }; const groupCode = ` <bento-form-layout> <bento-form-layout-group> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="50-50"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="25-75"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="75-25"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="33-66"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="66-33"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="25"> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="33"> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="50"> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="33-33-33"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> </bento-form-layout> `; export const Group: Story = { render: (_args, { argTypes }) => ({ components: { BentoAlert, BentoInputField, BentoFormLayout, BentoFormLayoutGroup, }, props: Object.keys(argTypes), template: ` <div> <bento-alert style="margin-bottom: 20px"> <template #description> The <code>bento-form-layout-group</code> <i>is not</i> a dynamic component and does not support changing it's content/default slot. If this functionality is needed, set the <code>v-if</code> statement <b>on</b> the component itself or around/outside it. </template> </bento-alert> <bento-form-layout v-bind="args"> <bento-form-layout-group> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="50"> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="50-50"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="25"> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="25-75"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="75-25"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="33"> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="33-66"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="66-33"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="33-33-33"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> </bento-form-layout> </div> `, setup(props) { return { args: isVue2 ? props : _args, dropdownItems: [...DROPDOWN_ITEMS], }; }, }), parameters: storybookDocsParameter(groupCode), }; const loadingFormCode = ` <bento-form-layout loading> <bento-form-layout-title el="h1"> Form request loading time <template #description> Form fields state while waiting for request to be processed </template> </bento-form-layout-title> <bento-input-field required model-value="Input field">Input field</bento-input-field> <template #actions-right> <bento-loading-button state="loading">Loading</bento-loading-button> </template> </bento-form-layout> `; export const LoadingForm: Story = { render: (_args, { argTypes }) => ({ components: { BentoButton, BentoCheckboxGroup, BentoDatePicker, BentoDropdown, BentoFormLayout, BentoFormLayoutGroup, BentoFormLayoutTitle, BentoInputField, BentoLoadingButton, BentoRadioGroup, BentoTextarea, ChevronLeftIcon, SaveIcon, }, props: Object.keys(argTypes), template: ` <bento-form-layout loading> <bento-form-layout-title el="h1"> Form request loading time <template #description> Form fields state while waiting for request to be processed </template> </bento-form-layout-title> <bento-date-picker label="Date picker" optional /> <bento-textarea label="Textarea" :characterLimit="400" model-value="Text area value..." required /> <bento-form-layout-group layout="25-75"> <bento-input-field model-value="Input value" required>Input field</bento-input-field> <bento-dropdown label="Dropdown" :items="[{ label: 'Dropdown value', value: 1 }]" :model-value="1" optional /> </bento-form-layout-group> <bento-radio-group label="Radio group" modelValue="no" optional :items="[ { label: 'No', value: 'no' }, { label: 'Yes', value: 'yes' }, ]" /> <bento-checkbox-group label="Checkbox group" optional :modelValue="['one', 'two']" :items="[ { label: 'One', value: 'one' }, { label: 'Two', value: 'two' }, ]" /> <template v-slot:actions-left> <bento-button variant="secondary"> Back <template v-slot:iconLeft> <chevron-left-icon /> </template> </bento-button> <bento-button variant="secondary"> Save for later <template v-slot:iconLeft> <save-icon /> </template> </bento-button> </template> <template v-slot:actions-right> <bento-loading-button state="loading">Validating</bento-loading-button> </template> </bento-form-layout> `, setup(props) { return { args: isVue2 ? props : _args, }; }, }), parameters: storybookDocsParameter(loadingFormCode), }; const validateCode = ` <template> <bento-form-layout :loading="isLoading"> <template #error-message v-if="errors"> There is an error. Correct the highlighted field. <ul v-if="errorArray.length"> <li v-for="(error, index) in errorArray" :key="index"> <bento-link to="#input-field-name" is-not-routing>{{ error }}</bento-link> </li> </ul> </template> <bento-form-layout-title el="h1"> Form validation <template #description> Form validation using useForm </template> </bento-form-layout-title> <bento-input-field id="input-field-name" :model-value="values.name" :error-message="errors?.name" @update:model-value="name => values.name = name" > Name </bento-input-field> <template #actions-right> <bento-loading-button :state="loadingButtonState" @click="handleSubmit"> Submit form </bento-loading-button> </template> </bento-form-layout> </template> <script setup lang="ts"> import { ref } from 'vue' import { // Components BentoFormLayout, BentoInputField, BentoLoadingButton, // Composables useForm, toTypedSchema, } from '@adyen/bento-vue2' import { z } from 'zod' const isLoading = ref(false); const loadingButtonState = ref<\`${BentoLoadingButtonState}\`>('start'); const { values, errors, submitForm } = useForm({ validationSchema: toTypedSchema( z.object({ name: z .string() .min(3, { message: 'Name is required' }) .startsWith('Jane') .max(4, { message: 'Name cannot be longer than 4 characters' }) .default('Joe'), }) ) }); const errorArray = computed(() => Object.values(errors.value)); const handleSubmit = submitForm(() => { isLoading.value = true; loadingButtonState.value = 'loading'; // Fake request time setTimeout(() => { isLoading.value = false; loadingButtonState.value = 'success'; }, 2000); }); </script> `; export const Validate: Story = { render: (_args, { argTypes }) => ({ components: { BentoInputField, BentoFormLayout, BentoFormLayoutGroup, BentoFormLayoutTitle, BentoLoadingButton, BentoLink, }, props: Object.keys(argTypes), template: ` <bento-form-layout v-bind="args" :loading="isLoading"> <template #error-message v-if="errors"> There is an error. Correct the highlighted field. <ul v-if="errorArray.length"> <li v-for="(error, index) in errorArray" :key="index"> <bento-link to="#input-field-name" is-not-routing>{{ error }}</bento-link> </li> </ul> </template> <bento-form-layout-title el="h1"> Form validation <template #description> Form validation using useForm </template> </bento-form-layout-title> <bento-input-field id="input-field-name" :model-value="values.name" :error-message="errors?.name" @update:model-value="name => values.name = name" > Name </bento-input-field> <template #actions-right> <bento-loading-button :state="loadingButtonState" @click="handleSubmit"> Submit form </bento-loading-button> </template> </bento-form-layout> `, setup(props) { const isLoading = ref(false); const loadingButtonState = ref<`${BentoLoadingButtonState}`>('start'); const validationSchema = toTypedSchema( z.object({ name: z .string() .min(3, { message: 'Name is required' }) .startsWith('Jane') .max(4, { message: 'Name cannot be longer than 4 characters' }) .default('Joe'), }) ); const { values, errors, submitForm } = useForm({ validationSchema, }); const handleSubmit = submitForm(() => { isLoading.value = true; loadingButtonState.value = 'loading'; // Fake request time setTimeout(() => { isLoading.value = false; loadingButtonState.value = 'success'; }, 2000); }); const errorArray = computed(() => Object.values(errors.value)); return { args: isVue2 ? props : _args, values, errors, errorArray, loadingButtonState, isLoading, // Methods handleSubmit, }; }, }), parameters: storybookDocsParameter(validateCode), };
|
|
1
|
+
import { computed, ref } from 'vue'; import { isVue2 } from 'vue-demi'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import type { Meta, StoryObj } from '@storybook/vue'; import { BentoButton, BentoLoadingButton, type BentoLoadingButtonState } from '@/components/button'; import { BentoInputField } from '@/components/input-field'; import { BentoDropdown } from '@/components/dropdown'; import { BentoDatePicker } from '@/components/date-picker'; import { BentoTextarea } from '@/components/textarea'; import { BentoCheckboxGroup } from '@/components/checkbox'; import { type BentoCheckboxGroupItems } from '@/components/checkbox/components/checkbox-group/checkbox-group.types'; import { type BentoListboxOptions } from '@/types/listbox'; import { BentoRadioGroup } from '@/components/radio-group'; import { BentoSelectionCardGroup } from '@/components/selection-card/components/selection-card-group'; import { BentoFileUploader } from '@/components/file-uploader'; import { type BentoRadioGroupItems } from '@/components/radio-group/radio-group.types'; import { BentoFormLayoutFieldTip, BentoFormLayoutGroup, BentoFormLayoutTitle } from './components'; import { toTypedSchema, useForm } from '@/composables/use-form-validate'; import { BentoAlert } from '@/components/alert'; import { BentoLink } from '@/components/link'; import BentoFormLayout from './form-layout.vue'; import ChevronLeftIcon from '@adyen/ui-assets-icons-16/vue/chevron-left'; import SaveIcon from '@adyen/ui-assets-icons-16/vue/save'; import { BentoFormLayoutFieldTipTypeOptions } from './components/form-layout-field-tip/form-layout-tip.types'; import * as z from 'zod'; import BentoFormLayoutFieldTipExample from './__tests__/form-layout-field-tip-example.vue?raw'; import BentoFormLayoutDefaultExample from './__tests__/form-layout-default-example.vue?raw'; import BentoFormLayoutValidateExample from './__tests__/form-layout-validate-example.vue?raw'; export default { title: 'Form layout', component: BentoFormLayout, argTypes: { default: { name: 'default', defaultValue: '', description: 'Can include various common form components: `bento-input-field`, `bento-dropdown`, `bento-date-picker`, `bento-checkbox-group`, etc.', table: { type: { summary: 'VNode[]' }, }, control: { disable: true, }, }, 'error-message': { name: 'error-message', defaultValue: '', description: 'Can include any HTMLElement', table: { type: { summary: 'VNode[]' }, }, control: { type: 'text', }, }, }, } as Meta<typeof BentoFormLayout>; type Story = StoryObj<typeof BentoFormLayout>; const DROPDOWN_ITEMS: BentoListboxOptions = [ { label: 'Cat', value: 'cat' }, { label: 'Dog', value: 'dog' }, ]; const CHECKBOX_GROUP_ITEMS: BentoCheckboxGroupItems = [ { value: 1, label: 'Label' }, { value: 2, label: 'Label' }, { value: 3, label: 'Label' }, ]; const RADIO_GROUP_ITEMS: BentoRadioGroupItems = [ { value: 1, label: 'Label' }, { value: 2, label: 'Label' }, { value: 3, label: 'Label' }, ]; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdown, BentoCheckboxGroup, BentoButton, BentoLoadingButton, BentoDatePicker, BentoInputField, BentoFileUploader, BentoFormLayout, BentoFormLayoutFieldTip, BentoFormLayoutGroup, BentoFormLayoutTitle, BentoRadioGroup, BentoTextarea, BentoSelectionCardGroup, }, props: Object.keys(argTypes), template: ` <bento-form-layout v-bind="args"> <template #error-message>{{ errorMessage }}</template> <bento-form-layout-title el="h1"> Section title <template #description> Optional section description </template> </bento-form-layout-title> <bento-input-field>Input field</bento-input-field> <bento-dropdown label="Dropdown" :items="dropdownItems" placeholder="Select an animal" /> <bento-date-picker label="Date picker" /> <bento-textarea label="Textarea" :character-limit="400" /> <bento-checkbox-group label="Checkbox group" :items="checkboxGroupItems" :hide-label="false" /> <bento-radio-group label="Radio group" :items="radioGroupItems" :hide-label="false" /> <bento-selection-card-group label="Selection card group" :items="selectionCardGroupItems" :hide-label="false" variant="radio" /> <bento-file-uploader label="File upload" description="Custom suppporting text" accept=".png,.jpeg,.pdf" :max-size="5242880" /> <bento-form-layout-field-tip> <bento-dropdown label="Dropdown" :items="dropdownItems" placeholder="Select an animal" /> <template #message>This is a small tip that can be used in small spaces</template> </bento-form-layout-field-tip> <bento-form-layout-title el="h2" subsection> Subsection title </bento-form-layout-title> <bento-form-layout-group> <bento-input-field>Input field (100%)</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="33-33-33"> <bento-input-field>Input field (33%)</bento-input-field> <bento-input-field>Input field (33%)</bento-input-field> <bento-input-field>Input field (33%)</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="50-50"> <bento-input-field>Input field (50%)</bento-input-field> <bento-input-field>Input field (50%)</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="25-75"> <bento-input-field>Input field (25%)</bento-input-field> <bento-input-field>Input field (75%)</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="75-25"> <bento-input-field>Input field (75%)</bento-input-field> <bento-input-field>Input field (25%)</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="33-66"> <bento-input-field>Input field (33%)</bento-input-field> <bento-input-field>Input field (66%)</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="66-33"> <bento-input-field>Input field (66%)</bento-input-field> <bento-input-field>Input field (33%)</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="25"> <bento-input-field>Input field (25%)</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="33"> <bento-input-field>Input field (33%)</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="50"> <bento-input-field>Input field (50%)</bento-input-field> </bento-form-layout-group> <template #actions-left> <bento-button variant="secondary"> Action </bento-button> <bento-button variant="secondary"> Action </bento-button> </template> <template #actions-right> <bento-button variant="secondary"> Action </bento-button> <bento-loading-button> Action </bento-loading-button> </template> </bento-form-layout> `, setup(props) { return { args: isVue2 ? props : _args, errorMessage: 'There is an error. Correct the highlighted field.', dropdownItems: [...DROPDOWN_ITEMS], checkboxGroupItems: [...CHECKBOX_GROUP_ITEMS], radioGroupItems: [...RADIO_GROUP_ITEMS], selectionCardGroupItems: [ { title: 'Card title', value: '1', description: 'Description', }, { title: 'Card title', value: '2', description: 'Description', }, { title: 'Card title', value: '3', description: 'Description', }, { title: 'Card title', value: '4', description: 'Description', }, ], }; }, }), parameters: storybookDocsParameter(BentoFormLayoutDefaultExample), }; export const WithTip: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdown, BentoFormLayout, BentoFormLayoutFieldTip, }, props: Object.keys(argTypes), template: ` <bento-form-layout v-bind="args"> <bento-form-layout-field-tip :type="args.type" :id="args.id ? args.id : undefined"> <bento-dropdown label="Dropdown" :items="dropdownItems" placeholder="Select an animal" :aria-describedby="args.id" /> <template #message>This is a small tip that can be used in small spaces</template> </bento-form-layout-field-tip> </bento-form-layout> `, setup(props) { return { args: isVue2 ? props : _args, dropdownItems: [...DROPDOWN_ITEMS], }; }, }), argTypes: { type: { options: BentoFormLayoutFieldTipTypeOptions, description: 'Type of a alert; set a distinctive icon and color. Defaults to highlight.', control: { type: 'select', }, table: { category: 'Form Layout Field Tip props', type: { summary: 'string' } }, }, id: { description: 'Unique identifier to the form field. Used for accessibility.', table: { category: 'Form Layout Field Tip props', type: { summary: 'string' } }, }, }, args: { id: 'dropdown_field_with_tip', type: 'highlight' }, parameters: storybookDocsParameter(BentoFormLayoutFieldTipExample), }; const groupCode = ` <bento-form-layout> <bento-form-layout-group> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="50-50"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="25-75"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="75-25"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="33-66"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="66-33"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="25"> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="33"> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="50"> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="33-33-33"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> </bento-form-layout> `; export const Group: Story = { render: (_args, { argTypes }) => ({ components: { BentoAlert, BentoInputField, BentoFormLayout, BentoFormLayoutGroup, }, props: Object.keys(argTypes), template: ` <div> <bento-alert style="margin-bottom: 20px"> <template #description> The <code>bento-form-layout-group</code> <i>is not</i> a dynamic component and does not support changing it's content/default slot. If this functionality is needed, set the <code>v-if</code> statement <b>on</b> the component itself or around/outside it. </template> </bento-alert> <bento-form-layout v-bind="args"> <bento-form-layout-group> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="50"> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="50-50"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="25"> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="25-75"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="75-25"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="33"> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="33-66"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="66-33"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="33-33-33"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> </bento-form-layout> </div> `, setup(props) { return { args: isVue2 ? props : _args, dropdownItems: [...DROPDOWN_ITEMS], }; }, }), parameters: storybookDocsParameter(groupCode), }; const loadingFormCode = ` <bento-form-layout loading> <bento-form-layout-title el="h1"> Form request loading time <template #description> Form fields state while waiting for request to be processed </template> </bento-form-layout-title> <bento-input-field required model-value="Input field">Input field</bento-input-field> <template #actions-right> <bento-loading-button state="loading">Loading</bento-loading-button> </template> </bento-form-layout> `; export const LoadingForm: Story = { render: (_args, { argTypes }) => ({ components: { BentoButton, BentoCheckboxGroup, BentoDatePicker, BentoDropdown, BentoFormLayout, BentoFormLayoutGroup, BentoFormLayoutTitle, BentoInputField, BentoLoadingButton, BentoRadioGroup, BentoTextarea, ChevronLeftIcon, SaveIcon, }, props: Object.keys(argTypes), template: ` <bento-form-layout loading> <bento-form-layout-title el="h1"> Form request loading time <template #description> Form fields state while waiting for request to be processed </template> </bento-form-layout-title> <bento-date-picker label="Date picker" optional /> <bento-textarea label="Textarea" :characterLimit="400" model-value="Text area value..." required /> <bento-form-layout-group layout="25-75"> <bento-input-field model-value="Input value" required>Input field</bento-input-field> <bento-dropdown label="Dropdown" :items="[{ label: 'Dropdown value', value: 1 }]" :model-value="1" optional /> </bento-form-layout-group> <bento-radio-group label="Radio group" modelValue="no" optional :items="[ { label: 'No', value: 'no' }, { label: 'Yes', value: 'yes' }, ]" /> <bento-checkbox-group label="Checkbox group" optional :modelValue="['one', 'two']" :items="[ { label: 'One', value: 'one' }, { label: 'Two', value: 'two' }, ]" /> <template v-slot:actions-left> <bento-button variant="secondary"> Back <template v-slot:iconLeft> <chevron-left-icon /> </template> </bento-button> <bento-button variant="secondary"> Save for later <template v-slot:iconLeft> <save-icon /> </template> </bento-button> </template> <template v-slot:actions-right> <bento-loading-button state="loading">Validating</bento-loading-button> </template> </bento-form-layout> `, setup(props) { return { args: isVue2 ? props : _args, }; }, }), parameters: storybookDocsParameter(loadingFormCode), }; export const Validate: Story = { render: (_args, { argTypes }) => ({ components: { BentoInputField, BentoFormLayout, BentoFormLayoutGroup, BentoFormLayoutTitle, BentoLoadingButton, BentoLink, }, props: Object.keys(argTypes), template: ` <bento-form-layout v-bind="args" :loading="isLoading"> <template #error-message v-if="errorArray.length"> There is an error. Correct the highlighted field. <ul> <li v-for="(error, index) in errorArray" :key="index"> <bento-link to="#input-field-name" is-not-routing>{{ error }}</bento-link> </li> </ul> </template> <bento-form-layout-title el="h1"> Form validation <template #description> Form validation using useForm </template> </bento-form-layout-title> <bento-input-field id="input-field-name" :model-value="values.name" :error-message="errors.name" @update:model-value="name => { if (values) { values.name = String(name); } }" > Name </bento-input-field> <bento-form-layout-group layout="50-50" :error-message="errors.email || errors.phone"> <bento-input-field id="input-field-email" :model-value="values.email" :error-message="errors.email" @update:model-value="email => { if (values) { values.email = String(email); } }" > Email </bento-input-field> <bento-input-field id="input-field-phone" :model-value="values.phone" :error-message="errors.phone" @update:model-value="phone => { if (values) { values.phone = String(phone); } }" > Phone </bento-input-field> </bento-form-layout-group> <template #actions-right> <bento-loading-button :state="loadingButtonState" @click="handleSubmit"> Submit form </bento-loading-button> </template> </bento-form-layout> `, setup(props) { const isLoading = ref(false); const loadingButtonState = ref<`${BentoLoadingButtonState}`>('start'); const validationSchema = toTypedSchema( z .object({ name: z .string() .min(3, { message: 'Name is required' }) .startsWith('Jane') .max(4, { message: 'Name cannot be longer than 4 characters' }) .default('Joe'), email: z.string().default(''), phone: z.string().default(''), }) .superRefine((data, ctx) => { if (data.email.length === 0 && data.phone.length === 0) { const issue = { code: z.ZodIssueCode.custom, message: 'At least one of Email or Phone is required', }; ctx.addIssue({ ...issue, path: ['email'] }); } }) ); const { values, errors, submitForm } = useForm({ validationSchema, }); const handleSubmit = submitForm(() => { isLoading.value = true; loadingButtonState.value = 'loading'; // Fake request time setTimeout(() => { isLoading.value = false; loadingButtonState.value = 'success'; }, 2000); }); const errorArray = computed(() => [...new Set(Object.values(errors.value))]); return { args: isVue2 ? props : _args, values, errors, errorArray, loadingButtonState, isLoading, // Methods handleSubmit, }; }, }), parameters: storybookDocsParameter(BentoFormLayoutValidateExample), };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import { type BentoLoadingButtonState } from '../button/components/loading-button/loading-button.types'; /** * @deprecated
|
|
1
|
+
import { type BentoLoadingButtonState } from '../button/components/loading-button/loading-button.types'; /** * @deprecated Since v2.0.0. Use string literal instead. */ export enum BentoHeaderVariant { PAGE = 'page', COMPONENT = 'component', } export interface BentoHeaderReloadButton { title: string; event?: (event: Event) => void; state?: `${BentoLoadingButtonState}`; }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import { type BentoDropdownProps } from '../dropdown/dropdown.types'; import { type FormField } from '../../types/form-field'; /** * @deprecated
|
|
1
|
+
import { type BentoDropdownProps } from '../dropdown/dropdown.types'; import { type FormField } from '../../types/form-field'; /** * @deprecated Since v2.0.0. Use string literal instead. */ export enum BentoInputFieldVariant { DEFAULT = 'default', PAYMENT_METHOD = 'payment-method', STATIC_VALUE = 'static-value', DROPDOWN = 'dropdown', } /** * @deprecated Since v2.0.0. Use string literal instead. */ export enum BentoInputFieldElementPosition { START = 'start', END = 'end', } export enum BentoInputFieldStateClass { CONDENSED = 'condensed', DISABLED = 'disabled', READONLY = 'readonly', ERROR = 'error', } /** * @deprecated Since v2.0.0. Use string literal instead. */ export enum BentoInputFieldType { SEARCH = 'search', TEXT = 'text', NUMBER = 'number', } export type BentoInputFieldValue = string | number; type DropdownProps = BentoDropdownProps; export interface BentoInputDropdownProps { /** * Text that labels the dropdown element */ 'aria-label'?: string; /** * Enables dynamic filtering */ dynamicFiltering?: DropdownProps['dynamicFiltering']; /** * List of available items */ items: DropdownProps['items']; /** * Sets the dropdown in read-only mode */ readonly?: DropdownProps['readonly']; /** * Selected value of the dropdown * @deprecated Since v2.0.0. Use `model-value` instead. */ value?: DropdownProps['value']; /** * Selected value of the dropdown */ modelValue?: DropdownProps['modelValue']; } /** * @deprecated Since v2.0.0. Use `@<emit name>` instead. */ export enum BentoInputFieldEvent { INPUT = 'input', ESCAPE_PRESSED = 'escape-pressed', UPDATE_DROPDOWN = 'update:dropdown', DROPDOWN_INPUT = 'dropdown-input', FOCUS = 'focus', CHANGE = 'change', BLUR = 'blur', KEYDOWN = 'keydown', } export interface BentoInputFieldProps extends FormField { /** * Indicates whether the element is exposed to an accessibility API. */ ariaHidden?: boolean; /** * Text that labels an the input element * * @deprecated Since v2.0.0. Use `aria-label` instead. */ ariaLabel?: string; /** * Reduces height of the input */ condensed?: boolean; /** * Dropdown props for the small selectable dropdown menu. * Properties supported: * * - `aria-label`: Text that labels the dropdown element * - `dynamicFiltering`: Enables dynamic filtering * - `items`: List of available items * - `readonly`: Sets the dropdown in read-only mode * - `value`: Selected value of the dropdown */ dropdown?: BentoInputDropdownProps; /** * Position of the dropdown before or after input value */ dropdownPosition?: BentoInputFieldElementPosition | `${BentoInputFieldElementPosition}`; /** * Adds error validation class for outlining input-field. * * @deprecated Since v2.0.0. Use `errorMessage` instead. */ error?: boolean; /** * Text displayed as the value of the input-field */ modelValue?: BentoInputFieldValue; /** * Renders a font numeric style with slashed zeros instead of normal ones. */ slashedZero?: boolean; /** * Static text that displayed before or after input value * Applicable to the Static Value variant */ staticValue?: string; /** * Position of Static Value before or after input value */ staticValuePosition?: BentoInputFieldElementPosition | `${BentoInputFieldElementPosition}`; /** * A string specifying the type of control to render. * All `input` types are available but only `search`, `text` and `number` are fully supported. * * If `type` is `search`, use `bento-search-bar` instead. */ type?: BentoInputFieldType | `${BentoInputFieldType}` | HTMLInputElement['type']; /** * Text displayed as the value of the input-field * @deprecated Since v2.0.0. Use `v-model` or `model-value` instead. */ value?: BentoInputFieldValue; /** * Describes the different variants for the input element */ variant?: BentoInputFieldVariant | `${BentoInputFieldVariant}`; }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <bento-typography class="b-input-field" data-testid="input-text-container" :class="inputFieldConditionalClasses" el="div" variant="body" > <field-label v-if="hasSlot('default') || label" :for="inputFieldId" data-testid="input-field-label" :tooltip-text="tooltipText" :optional="optional" :required="required" :label="label" @click="focusInput" > <slot></slot> </field-label> <div class="b-input-field__input-box" data-testid="input-box" :aria-disabled="disabled" @click="focusInput"> <!-- Optional Icon for Default text variant --> <span v-if="isDefaultVariant && (hasSlot('defaultIconBefore') || hasSlot('iconBefore'))" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__icon-before" > <slot v-if="hasSlot('iconBefore')" name="iconBefore" /> <slot v-if="hasSlot('defaultIconBefore') && !hasSlot('iconBefore')" name="defaultIconBefore" /> </span> <!-- Mandatory Icon for Payment Method variant --> <span v-if="isPaymentMethodVariant && hasSlot('paymentMethod')" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__payment-method" > <slot name="paymentMethod" /> </span> <!-- Dropdown at start --> <div v-if="shouldDisplayDropdownAtStart" :id="dropdownId" class="b-input-field__dropdown b-input-field__dropdown--start" @click.stop > <bento-dropdown v-if="dropdown" ref="inputDropdownElement" v-bind="inputFieldDropdownProps" :disabled="disabled" :readonly="isDropdownReadOnly" @update:model-value="onDropdownInput" /> </div> <!-- Static Value at start --> <bento-typography v-if="shouldDisplayStaticValueAtStart" :id="staticValueId" class="b-input-field__static-value b-input-field__static-value--start" el="span" variant="body" > <slot name="staticValue" /> </bento-typography> <div class="b-input-field__input-container"> <input :id="inputFieldId" v-bind="attrs" ref="inputFieldElement" class="b-input-field__input" :aria-label="computedAriaLabel" :aria-describedby="computedAriaDescribedBy" :aria-owns="computedAriaOwns" :aria-invalid="shouldShowError" :placeholder="placeholder" :required="required" :aria-required="required" :type="type" :value="modelValue ?? value" :disabled="disabled" :readonly="isReadOnly" @input="onInput" @change="emit('change')" @focus="onFocus" @blur="onBlur" @keydown="emit('keydown', $event)" @keyup="emit('keyup', $event)" @keydown.esc="emit('escape-pressed')" @click="emit('click', $event)" /> <!-- Hint at the end --> <bento-typography v-if="hint && isFocused && (modelValue || value)" el="span" class="b-input-field__hint" > {{ hint }} </bento-typography> </div> <span v-if="hasSlot('iconAfter')" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__icon-after" data-testid="input-text__icon-after" > <slot name="iconAfter" /> </span> <!-- Static Value at the end --> <bento-typography v-if="shouldDisplayStaticValueAtEnd" :id="staticValueId" class="b-input-field__static-value b-input-field__static-value--end" el="span" variant="body" > <slot name="staticValue" /> </bento-typography> <!-- Dropdown at end --> <div v-if="shouldDisplayDropdownAtEnd" :id="dropdownId" data-testid="input-field-dropdown-container-end" class="b-input-field__dropdown b-input-field__dropdown--end" @click.stop > <bento-dropdown v-if="dropdown" :id="dropdownId" ref="inputDropdownElement" v-bind="inputFieldDropdownProps" :disabled="disabled" :readonly="isDropdownReadOnly" @update:model-value="onDropdownInput" /> </div> </div> <error-message v-if="shouldShowError && !!errorMessage" :id="errorId" :error-message="errorMessage" class="b-input-field__error-message" /> <span v-if="hasSlot('description') || description" :id="descriptionId" class="b-input-field__description" @click="focusInput" > <bento-typography el="span" variant="body"> <slot id="description" name="description"> {{ description }} </slot> </bento-typography> </span> </bento-typography> </template> <script setup lang="ts"> import { computed, inject, provide, type Ref, ref, toRef, toRefs, useAttrs, useSlots } from 'vue'; // Components import { BentoDropdown } from '@/components/dropdown'; import { BentoTypography } from '@/components/typography'; import { ErrorMessage, FieldLabel } from '@/internal'; // Utils import { deprecate } from '@/utils/ts/deprecate'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; import { generateUid } from '@/core/utils/ts'; import { getSlotText } from '@/utils/ts/get-slot-text'; import { useAriaLabel } from '@/utils/ts/aria-label'; import { useI18n } from '@/utils/ts/i18n'; import { printDevelopmentWarning } from '@/utils/ts/print-development-warning'; import { INPUT_FIELD_COMPONENT_INJECTION_KEY, INPUT_FIELD_HINT_INJECTION_KEY, INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, } from '@/components/input-field/input-field.keys'; import { type BentoListboxSelectedValue } from '@/types/listbox'; // Composables import { useFormLayoutFieldLoading, useHasSlot } from '@/composables'; // Types import { type BentoInputDropdownProps, BentoInputFieldElementPosition, type BentoInputFieldProps, BentoInputFieldStateClass, BentoInputFieldType, type BentoInputFieldValue, BentoInputFieldVariant, } from './input-field.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const attrs = useAttrs(); const slots = useSlots(); const props = withDefaults(defineProps<BentoInputFieldProps>(), { ariaHidden: true, ariaLabel: undefined, condensed: false, description: '', disabled: false, dropdown: undefined, dropdownPosition: BentoInputFieldElementPosition.START, error: false, errorMessage: null, label: '', modelValue: undefined, optional: false, placeholder: '', required: false, readonly: false, slashedZero: false, staticValue: '', staticValuePosition: BentoInputFieldElementPosition.START, tooltipText: null, type: BentoInputFieldType.TEXT, value: undefined, variant: BentoInputFieldVariant.DEFAULT, }); const emit = defineEmits<{ /** * Emitted when the element has lost focus */ (e: 'blur'): void; /** * Emitted when the input is clicked */ (e: 'click', event: MouseEvent); /** * Emitted when the user modifies the element's value. Unlike the 'input' event, the change event is not necessarily fired for each alteration to an element's value */ (e: 'change'): void; /** * Emitted when the Input Field's internal dropdown changes it's value */ (e: 'dropdown-input', selectedValue: BentoListboxSelectedValue): void; /** * Emitted when the "ESC" key is pressed */ (e: 'escape-pressed'): void; /** * Emitted when the element has received focus */ (e: 'focus'): void; /** * Emitted when a key is pressed down */ (e: 'keydown', event: KeyboardEvent): void; /** * Emitted when a key is pressed and lifted up */ (e: 'keyup', event: KeyboardEvent): void; /** * Emitted when the component's model changes * @deprecated since version 2.0. Use `v-model` or `update:model-value` instead. */ (e: 'input', inputValue: string): void; /** * Emitted when the Input Field's internal dropdown changes it's value. Contains every dropdown prop passed with the updated selected value as 'value' */ (e: 'update:dropdown', updatedDropdownProps: BentoInputDropdownProps): void; /** * Emitted when the component's model changes */ (e: 'update:model-value', inputValue: BentoInputFieldValue): void; }>(); const { emitValue } = useFormFieldEmits<BentoInputFieldValue>(emit); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const hasSlot = useHasSlot(slots); const inputFieldElement = ref(null); const inputDropdownElement = ref(null); const inputFieldId = generateUid('input'); const dropdownId = generateUid('input-dropdown'); const staticValueId = generateUid('input-static-value'); const descriptionId = generateUid('input-description'); const errorId = generateUid('input-error'); const isFocused = ref(false); // Provide INPUT_FIELD_COMPONENT_INJECTION_KEY as true, to set the input only dropdown size variant in the BentoDropdown comp provide(INPUT_FIELD_COMPONENT_INJECTION_KEY, true); // Renders the input with a hint if a string is provided const hint = inject<Ref<string | undefined>>(INPUT_FIELD_HINT_INJECTION_KEY, ref(undefined)); const onFocus = () => { isFocused.value = true; emit('focus'); }; const onBlur = () => { isFocused.value = false; emit('blur'); }; const { isReadOnly } = useFormLayoutFieldLoading(toRef(props, 'readonly')); const isDropdownReadOnly = computed<BentoInputDropdownProps['readonly']>( () => isReadOnly.value || props.dropdown?.readonly ); const shouldShowStaticValue = computed( () => props.variant === BentoInputFieldVariant.STATIC_VALUE && !!slots.staticValue ); const isDefaultVariant = computed(() => props.variant === BentoInputFieldVariant.DEFAULT); const isPaymentMethodVariant = computed(() => props.variant === BentoInputFieldVariant.PAYMENT_METHOD); const shouldDisplayStaticValueAtStart = computed( () => shouldShowStaticValue.value && props.staticValuePosition === BentoInputFieldElementPosition.START ); const shouldDisplayStaticValueAtEnd = computed( () => shouldShowStaticValue.value && props.staticValuePosition === BentoInputFieldElementPosition.END ); const shouldShowDropdown = computed(() => props.variant === BentoInputFieldVariant.DROPDOWN && !!props.dropdown); const shouldDisplayDropdownAtStart = computed( () => shouldShowDropdown.value && props.dropdownPosition === BentoInputFieldElementPosition.START ); const shouldDisplayDropdownAtEnd = computed( () => shouldShowDropdown.value && props.dropdownPosition === BentoInputFieldElementPosition.END ); const shouldShowError = computed( () => !props.disabled && !isReadOnly.value && (!!props.errorMessage || props.error) ); const computedLabel = computed(() => (getSlotText(slots)('default') as string) || props.label); const computedDescription = computed(() => (getSlotText(slots)('description') as string) || props.description); const computedAriaOwns = computed(() => { if (shouldShowDropdown.value === true && shouldShowStaticValue.value === true) { return `${staticValueId} ${dropdownId}`; } if (shouldShowDropdown.value === true) { return `${dropdownId}`; } if (shouldShowStaticValue.value === true) { return `${staticValueId}`; } return null; }); // vue-i18n issue with t() used in script - direct usage in template works // requires either createI18n({ legacy: false }) or to be wrapped in computed() const computedAriaLabelFallbackMessage = computed(() => t('ariaLabelFallback')); const computedAriaLabelDropdowFallback = computed(() => t('ariaLabelDropdownFallback', { inputLabel: computedAriaLabel.value }) ); const ariaLabelAttribute = ref(attrs['aria-label']); const { ariaLabel } = toRefs(props); const computedAriaLabel = useAriaLabel({ ariaLabel: ariaLabelAttribute.value as string, // TODO: take this line away when the aria-label props is removed label: ariaLabel, defaultFallback: computedLabel.value || computedAriaLabelFallbackMessage, }); const computedAriaDescribedBy = computed( () => `${computedDescription.value ? descriptionId : ''} ${ shouldShowError.value && !!props.errorMessage ? errorId : '' }` ); const inputFieldConditionalClasses = computed(() => ({ [`b-input-field--${BentoInputFieldStateClass.CONDENSED}`]: props.condensed, [`b-input-field--${BentoInputFieldStateClass.DISABLED}`]: props.disabled, [`b-input-field--${BentoInputFieldStateClass.READONLY}`]: isReadOnly.value, [`b-input-field--${BentoInputFieldStateClass.ERROR}`]: shouldShowError.value, [`b-input-field--slashed-zero`]: props.slashedZero, })); const inputFieldDropdownProps = computed(() => ({ 'aria-label': computedAriaLabelDropdowFallback.value, ...props.dropdown, })); const onInput = (event: Event) => { const inputValue = (event.target as HTMLInputElement).value; if (!props.disabled && !isReadOnly.value) { emitValue(inputValue); } }; const onDropdownInput = (selectedValue: BentoListboxSelectedValue) => { emit('dropdown-input', selectedValue); emit('update:dropdown', { ...props?.dropdown, value: selectedValue }); }; const focusInput = () => { inputFieldElement.value.focus(); }; const focus = () => { if (props.variant === 'dropdown') { inputDropdownElement.value.focus(); } else { focusInput(); } }; // Expose method so that other components can focus on it in Vue3 defineExpose({ focusInput, focus, }); const inputFieldParentComponent = inject(INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, ''); if (!Object.values(BentoInputFieldType).includes(props.type as BentoInputFieldType) && !inputFieldParentComponent) { if (props.type === 'tel') { printDevelopmentWarning( `"bento-input-field" dropdown does not support the type '${props.type}', use "bento-input-field-phone-number" instead.` ); } else if (props.type === 'password') { printDevelopmentWarning( `"bento-input-field" dropdown does not support the type '${props.type}', use "bento-input-field-password" instead.` ); } else { printDevelopmentWarning(`"bento-input-field" dropdown does not support the type '${props.type}'`); } } /** * Deprecations */ if (props.error) { deprecate( 'BentoInputField "error" property', `Use the BentoInputField "errorMessage" property instead to indicate that there is a modifier error. Having the field as "undefined" will hide/remove the error. <bento-dropdown errorMessage="Error message text" />`, '2.0.0' ); } if (hasSlot('defaultIconBefore')) { deprecate( 'BentoInputField "defaultIconBefore" slot', `Use the BentoInputField "iconBefore" slot instead to add an icon at the left side of the input field <bento-input-field> \t<template #iconBefore> \t\t<my-icon-goes-here /> \t</template> </bento-input-field>`, '2.0.0' ); } if (props.value) { deprecate( 'BentoInputField "value" property', `The use of "value" prop in "BentoInputField" is no longer supported. Use the "v-model" or "model-value" property instead.`, '2.0.0' ); } </script> <script lang="ts"> /** * Input field component is used to create interactive controls for web-based * forms in order to accept data from the user. * * @example * import { BentoInputField } from '@adyen/bento-vue2' * * const inputValue = ref(''); * * export default { * components: { BentoInputField }, * tempate: ` * <bento-input-field * :model-value="inputValue" * :@update:model-value="newValue => inputValue.value = newValue" * > * } */ export default { name: 'bento-input-field', model: { prop: 'modelValue' }, i18n: { messages }, }; </script> <style lang="scss" scoped src="./input-field.scss" />
|
|
1
|
+
<template> <bento-typography class="b-input-field" v-bind="getRootAttributes($attrs)" :data-testid="getDataTestId($attrs)" :class="inputFieldConditionalClasses" el="div" variant="body" > <field-label v-if="hasSlot('default') || label" :for="inputFieldId" data-testid="input-field-label" :tooltip-text="tooltipText" :optional="optional" :required="required" :label="label" @click="focusInput" > <slot></slot> </field-label> <div class="b-input-field__input-box" data-testid="input-box" :aria-disabled="disabled" @click="focusInput"> <!-- Optional Icon for Default text variant --> <span v-if="isDefaultVariant && (hasSlot('defaultIconBefore') || hasSlot('iconBefore'))" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__icon-before" > <slot v-if="hasSlot('iconBefore')" name="iconBefore" /> <slot v-if="hasSlot('defaultIconBefore') && !hasSlot('iconBefore')" name="defaultIconBefore" /> </span> <!-- Mandatory Icon for Payment Method variant --> <span v-if="isPaymentMethodVariant && hasSlot('paymentMethod')" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__payment-method" > <slot name="paymentMethod" /> </span> <!-- Dropdown at start --> <div v-if="shouldDisplayDropdownAtStart" :id="dropdownId" class="b-input-field__dropdown b-input-field__dropdown--start" @click.stop > <bento-dropdown v-if="dropdown" ref="inputDropdownElement" v-bind="inputFieldDropdownProps" :disabled="disabled" :readonly="isDropdownReadOnly" @update:model-value="onDropdownInput" /> </div> <!-- Static Value at start --> <bento-typography v-if="shouldDisplayStaticValueAtStart" :id="staticValueId" class="b-input-field__static-value b-input-field__static-value--start" el="span" variant="body" > <slot name="staticValue" /> </bento-typography> <div class="b-input-field__input-container"> <input :id="inputFieldId" v-bind="getInputAttributes($attrs)" ref="inputFieldElement" class="b-input-field__input" :aria-label="computedAriaLabel" :aria-describedby="computedAriaDescribedBy" :aria-owns="computedAriaOwns" :aria-invalid="shouldShowError" :placeholder="placeholder" :required="required" :aria-required="required" :type="type" :value="modelValue ?? value" :disabled="disabled" :readonly="isReadOnly" @input="onInput" @change="emit('change')" @focus="onFocus" @blur="onBlur" @keydown="emit('keydown', $event)" @keyup="emit('keyup', $event)" @keydown.esc="emit('escape-pressed')" @click="emit('click', $event)" /> <!-- Hint at the end --> <bento-typography v-if="hint && isFocused && (modelValue || value)" el="span" class="b-input-field__hint" > {{ hint }} </bento-typography> </div> <span v-if="hasSlot('iconAfter')" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__icon-after" data-testid="input-text__icon-after" > <slot name="iconAfter" /> </span> <!-- Static Value at the end --> <bento-typography v-if="shouldDisplayStaticValueAtEnd" :id="staticValueId" class="b-input-field__static-value b-input-field__static-value--end" el="span" variant="body" > <slot name="staticValue" /> </bento-typography> <!-- Dropdown at end --> <div v-if="shouldDisplayDropdownAtEnd" :id="dropdownId" data-testid="input-field-dropdown-container-end" class="b-input-field__dropdown b-input-field__dropdown--end" @click.stop > <bento-dropdown v-if="dropdown" :id="dropdownId" ref="inputDropdownElement" v-bind="inputFieldDropdownProps" :disabled="disabled" :readonly="isDropdownReadOnly" @update:model-value="onDropdownInput" /> </div> </div> <error-message v-if="shouldShowError && !!errorMessage" :id="errorId" :error-message="errorMessage" class="b-input-field__error-message" /> <span v-if="hasSlot('description') || description" :id="descriptionId" class="b-input-field__description" @click="focusInput" > <bento-typography el="span" variant="body"> <slot id="description" name="description"> {{ description }} </slot> </bento-typography> </span> </bento-typography> </template> <script setup lang="ts"> import { computed, inject, provide, type Ref, ref, toRef, toRefs, useAttrs, useSlots } from 'vue'; // Components import { BentoDropdown } from '@/components/dropdown'; import { BentoTypography } from '@/components/typography'; import { ErrorMessage, FieldLabel } from '@/internal'; // Utils import { deprecate } from '@/utils/ts/deprecate'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; import { generateUid } from '@/core/utils/ts'; import { getSlotText } from '@/utils/ts/get-slot-text'; import { useAriaLabel } from '@/utils/ts/aria-label'; import { useI18n } from '@/utils/ts/i18n'; import { printDevelopmentWarning } from '@/utils/ts/print-development-warning'; import { INPUT_FIELD_COMPONENT_INJECTION_KEY, INPUT_FIELD_HINT_INJECTION_KEY, INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, } from '@/components/input-field/input-field.keys'; import { type BentoListboxSelectedValue } from '@/types/listbox'; // Composables import { useFormLayoutFieldLoading, useHasSlot } from '@/composables'; // Types import { type BentoInputDropdownProps, BentoInputFieldElementPosition, type BentoInputFieldProps, BentoInputFieldStateClass, BentoInputFieldType, type BentoInputFieldValue, BentoInputFieldVariant, } from './input-field.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const attrs = useAttrs(); const slots = useSlots(); const props = withDefaults(defineProps<BentoInputFieldProps>(), { ariaHidden: true, ariaLabel: undefined, condensed: false, description: '', disabled: false, dropdown: undefined, dropdownPosition: BentoInputFieldElementPosition.START, error: false, errorMessage: null, label: '', modelValue: undefined, optional: false, placeholder: '', required: false, readonly: false, slashedZero: false, staticValue: '', staticValuePosition: BentoInputFieldElementPosition.START, tooltipText: null, type: BentoInputFieldType.TEXT, value: undefined, variant: BentoInputFieldVariant.DEFAULT, }); const emit = defineEmits<{ /** * Emitted when the element has lost focus */ (e: 'blur'): void; /** * Emitted when the input is clicked */ (e: 'click', event: MouseEvent); /** * Emitted when the user modifies the element's value. Unlike the 'input' event, the change event is not necessarily fired for each alteration to an element's value */ (e: 'change'): void; /** * Emitted when the Input Field's internal dropdown changes it's value */ (e: 'dropdown-input', selectedValue: BentoListboxSelectedValue): void; /** * Emitted when the "ESC" key is pressed */ (e: 'escape-pressed'): void; /** * Emitted when the element has received focus */ (e: 'focus'): void; /** * Emitted when a key is pressed down */ (e: 'keydown', event: KeyboardEvent): void; /** * Emitted when a key is pressed and lifted up */ (e: 'keyup', event: KeyboardEvent): void; /** * Emitted when the component's model changes * @deprecated Since v2.0.0. Use `v-model` or `update:model-value` instead. */ (e: 'input', inputValue: string): void; /** * Emitted when the Input Field's internal dropdown changes it's value. Contains every dropdown prop passed with the updated selected value as 'value' */ (e: 'update:dropdown', updatedDropdownProps: BentoInputDropdownProps): void; /** * Emitted when the component's model changes */ (e: 'update:model-value', inputValue: BentoInputFieldValue): void; }>(); const { emitValue } = useFormFieldEmits<BentoInputFieldValue>(emit); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const hasSlot = useHasSlot(slots); const inputFieldElement = ref(null); const inputDropdownElement = ref(null); const inputFieldId = generateUid('input'); const dropdownId = generateUid('input-dropdown'); const staticValueId = generateUid('input-static-value'); const descriptionId = generateUid('input-description'); const errorId = generateUid('input-error'); const isFocused = ref(false); /** * Gets the overriden or the default data-testid attribute * @param attributes - the live attributes object */ function getDataTestId(attributes: Record<string, unknown>) { return (attributes['data-testid'] as string) ?? 'input-text-container'; } /** * Filters out all aria attributes for the root element. * @param attributes - the live attributes object */ function getRootAttributes(attributes: Record<string, unknown>) { return Object.fromEntries( Object.entries(attributes).filter(([key]) => !key.startsWith('aria-') && key !== 'data-testid') ); } /** * Filters out the data-testid attribute for the input element. * @param attributes - the live attributes object */ function getInputAttributes(attributes: Record<string, unknown>) { return Object.fromEntries(Object.entries(attributes).filter(([key]) => key !== 'data-testid')); } // Provide INPUT_FIELD_COMPONENT_INJECTION_KEY as true, to set the input only dropdown size variant in the BentoDropdown comp provide(INPUT_FIELD_COMPONENT_INJECTION_KEY, true); // Renders the input with a hint if a string is provided const hint = inject<Ref<string | undefined>>(INPUT_FIELD_HINT_INJECTION_KEY, ref(undefined)); const onFocus = () => { isFocused.value = true; emit('focus'); }; const onBlur = () => { isFocused.value = false; emit('blur'); }; const { isReadOnly } = useFormLayoutFieldLoading(toRef(props, 'readonly')); const isDropdownReadOnly = computed<BentoInputDropdownProps['readonly']>( () => isReadOnly.value || props.dropdown?.readonly ); const shouldShowStaticValue = computed( () => props.variant === BentoInputFieldVariant.STATIC_VALUE && !!slots.staticValue ); const isDefaultVariant = computed(() => props.variant === BentoInputFieldVariant.DEFAULT); const isPaymentMethodVariant = computed(() => props.variant === BentoInputFieldVariant.PAYMENT_METHOD); const shouldDisplayStaticValueAtStart = computed( () => shouldShowStaticValue.value && props.staticValuePosition === BentoInputFieldElementPosition.START ); const shouldDisplayStaticValueAtEnd = computed( () => shouldShowStaticValue.value && props.staticValuePosition === BentoInputFieldElementPosition.END ); const shouldShowDropdown = computed(() => props.variant === BentoInputFieldVariant.DROPDOWN && !!props.dropdown); const shouldDisplayDropdownAtStart = computed( () => shouldShowDropdown.value && props.dropdownPosition === BentoInputFieldElementPosition.START ); const shouldDisplayDropdownAtEnd = computed( () => shouldShowDropdown.value && props.dropdownPosition === BentoInputFieldElementPosition.END ); const shouldShowError = computed( () => !props.disabled && !isReadOnly.value && (!!props.errorMessage || props.error) ); const computedLabel = computed(() => (getSlotText(slots)('default') as string) || props.label); const computedDescription = computed(() => (getSlotText(slots)('description') as string) || props.description); const computedAriaOwns = computed(() => { if (shouldShowDropdown.value === true && shouldShowStaticValue.value === true) { return `${staticValueId} ${dropdownId}`; } if (shouldShowDropdown.value === true) { return `${dropdownId}`; } if (shouldShowStaticValue.value === true) { return `${staticValueId}`; } return null; }); // vue-i18n issue with t() used in script - direct usage in template works // requires either createI18n({ legacy: false }) or to be wrapped in computed() const computedAriaLabelFallbackMessage = computed(() => attrs?.['aria-labelledby'] ? undefined : t('ariaLabelFallback') ); const computedAriaLabelDropdowFallback = computed(() => t('ariaLabelDropdownFallback', { inputLabel: computedAriaLabel.value }) ); const ariaLabelAttribute = ref(attrs['aria-label']); const { ariaLabel } = toRefs(props); const computedAriaLabel = useAriaLabel({ ariaLabel: ariaLabelAttribute.value as string, // TODO: take this line away when the aria-label props is removed label: ariaLabel, defaultFallback: computedLabel.value || computedAriaLabelFallbackMessage, }); const computedAriaDescribedBy = computed( () => `${computedDescription.value ? descriptionId : ''} ${ shouldShowError.value && !!props.errorMessage ? errorId : '' }` ); const inputFieldConditionalClasses = computed(() => ({ [`b-input-field--${BentoInputFieldStateClass.CONDENSED}`]: props.condensed, [`b-input-field--${BentoInputFieldStateClass.DISABLED}`]: props.disabled, [`b-input-field--${BentoInputFieldStateClass.READONLY}`]: isReadOnly.value, [`b-input-field--${BentoInputFieldStateClass.ERROR}`]: shouldShowError.value, [`b-input-field--slashed-zero`]: props.slashedZero, })); const inputFieldDropdownProps = computed(() => ({ 'aria-label': computedAriaLabelDropdowFallback.value, ...props.dropdown, })); const onInput = (event: Event) => { const inputValue = (event.target as HTMLInputElement).value; if (!props.disabled && !isReadOnly.value) { emitValue(inputValue); } }; const onDropdownInput = (selectedValue: BentoListboxSelectedValue) => { emit('dropdown-input', selectedValue); emit('update:dropdown', { ...props?.dropdown, value: selectedValue }); }; const focusInput = () => { inputFieldElement.value.focus(); }; const focus = () => { if (props.variant === 'dropdown') { inputDropdownElement.value.focus(); } else { focusInput(); } }; // Expose method so that other components can focus on it in Vue3 defineExpose({ focusInput, focus, }); const inputFieldParentComponent = inject(INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, ''); if (!Object.values(BentoInputFieldType).includes(props.type as BentoInputFieldType) && !inputFieldParentComponent) { if (props.type === 'tel') { printDevelopmentWarning( `"bento-input-field" dropdown does not support the type '${props.type}', use "bento-input-field-phone-number" instead.` ); } else if (props.type === 'password') { printDevelopmentWarning( `"bento-input-field" dropdown does not support the type '${props.type}', use "bento-input-field-password" instead.` ); } else { printDevelopmentWarning(`"bento-input-field" dropdown does not support the type '${props.type}'`); } } /** * Deprecations */ if (props.error) { deprecate( 'BentoInputField "error" property', `Use the BentoInputField "errorMessage" property instead to indicate that there is a modifier error. Having the field as "undefined" will hide/remove the error. <bento-dropdown errorMessage="Error message text" />`, '2.0.0' ); } if (hasSlot('defaultIconBefore')) { deprecate( 'BentoInputField "defaultIconBefore" slot', `Use the BentoInputField "iconBefore" slot instead to add an icon at the left side of the input field <bento-input-field> \t<template #iconBefore> \t\t<my-icon-goes-here /> \t</template> </bento-input-field>`, '2.0.0' ); } if (props.value) { deprecate( 'BentoInputField "value" property', `The use of "value" prop in "BentoInputField" is no longer supported. Use the "v-model" or "model-value" property instead.`, '2.0.0' ); } </script> <script lang="ts"> /** * Input field component is used to create interactive controls for web-based * forms in order to accept data from the user. * * @example * import { BentoInputField } from '@adyen/bento-vue2' * * const inputValue = ref(''); * * export default { * components: { BentoInputField }, * tempate: ` * <bento-input-field * :model-value="inputValue" * :@update:model-value="newValue => inputValue.value = newValue" * > * } */ export default { name: 'bento-input-field', inheritAttrs: false, model: { prop: 'modelValue' }, i18n: { messages }, }; </script> <style lang="scss" scoped src="./input-field.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import type { BentoInputFieldProps, BentoInputFieldValue } from '@/components/input-field/input-field.types'; import { type FormField } from '../../types/form-field'; /** * Public */ export interface BentoInputFieldPasswordValidationListItem { label?: string; key?: BentoInputFieldPasswordDefaultRuleKeys | string; // eslint-disable-next-line @typescript-eslint/no-explicit-any validate?: (value: BentoInputFieldValue, ...args: Array<any>) => boolean; // eslint-disable-next-line @typescript-eslint/no-explicit-any additionalArgs?: Array<any>; disabled?: boolean; } export type BentoInputFieldPasswordDefaultRuleKeys = | 'minLength' | 'requireLowercase' | 'requireNumeric' | 'requireSpecialCharacter' | 'requireUppercase'; export type BentoInputFieldPasswordErrorsList = Record<BentoInputFieldPasswordDefaultRuleKeys | string, string>; export interface BentoInputFieldPasswordValidation { title?: string; suggestion?: string; extendDefaultValidation?: boolean; list: Array<BentoInputFieldPasswordValidationListItem>; } export interface BentoInputFieldPasswordProps extends FormField { /** * Reduces height of the input */ condensed?: BentoInputFieldProps['condensed']; /** * The debounce time in milliseconds for running validation and emitting the `input` event after the user has stopped typing. * This helps to avoid excessive validation checks on every keystroke. Defaults to `500`. */ debounceTime?: number; /** * Performs real-time validations over the input value. Default: `false`. * Disabling the option (set to `true`) will not run pre-defined validations or custom validations defined in the `validation` field. */ disableValidation?: boolean; /** * Text displayed as the value of the input-field * @deprecated
|
|
1
|
+
import type { BentoInputFieldProps, BentoInputFieldValue } from '@/components/input-field/input-field.types'; import { type FormField } from '../../types/form-field'; /** * Public */ export interface BentoInputFieldPasswordValidationListItem { label?: string; key?: BentoInputFieldPasswordDefaultRuleKeys | string; // eslint-disable-next-line @typescript-eslint/no-explicit-any validate?: (value: BentoInputFieldValue, ...args: Array<any>) => boolean; // eslint-disable-next-line @typescript-eslint/no-explicit-any additionalArgs?: Array<any>; disabled?: boolean; } export type BentoInputFieldPasswordDefaultRuleKeys = | 'minLength' | 'requireLowercase' | 'requireNumeric' | 'requireSpecialCharacter' | 'requireUppercase'; export type BentoInputFieldPasswordErrorsList = Record<BentoInputFieldPasswordDefaultRuleKeys | string, string>; export interface BentoInputFieldPasswordValidation { title?: string; suggestion?: string; extendDefaultValidation?: boolean; list: Array<BentoInputFieldPasswordValidationListItem>; } export interface BentoInputFieldPasswordProps extends FormField { /** * Reduces height of the input */ condensed?: BentoInputFieldProps['condensed']; /** * The debounce time in milliseconds for running validation and emitting the `input` event after the user has stopped typing. * This helps to avoid excessive validation checks on every keystroke. Defaults to `500`. */ debounceTime?: number; /** * Performs real-time validations over the input value. Default: `false`. * Disabling the option (set to `true`) will not run pre-defined validations or custom validations defined in the `validation` field. */ disableValidation?: boolean; /** * Text displayed as the value of the input-field * @deprecated Since v2.0.0. Use `v-model` or `model-value` instead. */ value?: BentoInputFieldProps['value']; /** * Text displayed as the value of the input-field */ modelValue?: BentoInputFieldProps['modelValue']; /** * Configuration object for password field validation. * * This object defines the validation criteria and associated messages for a password field. * It includes the following properties: * * - `title`: The title displayed at the top of the validation hint. * - `suggestion`: The message displayed at the bottom of the validation hint, providing additional guidance. * - `extendDefaultValidation`: Set to true if you want to add your custom rules to the default validation instead of overriding the list. * - `list`: An array of callback functions to be executed on the value after each user input, validating the password against the specified rules. * * By default, the validation checks that: * * - The value has a minimum length of 12 characters. * - The value contains at least one lowercase letter. * - The value contains at least one uppercase letter. * - The value contains at least one numeric digit. * - The value contains at least one special character. * * */ validation?: BentoInputFieldPasswordValidation; /** * Whether the password validation hint is displayed when the input field is activated via focus or input */ withHint?: boolean; } /** * Private */ export enum BentoInputFieldPasswordType { TEXT = 'text', PASSWORD = 'password', } export enum BentoInputFieldPasswordDefaultRule { MIN_LENGTH = 'minLength', REQUIRE_LOWERCASE = 'requireLowercase', REQUIRE_NUMERIC = 'requireNumeric', REQUIRE_SPECIAL_CHARACTER = 'requireSpecialCharacter', REQUIRE_UPPERCASE = 'requireUppercase', }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div ref="inputFieldPasswordRef"> <bento-input-field ref="inputFieldPasswordInputRef" v-bind="inputFieldArgs" class="b-input-field-password__input" spellcheck="false" @blur="onBlur" @focus="onFocus" @update:model-value="onUpdateModelValue" @keydown="checkCapsLock" @keyup="checkCapsLock" > <template v-if="label">{{ label }}</template> <template v-else><slot /></template> <template v-if="description" #description> <slot name="description">{{ description }}</slot> </template> <template v-if="isCapsLockOn" #iconBefore> <div class="b-input-field-password__caps-lock-indicator" @mouseenter="handleCapsLockMouseEnter" @mouseleave="handleCapsLockMouseLeave" > <bento-image ref="inputFieldPasswordCapsLockIconRef" :src="CapsLockIcon" :alt="t('capsLockEnabled')" /> </div> <tooltip v-if="inputFieldPasswordCapsLockIconRef" :class="tooltipConditionalClass" :content="t('capsLockEnabled')" :disabled-focus-trap="true" :fallback-position="['top', 'bottom']" :is-shown="isTooltipDisplayed" :target-element="inputFieldPasswordCapsLockIconRef" /> </template> <template v-if="!disabled" #iconAfter> <bento-button variant="tertiary" @click="toggleInputType"> <template #iconLeft> <show-icon v-if="!isPasswordReadable" :svg-title="t('showPassword')" /> <hide-icon v-if="isPasswordReadable" :svg-title="t('hidePassword')" /> </template> </bento-button> </template> </bento-input-field> <bento-popover v-if="hasValidations" :open="isHintDisplayed" :target-element="inputFieldPasswordInputRef" :disable-focus-trap="true" :aria-label="computedValidations.title" :aria-describedby="validationListId" position="right-start" > <div :id="validationPopoverId"> <bento-typography v-if="computedValidations.title" class="b-input-field-password__validation-title"> {{ computedValidations.title }} </bento-typography> <p v-if="!hasErrors" class="b-input-field-password__validation-status--valid" role="alert"> {{ t('validationValid') }} </p> <ul :id="validationListId" class="b-input-field-password__validation-list"> <li v-for="(rule, index) in computedValidationList" :key="index" :class="passwordValidityConditionalClass[index]" aria-live="polite" aria-atomic="true" > <span class="b-input-field-password__validation-list-icon"> <checkmark-circle-fill-icon v-if="passwordValidity[index]" :svg-title="t('validationChecked')" /> <dot-icon v-else :svg-title="t('validationUnchecked')" /> </span> <bento-typography class="b-input-field-password__validation-list-text" el="span"> {{ rule.label }} </bento-typography> </li> </ul> <bento-typography v-if="computedValidations.suggestion" class="b-input-field-password__validation-suggestion" > {{ computedValidations.suggestion }} </bento-typography> </div> </bento-popover> </div> </template> <script setup lang="ts"> import { BentoButton } from '@/components/button'; import { BentoImage } from '@/components/image'; import { BentoInputField } from '@/components/input-field'; import { BentoPopover } from '@/components/popover'; import { BentoTypography } from '@/components/typography'; import { Tooltip } from '@/internal'; import type { BentoInputFieldProps } from '@/components/input-field/input-field.types'; import { BentoInputFieldPasswordDefaultRule, type BentoInputFieldPasswordErrorsList, type BentoInputFieldPasswordProps, BentoInputFieldPasswordType, type BentoInputFieldPasswordValidation, type BentoInputFieldPasswordValidationListItem, } from './input-field-password.types'; import CheckmarkCircleFillIcon from '@adyen/ui-assets-icons-16/vue/checkmark-circle-fill'; import DotIcon from '@adyen/ui-assets-icons-16/vue/dot'; import HideIcon from '@adyen/ui-assets-icons-16/vue/hide'; import ShowIcon from '@adyen/ui-assets-icons-16/vue/show'; import CapsLockIcon from './assets/capslock.svg'; import { useFocusWithin, watchDebounced } from '@vueuse/core'; import { computed, provide, ref, type SetupContext, useAttrs, watch } from 'vue'; import { useI18n } from '@/utils/ts/i18n'; import { minLength, requireLowercase, requireNumeric, requireSpecialCharacter, requireUppercase, } from '@/components/input-field-password/utilities/input-field-password.validation'; import { generateUid } from '@/core/utils/ts'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; import { deprecate } from '@/utils/ts/deprecate'; import messages from './messages.json'; import { INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY } from '@/components/input-field/input-field.keys'; type MessageSchema = (typeof messages)['en-US']; const { t, n } = useI18n<{ message: MessageSchema }>({ messages }); const props = withDefaults(defineProps<BentoInputFieldPasswordProps>(), { condensed: false, debounceTime: DEBOUNCE_DURATION, description: null, disabled: false, disableValidation: false, errorMessage: null, label: null, optional: false, placeholder: '', readonly: false, required: false, tooltipText: null, validation: undefined, value: undefined, modelValue: undefined, withHint: true, }); const emit = defineEmits<{ /** * Emitted when the element has lost focus */ (e: 'blur'): void; /** * Emitted when the element has received focus */ (e: 'focus'): void; /** * Emits an `input:valid` event when the value is validated against the provided validations. * This emit has a debounce and is emitted after an `input` event. */ (e: 'input:valid'): void; /** * Emits an `input:error` event when the value fails the validation. * This emit has a debounce and is emitted after an `input` event. */ (e: 'input:error', errors: BentoInputFieldPasswordErrorsList): void; /** * Emits an `input` event whenever the value in the input is changed. * @deprecated from version 2.0.0. Use `v-model` or `update:model-value` instead. */ (e: 'input', value: BentoInputFieldPasswordProps['value']): void; /** * Emits an `update:model-value` event whenever the value in the input is changed. */ (e: 'update:model-value', value: BentoInputFieldPasswordProps['modelValue']): void; }>(); const attrs = useAttrs(); const inputType = ref(BentoInputFieldPasswordType.PASSWORD); const inputValue = ref(props.modelValue ?? props.value); const isHintDisplayed = ref(false); const isTouched = ref(false); const passwordValidity = ref<Array<boolean>>([]); const errorsList = ref<BentoInputFieldPasswordErrorsList>({}); const capsLockTooltipTimeoutID = ref<ReturnType<typeof setTimeout>>(null); const isTooltipDisplayed = ref(false); const inputFieldPasswordRef = ref(null); const inputFieldPasswordInputRef = ref(null); const inputFieldPasswordCapsLockIconRef = ref(null); const isCapsLockOn = ref(false); provide(INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, 'bento-input-field-password'); const { focused } = useFocusWithin(inputFieldPasswordRef); const { emitValue } = useFormFieldEmits<BentoInputFieldPasswordProps['modelValue']>(emit); const validationListId = generateUid('validationListId'); const validationPopoverId = generateUid('validationPopoverId'); const isPasswordReadable = computed(() => inputType.value === BentoInputFieldPasswordType.TEXT); const passwordValidityConditionalClass = computed(() => passwordValidity.value.map(validity => validity ? 'b-input-field-password__validation-list--valid' : 'b-input-field-password__validation-list--invalid' ) ); const tooltipConditionalClass = computed(() => capsLockTooltipTimeoutID.value ? 'b-input-field-password__input-tooltip--timed' : '' ); const inputFieldArgs = computed<BentoInputFieldProps & SetupContext['attrs']>(() => ({ ariaHidden: false, condensed: props.condensed, disabled: props.disabled, errorMessage: props.errorMessage, optional: props.optional, placeholder: props.placeholder, readonly: props.readonly, required: props.required, tooltipText: props.tooltipText, type: inputType.value, modelValue: inputValue.value, 'aria-haspopup': hasValidations.value ? 'dialog' : false, 'aria-describedby': hasValidations.value ? validationPopoverId : '', ...attrs, })); const computedValidations = computed<BentoInputFieldPasswordValidation>(() => { if (props.validation && !props.validation.extendDefaultValidation) { return props.validation; } const defaultValidationList = [ { label: t('validationMinLength', { numberOfCharacters: n(MIN_PASSWORD_LENGTH) }), key: BentoInputFieldPasswordDefaultRule.MIN_LENGTH, validate: minLength, }, { label: t('validationRequireLowercase', { numberOne: n(1) }), key: BentoInputFieldPasswordDefaultRule.REQUIRE_LOWERCASE, validate: requireLowercase, }, { label: t('validationRequireUppercase', { numberOne: n(1) }), key: BentoInputFieldPasswordDefaultRule.REQUIRE_UPPERCASE, validate: requireUppercase, }, { label: t('validationRequireNumeric', { numberOne: n(1) }), key: BentoInputFieldPasswordDefaultRule.REQUIRE_NUMERIC, validate: requireNumeric, }, { label: t('validationRequireSpecialCharacter', { numberOne: n(1) }), key: BentoInputFieldPasswordDefaultRule.REQUIRE_SPECIAL_CHARACTER, validate: requireSpecialCharacter, }, ]; let validationList: Array<BentoInputFieldPasswordValidationListItem> = defaultValidationList; if (props.validation) { const defaultValidationKeys: Array<string> = defaultValidationList.map(rule => rule.key); const customRuleKeys = props.validation?.list?.map(rule => rule.key); // remove from the custom list any rules with default keys (rules that are overriding existent rules) const customValidationList = props.validation?.list?.filter(rule => !defaultValidationKeys.includes(rule.key)) ?? []; // loop through the default list and replace default rule with the custom rule if it exists const validationListWithOverrides = defaultValidationList.map(rule => { if (customRuleKeys?.includes(rule.key)) { const customRule = props.validation?.list?.find(customRule => customRule.key === rule.key); return customRule; } else { return rule; } }); validationList = [...validationListWithOverrides, ...customValidationList]; } return { title: props.validation?.title ?? t('validationTitle'), suggestion: props.validation?.suggestion ?? t('validationSuggestion'), list: validationList, }; }); const computedValidationList = computed(() => computedValidations.value?.list?.filter(rule => !rule.disabled)); const hasErrors = computed(() => passwordValidity.value?.some(validation => !validation)); const hasValidations = computed( () => inputFieldPasswordInputRef.value && computedValidations.value?.list?.length > 0 ); const checkCapsLock = (e: KeyboardEvent) => { // Check if getModifierState exists and is a function in case the user's browser is autofilling if (typeof e.getModifierState === 'function') { isCapsLockOn.value = e.getModifierState('CapsLock'); } }; const toggleInputType = () => { if (isPasswordReadable.value) { inputType.value = BentoInputFieldPasswordType.PASSWORD; } else { inputType.value = BentoInputFieldPasswordType.TEXT; } }; const onUpdateModelValue = (value: string) => { if (props.disabled) { return; } inputValue.value = value; emitValue(inputValue.value); }; const onFocus = () => { emit('focus'); }; const onBlur = () => { emit('blur'); }; const checkValidity = (value: BentoInputFieldPasswordProps['value']) => { const rulesValidityObj = computedValidationList.value?.map(validationItem => ({ key: validationItem.key, label: validationItem.label, isValid: validationItem.validate(value, ...(validationItem.additionalArgs ?? [])), })); let customRuleIndex = 1; passwordValidity.value = rulesValidityObj?.map(error => error.isValid); errorsList.value = rulesValidityObj ?.filter(rule => !rule.isValid) .reduce((errorsObject, error) => { const key = error.key ?? `custom-rule-${customRuleIndex++}`; return { ...errorsObject, [key]: error.label, }; }, {}); }; const handleCapsLockMouseLeave = () => { isTooltipDisplayed.value = false; capsLockTooltipTimeoutID.value = null; }; const handleCapsLockMouseEnter = () => { isTooltipDisplayed.value = true; }; watch( () => [props.value, props.modelValue], () => { inputValue.value = props.modelValue ?? props.value; } ); watch(focused, focused => { // Always set dirty to true when blur occurs if (!focused) { isTouched.value = true; } // Disable validations & hint if "disableValidation" is "true" if (!props.disableValidation) { if (props.withHint) { isHintDisplayed.value = focused; } checkValidity(inputValue.value); } }); watch(isCapsLockOn, isCapsLockOn => { if (!isCapsLockOn) { clearTimeout(capsLockTooltipTimeoutID.value); capsLockTooltipTimeoutID.value = null; isTooltipDisplayed.value = false; } else { isTooltipDisplayed.value = true; capsLockTooltipTimeoutID.value = setTimeout(() => { capsLockTooltipTimeoutID.value = null; isTooltipDisplayed.value = false; }, TOOLTIP_TIMEOUT); } }); watchDebounced( inputValue, inputValue => { checkValidity(inputValue); if (!hasErrors.value) { emit('input:valid'); } else { emit('input:error', errorsList.value); } }, // wait for 0.5s before checking validity OR 2s of continuous typing { debounce: props.debounceTime, maxWait: DEBOUNCE_MAX_WAIT } ); if (props.value) { deprecate( 'BentoInputFieldPassword "value" property', `The use of "value" prop in "BentoInputFieldPassword" is no longer supported. Use the "v-model" or "model-value" property instead.`, '2.0.0' ); } </script> <script lang="ts"> const MIN_PASSWORD_LENGTH = 12; const DEBOUNCE_DURATION = 500; const DEBOUNCE_MAX_WAIT = 2000; const TOOLTIP_TIMEOUT = 2000; /** * The input field password is a component designed to support users' preference for entering values securely. * This is an extension of the `bento-input-field` component. * * @example * import { BentoInputFieldPassword } from '@adyen/bento-vue2'; * * export default { * components: { BentoInputFieldPassword }, * template: ` * <bento-input-field-password * label="Label" * error-message="Error" * :validation="{ * extendDefaultValidation: true, * list: [ * { label: '18 characters', key: 'minLength', validate: minLength, additionalArgs: [18] }, * { label: 'Always true', key: 'alwaysTrue', validate: () => true }, * { key: 'requireSpecialCharacter', disabled: true } * ], * }" * /> * ` * } */ export default { i18n: { messages }, name: 'bento-input-field-password', model: { prop: 'modelValue' }, }; </script> <style lang="scss" scoped src="./input-field-password.scss" />
|
|
1
|
+
<template> <div ref="inputFieldPasswordRef"> <bento-input-field ref="inputFieldPasswordInputRef" v-bind="inputFieldArgs" class="b-input-field-password__input" spellcheck="false" @blur="onBlur" @focus="onFocus" @update:model-value="onUpdateModelValue" @keydown="checkCapsLock" @keyup="checkCapsLock" > <template v-if="label">{{ label }}</template> <template v-else><slot /></template> <template v-if="description" #description> <slot name="description">{{ description }}</slot> </template> <template v-if="isCapsLockOn" #iconBefore> <div class="b-input-field-password__caps-lock-indicator" @mouseenter="handleCapsLockMouseEnter" @mouseleave="handleCapsLockMouseLeave" > <bento-image ref="inputFieldPasswordCapsLockIconRef" :src="CapsLockIcon" :alt="t('capsLockEnabled')" /> </div> <tooltip v-if="inputFieldPasswordCapsLockIconRef" :class="tooltipConditionalClass" :content="t('capsLockEnabled')" :disabled-focus-trap="true" :fallback-position="['top', 'bottom']" :is-shown="isTooltipDisplayed" :target-element="inputFieldPasswordCapsLockIconRef" /> </template> <template v-if="!disabled" #iconAfter> <bento-button variant="tertiary" @click="toggleInputType"> <template #iconLeft> <show-icon v-if="!isPasswordReadable" :svg-title="t('showPassword')" /> <hide-icon v-if="isPasswordReadable" :svg-title="t('hidePassword')" /> </template> </bento-button> </template> </bento-input-field> <bento-popover v-if="hasValidations" :open="isHintDisplayed" :target-element="inputFieldPasswordInputRef" :disable-focus-trap="true" :aria-label="computedValidations.title" :aria-describedby="validationListId" position="right-start" > <div :id="validationPopoverId"> <bento-typography v-if="computedValidations.title" class="b-input-field-password__validation-title"> {{ computedValidations.title }} </bento-typography> <p v-if="!hasErrors" class="b-input-field-password__validation-status--valid" role="alert"> {{ t('validationValid') }} </p> <ul :id="validationListId" class="b-input-field-password__validation-list"> <li v-for="(rule, index) in computedValidationList" :key="index" :class="passwordValidityConditionalClass[index]" aria-live="polite" aria-atomic="true" > <span class="b-input-field-password__validation-list-icon"> <checkmark-circle-fill-icon v-if="passwordValidity[index]" :svg-title="t('validationChecked')" /> <dot-icon v-else :svg-title="t('validationUnchecked')" /> </span> <bento-typography class="b-input-field-password__validation-list-text" el="span"> {{ rule.label }} </bento-typography> </li> </ul> <bento-typography v-if="computedValidations.suggestion" class="b-input-field-password__validation-suggestion" > {{ computedValidations.suggestion }} </bento-typography> </div> </bento-popover> </div> </template> <script setup lang="ts"> import { BentoButton } from '@/components/button'; import { BentoImage } from '@/components/image'; import { BentoInputField } from '@/components/input-field'; import { BentoPopover } from '@/components/popover'; import { BentoTypography } from '@/components/typography'; import { Tooltip } from '@/internal'; import type { BentoInputFieldProps } from '@/components/input-field/input-field.types'; import { BentoInputFieldPasswordDefaultRule, type BentoInputFieldPasswordErrorsList, type BentoInputFieldPasswordProps, BentoInputFieldPasswordType, type BentoInputFieldPasswordValidation, type BentoInputFieldPasswordValidationListItem, } from './input-field-password.types'; import CheckmarkCircleFillIcon from '@adyen/ui-assets-icons-16/vue/checkmark-circle-fill'; import DotIcon from '@adyen/ui-assets-icons-16/vue/dot'; import HideIcon from '@adyen/ui-assets-icons-16/vue/hide'; import ShowIcon from '@adyen/ui-assets-icons-16/vue/show'; import CapsLockIcon from './assets/capslock.svg'; import { useFocusWithin, watchDebounced } from '@vueuse/core'; import { computed, provide, ref, type SetupContext, useAttrs, watch } from 'vue'; import { useI18n } from '@/utils/ts/i18n'; import { minLength, requireLowercase, requireNumeric, requireSpecialCharacter, requireUppercase, } from '@/components/input-field-password/utilities/input-field-password.validation'; import { generateUid } from '@/core/utils/ts'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; import { deprecate } from '@/utils/ts/deprecate'; import messages from './messages.json'; import { INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY } from '@/components/input-field/input-field.keys'; type MessageSchema = (typeof messages)['en-US']; const { t, n } = useI18n<{ message: MessageSchema }>({ messages }); const props = withDefaults(defineProps<BentoInputFieldPasswordProps>(), { condensed: false, debounceTime: DEBOUNCE_DURATION, description: null, disabled: false, disableValidation: false, errorMessage: null, label: null, optional: false, placeholder: '', readonly: false, required: false, tooltipText: null, validation: undefined, value: undefined, modelValue: undefined, withHint: true, }); const emit = defineEmits<{ /** * Emitted when the element has lost focus */ (e: 'blur'): void; /** * Emitted when the element has received focus */ (e: 'focus'): void; /** * Emits an `input:valid` event when the value is validated against the provided validations. * This emit has a debounce and is emitted after an `input` event. */ (e: 'input:valid'): void; /** * Emits an `input:error` event when the value fails the validation. * This emit has a debounce and is emitted after an `input` event. */ (e: 'input:error', errors: BentoInputFieldPasswordErrorsList): void; /** * Emits an `input` event whenever the value in the input is changed. * @deprecated Since v2.0.0. Use `v-model` or `update:model-value` instead. */ (e: 'input', value: BentoInputFieldPasswordProps['value']): void; /** * Emits an `update:model-value` event whenever the value in the input is changed. */ (e: 'update:model-value', value: BentoInputFieldPasswordProps['modelValue']): void; }>(); const attrs = useAttrs(); const inputType = ref(BentoInputFieldPasswordType.PASSWORD); const inputValue = ref(props.modelValue ?? props.value); const isHintDisplayed = ref(false); const isTouched = ref(false); const passwordValidity = ref<Array<boolean>>([]); const errorsList = ref<BentoInputFieldPasswordErrorsList>({}); const capsLockTooltipTimeoutID = ref<ReturnType<typeof setTimeout>>(null); const isTooltipDisplayed = ref(false); const inputFieldPasswordRef = ref(null); const inputFieldPasswordInputRef = ref(null); const inputFieldPasswordCapsLockIconRef = ref(null); const isCapsLockOn = ref(false); provide(INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, 'bento-input-field-password'); const { focused } = useFocusWithin(inputFieldPasswordRef); const { emitValue } = useFormFieldEmits<BentoInputFieldPasswordProps['modelValue']>(emit); const validationListId = generateUid('validationListId'); const validationPopoverId = generateUid('validationPopoverId'); const isPasswordReadable = computed(() => inputType.value === BentoInputFieldPasswordType.TEXT); const passwordValidityConditionalClass = computed(() => passwordValidity.value.map(validity => validity ? 'b-input-field-password__validation-list--valid' : 'b-input-field-password__validation-list--invalid' ) ); const tooltipConditionalClass = computed(() => capsLockTooltipTimeoutID.value ? 'b-input-field-password__input-tooltip--timed' : '' ); const inputFieldArgs = computed<BentoInputFieldProps & SetupContext['attrs']>(() => ({ ariaHidden: false, condensed: props.condensed, disabled: props.disabled, errorMessage: props.errorMessage, optional: props.optional, placeholder: props.placeholder, readonly: props.readonly, required: props.required, tooltipText: props.tooltipText, type: inputType.value, modelValue: inputValue.value, 'aria-haspopup': hasValidations.value ? 'dialog' : false, 'aria-describedby': hasValidations.value ? validationPopoverId : '', ...attrs, })); const computedValidations = computed<BentoInputFieldPasswordValidation>(() => { if (props.validation && !props.validation.extendDefaultValidation) { return props.validation; } const defaultValidationList = [ { label: t('validationMinLength', { numberOfCharacters: n(MIN_PASSWORD_LENGTH) }), key: BentoInputFieldPasswordDefaultRule.MIN_LENGTH, validate: minLength, }, { label: t('validationRequireLowercase', { numberOne: n(1) }), key: BentoInputFieldPasswordDefaultRule.REQUIRE_LOWERCASE, validate: requireLowercase, }, { label: t('validationRequireUppercase', { numberOne: n(1) }), key: BentoInputFieldPasswordDefaultRule.REQUIRE_UPPERCASE, validate: requireUppercase, }, { label: t('validationRequireNumeric', { numberOne: n(1) }), key: BentoInputFieldPasswordDefaultRule.REQUIRE_NUMERIC, validate: requireNumeric, }, { label: t('validationRequireSpecialCharacter', { numberOne: n(1) }), key: BentoInputFieldPasswordDefaultRule.REQUIRE_SPECIAL_CHARACTER, validate: requireSpecialCharacter, }, ]; let validationList: Array<BentoInputFieldPasswordValidationListItem> = defaultValidationList; if (props.validation) { const defaultValidationKeys: Array<string> = defaultValidationList.map(rule => rule.key); const customRuleKeys = props.validation?.list?.map(rule => rule.key); // remove from the custom list any rules with default keys (rules that are overriding existent rules) const customValidationList = props.validation?.list?.filter(rule => !defaultValidationKeys.includes(rule.key)) ?? []; // loop through the default list and replace default rule with the custom rule if it exists const validationListWithOverrides = defaultValidationList.map(rule => { if (customRuleKeys?.includes(rule.key)) { const customRule = props.validation?.list?.find(customRule => customRule.key === rule.key); return customRule; } else { return rule; } }); validationList = [...validationListWithOverrides, ...customValidationList]; } return { title: props.validation?.title ?? t('validationTitle'), suggestion: props.validation?.suggestion ?? t('validationSuggestion'), list: validationList, }; }); const computedValidationList = computed(() => computedValidations.value?.list?.filter(rule => !rule.disabled)); const hasErrors = computed(() => passwordValidity.value?.some(validation => !validation)); const hasValidations = computed( () => inputFieldPasswordInputRef.value && computedValidations.value?.list?.length > 0 ); const checkCapsLock = (e: KeyboardEvent) => { // Check if getModifierState exists and is a function in case the user's browser is autofilling if (typeof e.getModifierState === 'function') { isCapsLockOn.value = e.getModifierState('CapsLock'); } }; const toggleInputType = () => { if (isPasswordReadable.value) { inputType.value = BentoInputFieldPasswordType.PASSWORD; } else { inputType.value = BentoInputFieldPasswordType.TEXT; } }; const onUpdateModelValue = (value: string) => { if (props.disabled) { return; } inputValue.value = value; emitValue(inputValue.value); }; const onFocus = () => { emit('focus'); }; const onBlur = () => { emit('blur'); }; const checkValidity = (value: BentoInputFieldPasswordProps['value']) => { const rulesValidityObj = computedValidationList.value?.map(validationItem => ({ key: validationItem.key, label: validationItem.label, isValid: validationItem.validate(value, ...(validationItem.additionalArgs ?? [])), })); let customRuleIndex = 1; passwordValidity.value = rulesValidityObj?.map(error => error.isValid); errorsList.value = rulesValidityObj ?.filter(rule => !rule.isValid) .reduce((errorsObject, error) => { const key = error.key ?? `custom-rule-${customRuleIndex++}`; return { ...errorsObject, [key]: error.label, }; }, {}); }; const handleCapsLockMouseLeave = () => { isTooltipDisplayed.value = false; capsLockTooltipTimeoutID.value = null; }; const handleCapsLockMouseEnter = () => { isTooltipDisplayed.value = true; }; watch( () => [props.value, props.modelValue], () => { inputValue.value = props.modelValue ?? props.value; } ); watch(focused, focused => { // Always set dirty to true when blur occurs if (!focused) { isTouched.value = true; } // Disable validations & hint if "disableValidation" is "true" if (!props.disableValidation) { if (props.withHint) { isHintDisplayed.value = focused; } checkValidity(inputValue.value); } }); watch(isCapsLockOn, isCapsLockOn => { if (!isCapsLockOn) { clearTimeout(capsLockTooltipTimeoutID.value); capsLockTooltipTimeoutID.value = null; isTooltipDisplayed.value = false; } else { isTooltipDisplayed.value = true; capsLockTooltipTimeoutID.value = setTimeout(() => { capsLockTooltipTimeoutID.value = null; isTooltipDisplayed.value = false; }, TOOLTIP_TIMEOUT); } }); watchDebounced( inputValue, inputValue => { checkValidity(inputValue); if (!hasErrors.value) { emit('input:valid'); } else { emit('input:error', errorsList.value); } }, // wait for 0.5s before checking validity OR 2s of continuous typing { debounce: props.debounceTime, maxWait: DEBOUNCE_MAX_WAIT } ); if (props.value) { deprecate( 'BentoInputFieldPassword "value" property', `The use of "value" prop in "BentoInputFieldPassword" is no longer supported. Use the "v-model" or "model-value" property instead.`, '2.0.0' ); } </script> <script lang="ts"> const MIN_PASSWORD_LENGTH = 12; const DEBOUNCE_DURATION = 500; const DEBOUNCE_MAX_WAIT = 2000; const TOOLTIP_TIMEOUT = 2000; /** * The input field password is a component designed to support users' preference for entering values securely. * This is an extension of the `bento-input-field` component. * * @example * import { BentoInputFieldPassword } from '@adyen/bento-vue2'; * * export default { * components: { BentoInputFieldPassword }, * template: ` * <bento-input-field-password * label="Label" * error-message="Error" * :validation="{ * extendDefaultValidation: true, * list: [ * { label: '18 characters', key: 'minLength', validate: minLength, additionalArgs: [18] }, * { label: 'Always true', key: 'alwaysTrue', validate: () => true }, * { key: 'requireSpecialCharacter', disabled: true } * ], * }" * /> * ` * } */ export default { i18n: { messages }, name: 'bento-input-field-password', model: { prop: 'modelValue' }, }; </script> <style lang="scss" scoped src="./input-field-password.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import { type CountryCode } from 'libphonenumber-js'; import { type FormField } from '../../types/form-field'; import { type BentoInputDropdownProps } from '@/components/input-field/input-field.types'; export type BentoInputFieldPhoneNumberDropdownProps = Pick<BentoInputDropdownProps, 'readonly'>; /** * String literal with all the available country codes. */ export type BentoInputFieldPhoneNumberCountryCode = CountryCode | any; export interface BentoInputFieldPhoneNumberProps extends FormField { /** * Array of country codes to show on the dropdown */ countries?: Array<BentoInputFieldPhoneNumberCountryCode>; /** * Dropdown props for the small selectable dropdown menu. * Properties supported: * * - `readonly`: Sets the dropdown in read-only mode */ dropdown?: BentoInputFieldPhoneNumberDropdownProps; /** * If set, allows filtering on the countries dropdown */ dynamicFiltering?: boolean; /** * If true, applies error styling to the field. * @deprecated Use `errorMessage`
|
|
1
|
+
import { type CountryCode } from 'libphonenumber-js'; import { type FormField } from '../../types/form-field'; import { type BentoInputDropdownProps } from '@/components/input-field/input-field.types'; export type BentoInputFieldPhoneNumberDropdownProps = Pick<BentoInputDropdownProps, 'readonly'>; /** * String literal with all the available country codes. */ export type BentoInputFieldPhoneNumberCountryCode = CountryCode | any; export interface BentoInputFieldPhoneNumberProps extends FormField { /** * Array of country codes to show on the dropdown */ countries?: Array<BentoInputFieldPhoneNumberCountryCode>; /** * Dropdown props for the small selectable dropdown menu. * Properties supported: * * - `readonly`: Sets the dropdown in read-only mode */ dropdown?: BentoInputFieldPhoneNumberDropdownProps; /** * If set, allows filtering on the countries dropdown */ dynamicFiltering?: boolean; /** * If true, applies error styling to the field. * @deprecated Since v2.0.0. Use `errorMessage` instead. */ error?: boolean; /** * Selected country code */ selectedCountry?: BentoInputFieldPhoneNumberCountryCode; /** * The current value of the field. * @deprecated Since v2.0.0. Use `v-model` or `model-value` instead. */ value?: string | number; /** * The current value of the field. */ modelValue?: string | number; }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <bento-input-field v-bind="computedProps" ref="inputField" variant="dropdown" :dropdown="dropdown" class="b-input-field-phone-number" :model-value="displayValue" :placeholder="computedPlaceholder" type="tel" @blur="onBlur" @update:model-value="onUpdateModelValue" @keydown="onKeydown" @dropdown-input="onCountryChange" > <template v-if="label">{{ label }}</template> <template v-else><slot /></template> <template #description> <slot name="description">{{ description }}</slot> </template> </bento-input-field> </template> <script setup lang="ts"> import { computed, nextTick, provide, ref, watch } from 'vue'; import { type BentoInputDropdownProps, BentoInputField } from '@/components/input-field'; import { AsYouType, type CountryCode } from 'libphonenumber-js'; import { useI18n } from '@/utils/ts/i18n'; import { appendRegionCode, BentoInputFieldPhoneNumberCountries, getCountriesDropdownItems, getFormattedPhoneCode, getPhoneNumberPlaceholder, handleInputCursor, hasDefaultRegionCode, parsePhoneAndCountry, preventRegionCodeRepetition, stripRegionCode, } from './utils'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; import { deprecate } from '@/utils/ts/deprecate'; import type { BentoInputFieldPhoneNumberProps } from './input-field-phone-number.types'; import messages from './messages.json'; import { INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY } from '@/components/input-field/input-field.keys'; type MessageSchema = (typeof messages)['en-US']; const { t, locale } = useI18n<{ message: MessageSchema }>({ messages }); const props = withDefaults(defineProps<BentoInputFieldPhoneNumberProps>(), { countries: () => BentoInputFieldPhoneNumberCountries, description: undefined, dynamicFiltering: true, }); /** * Avoid passing `value` to `bento-input-field`. * This component uses `displayValue` instead. */ const computedProps = computed(() => { const { value, ...allProps } = props; return allProps; }); const emit = defineEmits<{ /** * Emitted when the element has lost focus */ (e: 'blur'): void; /** * Emitted when the value changes. The value is the unformatted phone number in E.164 format. * @deprecated from version 2.0.0. Use `v-model` or `update:model-value` instead. */ (e: 'input', value: string): void; /** * Emitted when the value changes. The value is the unformatted phone number in E.164 format. */ (e: 'update:model-value', value: string): void; /** * Emitted when the selected country is updated. */ (e: 'update:selected-country', country: CountryCode): void; /** * Emitted when the phone number's validity changes. * The payload is a boolean indicating whether the number is valid. */ (e: 'input:valid', value: boolean): void; }>(); const inputField = ref(null); const displayValue = ref(''); const internalValue = ref(''); const insertedChar = ref(''); const { emitValue } = useFormFieldEmits<string>(emit); provide(INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, 'bento-input-field-phone-number'); const computedPlaceholder = computed(() => { if (props.placeholder) { return props.placeholder; } if (props.selectedCountry) { return getPhoneNumberPlaceholder(props.selectedCountry); } return ''; }); const isDropdownReadonly = computed(() => props.dropdown?.readonly); const countryItems = computed(() => getCountriesDropdownItems(props.countries, locale.value)); const dropdown = computed<BentoInputDropdownProps>(() => ({ items: countryItems.value, modelValue: props.selectedCountry, readonly: isDropdownReadonly.value, dynamicFiltering: props.dynamicFiltering, 'aria-label': t('countrySelectorDropdown'), })); const onBlur = () => emit('blur'); const onUpdateModelValue = async (value: string) => { const inputEl = inputField.value.$refs.inputFieldElement; let formatter: AsYouType; // Check if the input value starts with '+' or '00', indicating it includes a country code. if (value.startsWith('+') || value.startsWith('00')) { // Reset displayValue to ensure changes are always reflected. // For example, if you paste '+1 264 123 4567' multiple times, the display value should always be '1234567'. // Without this, the displayValue will be '1234567' the first time but subsequently stay as '+1 264 123 4567'. displayValue.value = ''; // Normalize the value by replacing '00' with '+'. const normalizedValue = value.startsWith('00') ? `+${value.slice(2)}` : value; // Create a new AsYouType formatter to format the number as it's typed. formatter = new AsYouType(); formatter.input(normalizedValue); // Get the country code from the formatted number. const country = formatter.getCountry(); // Check if the extracted country code is valid and included in the allowed countries. const isValidCountry = country && props.countries.includes(country); // If the dropdown is readonly, determine whether to strip the region code based on the selected country. if (isDropdownReadonly.value) { displayValue.value = props.selectedCountry === country ? stripRegionCode(formatter.getNationalNumber(), country) : value; } else { // Emit an event to update the selected country, if valid. Otherwise, set it to null. emit('update:selected-country', isValidCountry ? country : null); // Strip the region code from the formatted number if the country is valid. displayValue.value = isValidCountry ? stripRegionCode(formatter.getNationalNumber(), country) : value; } const numberValue = formatter.getNumberValue() ?? ''; const formattedPhoneCode = getFormattedPhoneCode(country); const shouldClearInput = isValidCountry && numberValue.startsWith(formattedPhoneCode) && numberValue.length <= formattedPhoneCode.length; internalValue.value = shouldClearInput ? '' : numberValue; emitValue(internalValue.value); } else { // If the input doesn't start with a country code, format the number based on the selected country. formatter = new AsYouType(props.selectedCountry); const formatted = formatter.input(appendRegionCode(value, props.selectedCountry)); // Prevent repetition of the region code and get the unformatted number. const numberValue = formatter.getNumberValue(); const unformattedNumber = numberValue ? preventRegionCodeRepetition(value, numberValue, props.selectedCountry) : value; // Emit raw input when parsing fails i.e. number do not have country code // Emit the unformatted number value and update the display value with the stripped region code. internalValue.value = unformattedNumber; emitValue(internalValue.value); displayValue.value = stripRegionCode(formatted, props.selectedCountry); } await nextTick(); // Re-parse the number to ensure correct formatting, e.g., adding a leading '0' for NL numbers if valid and `hasDefaultRegionCode` returns false. if (formatter.isValid() && props.selectedCountry && !hasDefaultRegionCode(props.selectedCountry)) { const { displayNumber, isValid } = parsePhoneAndCountry( displayValue.value?.toString(), props.selectedCountry, props.countries ); displayValue.value = displayNumber; emitValidation(isValid); } else { emitValidation(formatter.isValid()); } handleInputCursor(inputEl, displayValue.value.toString(), insertedChar.value); }; const emitValidation = (isValid: boolean) => { // Numbers shorter than 5 digits are never valid phone numbers. // `libphonenumber-js` rejects numbers under 5 digits (`TOO_SHORT`). // We don’t call that check directly here, but use the same value for consistency. const MIN_PHONE_NUMBER_LENGTH_FOR_VALIDATION = 5; const numberLength = displayValue.value?.toString().replace(/\D/g, '')?.length ?? 0; const isValidNumberLength = numberLength >= MIN_PHONE_NUMBER_LENGTH_FOR_VALIDATION; emit('input:valid', isValidNumberLength && isValid); }; const onKeydown = event => { if (/^[a-zA-Z]$/.test(event.key) && !event.metaKey && !event.ctrlKey) { event.preventDefault(); return; } insertedChar.value = event.key; }; const onCountryChange = (country: CountryCode) => { emit('update:selected-country', country); const isUnformatted = displayValue.value?.toString().startsWith('+'); if (hasDefaultRegionCode(country) && !isUnformatted) { let formatter = new AsYouType(country); const formatted = formatter.input(appendRegionCode(displayValue.value?.toString(), country)); const unformattedNumber = preventRegionCodeRepetition(displayValue.value?.toString(), formatter.getNumberValue(), country) ?? ''; internalValue.value = unformattedNumber; emitValue(internalValue.value); displayValue.value = stripRegionCode(formatted, country); emitValidation(formatter.isValid()); } else { const { displayNumber, unformattedNumber, isValid } = parsePhoneAndCountry( displayValue.value?.toString(), country, props.countries ); internalValue.value = unformattedNumber ?? ''; emitValue(internalValue.value); displayValue.value = displayNumber; emitValidation(isValid); } }; watch( () => [props.value, props.modelValue], () => { const newValue = props.modelValue ?? props.value; if (newValue === internalValue.value) { return; } const { displayNumber, country, isValid } = parsePhoneAndCountry( newValue?.toString(), props.selectedCountry, props.countries ); if (country && country !== props.selectedCountry && !isDropdownReadonly.value) { emit('update:selected-country', country); } displayValue.value = stripRegionCode(displayNumber, country); emitValidation(isValid); }, { immediate: true } ); if (props.value) { deprecate( 'BentoInputFieldPhoneNumber "value" property', `The use of "value" prop in "BentoInputFieldPhoneNumber" is no longer supported. Use the "v-model" or "model-value" property instead.`, '2.0.0' ); } </script> <script lang="ts"> /** * Input field phone number can be used to display phone numbers that come with a country code. * * @example * import { BentoInputFieldPhoneNumber } from '@adyen/bento-vue2'; * * export default { * components: { BentoInputFieldPhoneNumber }, * template: ` * <bento-input-field-phone-number * label="Phone number" * :countries="['NL', 'BE']" * model-value="0612345678" * selected-country="NL" * /> * ` * } */ export default { i18n: { messages }, model: { prop: 'modelValue' }, }; </script> <style lang="scss" scoped src="./input-field-phone-number.scss" />
|
|
1
|
+
<template> <bento-input-field v-bind="computedProps" ref="inputField" variant="dropdown" :dropdown="dropdown" class="b-input-field-phone-number" :model-value="displayValue" :placeholder="computedPlaceholder" type="tel" @blur="onBlur" @update:model-value="onUpdateModelValue" @keydown="onKeydown" @dropdown-input="onCountryChange" > <template v-if="label">{{ label }}</template> <template v-else><slot /></template> <template #description> <slot name="description">{{ description }}</slot> </template> </bento-input-field> </template> <script setup lang="ts"> import { computed, nextTick, provide, ref, watch } from 'vue'; import { type BentoInputDropdownProps, BentoInputField } from '@/components/input-field'; import { AsYouType, type CountryCode } from 'libphonenumber-js'; import { useI18n } from '@/utils/ts/i18n'; import { appendRegionCode, BentoInputFieldPhoneNumberCountries, getCountriesDropdownItems, getFormattedPhoneCode, getPhoneNumberPlaceholder, handleInputCursor, hasDefaultRegionCode, parsePhoneAndCountry, preventRegionCodeRepetition, stripRegionCode, } from './utils'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; import { deprecate } from '@/utils/ts/deprecate'; import type { BentoInputFieldPhoneNumberProps } from './input-field-phone-number.types'; import messages from './messages.json'; import { INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY } from '@/components/input-field/input-field.keys'; type MessageSchema = (typeof messages)['en-US']; const { t, locale } = useI18n<{ message: MessageSchema }>({ messages }); const props = withDefaults(defineProps<BentoInputFieldPhoneNumberProps>(), { countries: () => BentoInputFieldPhoneNumberCountries, description: undefined, dynamicFiltering: true, }); /** * Avoid passing `value` to `bento-input-field`. * This component uses `displayValue` instead. */ const computedProps = computed(() => { const { value, ...allProps } = props; return allProps; }); const emit = defineEmits<{ /** * Emitted when the element has lost focus */ (e: 'blur'): void; /** * Emitted when the value changes. The value is the unformatted phone number in E.164 format. * @deprecated Since v2.0.0. Use `v-model` or `update:model-value` instead. */ (e: 'input', value: string): void; /** * Emitted when the value changes. The value is the unformatted phone number in E.164 format. */ (e: 'update:model-value', value: string): void; /** * Emitted when the selected country is updated. */ (e: 'update:selected-country', country: CountryCode): void; /** * Emitted when the phone number's validity changes. * The payload is a boolean indicating whether the number is valid. */ (e: 'input:valid', value: boolean): void; }>(); const inputField = ref(null); const displayValue = ref(''); const internalValue = ref(''); const insertedChar = ref(''); const { emitValue } = useFormFieldEmits<string>(emit); provide(INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, 'bento-input-field-phone-number'); const computedPlaceholder = computed(() => { if (props.placeholder) { return props.placeholder; } if (props.selectedCountry) { return getPhoneNumberPlaceholder(props.selectedCountry); } return ''; }); const isDropdownReadonly = computed(() => props.dropdown?.readonly); const countryItems = computed(() => getCountriesDropdownItems(props.countries, locale.value)); const dropdown = computed<BentoInputDropdownProps>(() => ({ items: countryItems.value, modelValue: props.selectedCountry, readonly: isDropdownReadonly.value, dynamicFiltering: props.dynamicFiltering, 'aria-label': t('countrySelectorDropdown'), })); const onBlur = () => emit('blur'); const onUpdateModelValue = async (value: string) => { const inputEl = inputField.value.$refs.inputFieldElement; let formatter: AsYouType; // Check if the input value starts with '+' or '00', indicating it includes a country code. if (value.startsWith('+') || value.startsWith('00')) { // Reset displayValue to ensure changes are always reflected. // For example, if you paste '+1 264 123 4567' multiple times, the display value should always be '1234567'. // Without this, the displayValue will be '1234567' the first time but subsequently stay as '+1 264 123 4567'. displayValue.value = ''; // Normalize the value by replacing '00' with '+'. const normalizedValue = value.startsWith('00') ? `+${value.slice(2)}` : value; // Create a new AsYouType formatter to format the number as it's typed. formatter = new AsYouType(); formatter.input(normalizedValue); // Get the country code from the formatted number. const country = formatter.getCountry(); // Check if the extracted country code is valid and included in the allowed countries. const isValidCountry = country && props.countries.includes(country); // If the dropdown is readonly, determine whether to strip the region code based on the selected country. if (isDropdownReadonly.value) { displayValue.value = props.selectedCountry === country ? stripRegionCode(formatter.getNationalNumber(), country) : value; } else { // Emit an event to update the selected country, if valid. Otherwise, set it to null. emit('update:selected-country', isValidCountry ? country : null); // Strip the region code from the formatted number if the country is valid. displayValue.value = isValidCountry ? stripRegionCode(formatter.getNationalNumber(), country) : value; } const numberValue = formatter.getNumberValue() ?? ''; const formattedPhoneCode = getFormattedPhoneCode(country); const shouldClearInput = isValidCountry && numberValue.startsWith(formattedPhoneCode) && numberValue.length <= formattedPhoneCode.length; internalValue.value = shouldClearInput ? '' : numberValue; emitValue(internalValue.value); } else { // If the input doesn't start with a country code, format the number based on the selected country. formatter = new AsYouType(props.selectedCountry); const formatted = formatter.input(appendRegionCode(value, props.selectedCountry)); // Prevent repetition of the region code and get the unformatted number. const numberValue = formatter.getNumberValue(); const unformattedNumber = numberValue ? preventRegionCodeRepetition(value, numberValue, props.selectedCountry) : value; // Emit raw input when parsing fails i.e. number do not have country code // Emit the unformatted number value and update the display value with the stripped region code. internalValue.value = unformattedNumber; emitValue(internalValue.value); displayValue.value = stripRegionCode(formatted, props.selectedCountry); } await nextTick(); // Re-parse the number to ensure correct formatting, e.g., adding a leading '0' for NL numbers if valid and `hasDefaultRegionCode` returns false. if (formatter.isValid() && props.selectedCountry && !hasDefaultRegionCode(props.selectedCountry)) { const { displayNumber, isValid } = parsePhoneAndCountry( displayValue.value?.toString(), props.selectedCountry, props.countries ); displayValue.value = displayNumber; emitValidation(isValid); } else { emitValidation(formatter.isValid()); } handleInputCursor(inputEl, displayValue.value.toString(), insertedChar.value); }; const emitValidation = (isValid: boolean) => { // Numbers shorter than 5 digits are never valid phone numbers. // `libphonenumber-js` rejects numbers under 5 digits (`TOO_SHORT`). // We don’t call that check directly here, but use the same value for consistency. const MIN_PHONE_NUMBER_LENGTH_FOR_VALIDATION = 5; const numberLength = displayValue.value?.toString().replace(/\D/g, '')?.length ?? 0; const isValidNumberLength = numberLength >= MIN_PHONE_NUMBER_LENGTH_FOR_VALIDATION; emit('input:valid', isValidNumberLength && isValid); }; const onKeydown = event => { if (/^[a-zA-Z]$/.test(event.key) && !event.metaKey && !event.ctrlKey) { event.preventDefault(); return; } insertedChar.value = event.key; }; const onCountryChange = (country: CountryCode) => { emit('update:selected-country', country); const isUnformatted = displayValue.value?.toString().startsWith('+'); if (hasDefaultRegionCode(country) && !isUnformatted) { let formatter = new AsYouType(country); const formatted = formatter.input(appendRegionCode(displayValue.value?.toString(), country)); const unformattedNumber = preventRegionCodeRepetition(displayValue.value?.toString(), formatter.getNumberValue(), country) ?? ''; internalValue.value = unformattedNumber; emitValue(internalValue.value); displayValue.value = stripRegionCode(formatted, country); emitValidation(formatter.isValid()); } else { const { displayNumber, unformattedNumber, isValid } = parsePhoneAndCountry( displayValue.value?.toString(), country, props.countries ); internalValue.value = unformattedNumber ?? ''; emitValue(internalValue.value); displayValue.value = displayNumber; emitValidation(isValid); } }; watch( () => [props.value, props.modelValue], () => { const newValue = props.modelValue ?? props.value; if (newValue === internalValue.value) { return; } const { displayNumber, country, isValid } = parsePhoneAndCountry( newValue?.toString(), props.selectedCountry, props.countries ); if (country && country !== props.selectedCountry && !isDropdownReadonly.value) { emit('update:selected-country', country); } displayValue.value = stripRegionCode(displayNumber, country); emitValidation(isValid); }, { immediate: true } ); if (props.value) { deprecate( 'BentoInputFieldPhoneNumber "value" property', `The use of "value" prop in "BentoInputFieldPhoneNumber" is no longer supported. Use the "v-model" or "model-value" property instead.`, '2.0.0' ); } </script> <script lang="ts"> /** * Input field phone number can be used to display phone numbers that come with a country code. * * @example * import { BentoInputFieldPhoneNumber } from '@adyen/bento-vue2'; * * export default { * components: { BentoInputFieldPhoneNumber }, * template: ` * <bento-input-field-phone-number * label="Phone number" * :countries="['NL', 'BE']" * model-value="0612345678" * selected-country="NL" * /> * ` * } */ export default { i18n: { messages }, model: { prop: 'modelValue' }, }; </script> <style lang="scss" scoped src="./input-field-phone-number.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <bento-input-field class="b-input-time" :condensed="props.condensed" :disabled="props.disabled" :error-message="
|
|
1
|
+
<template> <bento-input-field class="b-input-time" :type="inputType" :step="isTouchDevice ? 1 : undefined" :condensed="props.condensed" :disabled="props.disabled" :error-message="inputErrorMessage" :label="props.label" :model-value="inputModelValue" :optional="props.optional" :placeholder="computedPlaceholder" :readonly="props.readonly" :required="props.required" :tooltip-text="props.tooltipText" v-bind="$attrs" @update:model-value="onInput" @keydown="onKeyDown" > <template v-if="hasSlot('default')" #default> <slot name="default"></slot> </template> <template v-if="!isTouchDevice" #description> <slot name="description">{{ props.description ? `${props.description} ${t('timeFormatDescription')}` : t('timeFormatDescription') }}</slot> </template> </bento-input-field> </template> <script setup lang="ts"> // Components import { BentoInputField } from '@/components/input-field'; import { INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY } from '@/components/input-field/input-field.keys'; // Utils import { debounce } from '@/utils/ts/debounce'; import { useI18n } from '@/utils/ts/i18n'; import { formatTimeInputValue, isValidTimeString, validateTimeInput } from '@/utils/ts/time-input'; // Types import type { BentoInputFieldValue } from '@/components/input-field/input-field.types'; import { type BentoInputTimeProps } from './input-time.types'; // Localization import messages from './messages.json'; import { computed, provide, ref, useSlots, watch } from 'vue'; import { useHasSlot, useTouchDevice } from '@/composables'; const VALIDATION_DEBOUNCE_TIME = 300; provide(INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, 'bento-input-time'); const slots = useSlots(); const hasSlot = useHasSlot(slots); const { isTouchDevice } = useTouchDevice(); type MessageSchema = (typeof messages)['en-US']; const { t } = useI18n<{ message: MessageSchema }>({ messages }); const props = withDefaults(defineProps<BentoInputTimeProps>(), { condensed: false, description: undefined, disabled: false, errorMessage: null, label: '', modelValue: undefined, optional: false, placeholder: '', readonly: false, required: false, tooltipText: null, }); const emit = defineEmits<{ /** * Emitted when the component's model changes */ (e: 'update:model-value', value: BentoInputFieldValue): void; /** * Emitted when a key is pressed down */ (e: 'keydown', event: KeyboardEvent): void; /** * Emitted when the time value passes validation (complete HH:MM:SS format) */ (e: 'input:valid'): void; /** * Emitted when the time value fails validation */ (e: 'input:error'): void; }>(); const internalError = ref(false); const computedErrorMessage = computed(() => { if (props.errorMessage) { return props.errorMessage; } return internalError.value ? t('invalidTimeFormat') : null; }); const inputErrorMessage = computed(() => (isTouchDevice.value ? props.errorMessage : computedErrorMessage.value)); const inputType = computed(() => (isTouchDevice.value ? 'time' : undefined)); const inputModelValue = computed(() => props.modelValue); const computedPlaceholder = computed(() => (isTouchDevice ? undefined : props.placeholder)); const validateTime = (timeText: string) => { if (!timeText) { internalError.value = false; return; } const isValid = isValidTimeString(timeText); internalError.value = !isValid; if (isValid) { emit('input:valid'); } else { emit('input:error'); } }; const validateTimeWithDebounce = debounce(validateTime, VALIDATION_DEBOUNCE_TIME); // Watch for external modelValue changes (e.g., when parent resets the value) watch( () => props.modelValue, newValue => { const timeText = String(newValue ?? ''); // Clear the error only when the new value is a complete, valid time string if (timeText.length === 8 && isValidTimeString(timeText)) { internalError.value = false; } } ); const onInput = (value: BentoInputFieldValue) => { if (props.disabled || props.readonly) { return; } if (isTouchDevice.value) { emit('update:model-value', String(value ?? '')); emit('input:valid'); } else { const formatted = formatTimeInputValue(String(value ?? '')); emit('update:model-value', formatted); validateTimeWithDebounce(formatted); } }; const onKeyDown = (event: KeyboardEvent) => { if (!isTouchDevice.value) { validateTimeInput(event); } emit('keydown', event); }; </script> <script lang="ts"> /** * Input time component for entering time values in HH:MM:SS format. * This is an extension of the `bento-input-field` component with built-in * time formatting and validation logic. * * @usage * import { BentoInputTime } from '@adyen/bento-vue2'; * * const timeValue = ref(''); * * export default { * components: { BentoInputTime }, * template: ` * <bento-input-time * label="Start time" * :model-value="timeValue" * @update:model-value="newValue => timeValue.value = newValue" * /> * ` * } */ export default { i18n: { messages }, name: 'bento-input-time', inheritAttrs: false, model: { prop: 'modelValue' }, }; </script> <style lang="scss" scoped src="./input-time.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import { type Ref, type UnwrapNestedRefs } from 'vue'; import type { Day } from 'date-fns'; export { CalendarMonthFirstDayOfWeek as CalendarFirstDayOfWeek } from './components/calendar-month/calendar-month.types'; export type { CalendarMonthIsDateDisabled as CalendarIsDateDisabled } from './components/calendar-month/calendar-month.types'; export { CalendarYearPosition as CalendarPosition } from './components/calendar-year/calendar-year.types'; export type CalendarGranularityType = 'daily' | 'weekly' | 'monthly' | 'quarterly'; export interface CalendarGranularityConfig<T extends CalendarGranularityType = CalendarGranularityType> { type: T; earliest?: Date; latest?: Date; disabled?: boolean; } export interface CalendarRangeDateValue { startDate: Date; endDate: Date; granularity?: CalendarGranularityType; range?: string; } export type CalendarSingleDateValue = Date; export type CalendarValue = CalendarSingleDateValue | CalendarRangeDateValue; interface UseCalendarPaneProps { isDateDisabled?: (date: Date) => boolean; maxRange?: Ref<number>; selectedDate: Ref<CalendarValue>; granularity?: Ref<CalendarGranularityConfig>; firstDayOfWeek: Day; min?: Ref<Date>; max?: Ref<Date>; } export type UseCalendarPane = ( propRefs: UseCalendarPaneProps, emit ) => { internalValue: UnwrapNestedRefs<CalendarRangeDateValue>; onDateInput: (date: Date) => void; onMouseOver: (date: Date) => void; }; export enum CalendarEvent { INPUT = 'input', START_DATE_SELECTED = 'start-date-selected', END_DATE_SELECTED = 'end-date-selected', } /** * Private types */ export enum CalendarVariant { DAY = 'day', MONTH = 'month', }
|
|
1
|
+
import { type Ref, type UnwrapNestedRefs } from 'vue'; import type { Day } from 'date-fns'; export { CalendarMonthFirstDayOfWeek as CalendarFirstDayOfWeek } from './components/calendar-month/calendar-month.types'; export type { CalendarMonthIsDateDisabled as CalendarIsDateDisabled } from './components/calendar-month/calendar-month.types'; export { CalendarYearPosition as CalendarPosition } from './components/calendar-year/calendar-year.types'; export type CalendarGranularityType = 'daily' | 'weekly' | 'monthly' | 'quarterly'; export interface CalendarGranularityConfig<T extends CalendarGranularityType = CalendarGranularityType> { type: T; earliest?: Date; latest?: Date; disabled?: boolean; maxRange?: number; } export interface CalendarRangeDateValue { startDate: Date; endDate: Date; granularity?: CalendarGranularityType; range?: string; } export type CalendarSingleDateValue = Date; export type CalendarValue = CalendarSingleDateValue | CalendarRangeDateValue; interface UseCalendarPaneProps { isDateDisabled?: (date: Date) => boolean; maxRange?: Ref<number>; selectedDate: Ref<CalendarValue>; granularity?: Ref<CalendarGranularityConfig>; firstDayOfWeek: Day; min?: Ref<Date>; max?: Ref<Date>; } export type UseCalendarPane = ( propRefs: UseCalendarPaneProps, emit ) => { internalValue: UnwrapNestedRefs<CalendarRangeDateValue>; onDateInput: (date: Date) => void; onMouseOver: (date: Date) => void; }; export enum CalendarEvent { INPUT = 'input', START_DATE_SELECTED = 'start-date-selected', END_DATE_SELECTED = 'end-date-selected', } /** * Private types */ export enum CalendarVariant { DAY = 'day', MONTH = 'month', }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/** * Defines how to measure and adjust a date range for a specific granularity (daily, weekly, etc.). * Used by `clampToMaxRange` to enforce maximum range limits in a granularity-agnostic way. */ export interface GranularityClampConfig { /** Returns how many units (days, weeks, months, quarters) fit between two dates. */ getCount: (end: Date, start: Date) => number; /** Subtracts a number of units from a date (e.g. `subDays`, `subWeeks`). */ sub: (date: Date, amount: number) => Date; /** Adds a number of units to a date (e.g. `addDays`, `addWeeks`). */ add: (date: Date, amount: number) => Date; /** Snaps a date to the start of its unit boundary (e.g. `startOfMonth`). Only needed for granularities whose units don't align to individual days. */ snapStart?: (date: Date) => Date; /** Snaps a date to the end of its unit boundary (e.g. `endOfMonth`). Only needed for granularities whose units don't align to individual days. */ snapEnd?: (date: Date) => Date; }
|