@adyen/bento-mcp 0.2.1 → 0.4.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/SKILL.md +56 -6
- package/dist/assets/components/data-grid/components/data-grid-cell/data-grid-cell.vue +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-columns/data-grid-columns.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.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/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/filter-bar/components/date-filter/date-filter.vue +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/input-field/input-field.vue +1 -1
- package/dist/assets/components/input-time/input-time.docs.mdx +52 -0
- package/dist/assets/components/input-time/input-time.stories.ts +1 -0
- package/dist/assets/components/input-time/input-time.types.ts +1 -0
- package/dist/assets/components/input-time/input-time.vue +1 -0
- package/dist/assets/components/internal/listbox/components/listbox-multi-select/listbox-multi-select.vue +1 -1
- package/dist/assets/components/secondary-nav/components/secondary-nav-category/secondary-nav-category.vue +1 -1
- package/dist/assets/components/secondary-nav/components/secondary-nav-item/secondary-nav-item.vue +1 -1
- package/dist/assets/components/secondary-nav/secondary-nav.docs.mdx +32 -2
- package/dist/assets/components/secondary-nav/secondary-nav.stories.ts +1 -1
- package/dist/assets/components/secondary-nav/secondary-nav.types.ts +1 -1
- package/dist/assets/components/secondary-nav/secondary-nav.vue +1 -1
- package/dist/assets/components.json +101 -99
- package/dist/assets/deprecations.md +196 -196
- package/dist/assets/index.ts +1 -1
- package/dist/assets/tailwind/README.md +45 -5
- package/dist/main.js +2 -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
|
@@ -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
|
-
<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="computedRootAttributes" 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 computedRootAttributes = computed(() => { return Object.fromEntries(Object.entries(attrs).filter(([key]) => !key.startsWith('aria-'))); }); 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(() => 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" />
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Canvas, Meta, Source } from '@storybook/blocks';
|
|
2
|
+
import * as InputTimeStories from './input-time.stories';
|
|
3
|
+
|
|
4
|
+
<Meta of={InputTimeStories} />
|
|
5
|
+
|
|
6
|
+
# Input Time
|
|
7
|
+
|
|
8
|
+
The input time component is designed for entering time values in `HH:MM:SS` format. It is an extension of the
|
|
9
|
+
`bento-input-field` component with built-in time character validation and auto-formatting logic.
|
|
10
|
+
|
|
11
|
+
<Canvas of={InputTimeStories.Default} />
|
|
12
|
+
|
|
13
|
+
## Use Cases
|
|
14
|
+
|
|
15
|
+
Use this component if you need to collect a time of day value from a user.
|
|
16
|
+
|
|
17
|
+
### Do not use
|
|
18
|
+
|
|
19
|
+
Avoid using this component for:
|
|
20
|
+
|
|
21
|
+
- Date inputs — use `bento-input-field` with an appropriate format or the `bento-date-picker` component.
|
|
22
|
+
- General text inputs — use `bento-input-field` instead.
|
|
23
|
+
|
|
24
|
+
## Behaviour
|
|
25
|
+
|
|
26
|
+
### Auto-formatting
|
|
27
|
+
|
|
28
|
+
As the user types, the component automatically inserts `:` separators to produce a well-formed `HH:MM:SS` string. For
|
|
29
|
+
example, typing `1` produces `1`, then `12` produces `12:`, then `1230` produces `12:30:`, and so on.
|
|
30
|
+
|
|
31
|
+
### Key validation
|
|
32
|
+
|
|
33
|
+
Non-numeric characters (except `:` at valid positions and control keys such as `Backspace`, `ArrowLeft`, `ArrowRight`,
|
|
34
|
+
and `Tab`) are silently blocked via the `keydown` handler.
|
|
35
|
+
|
|
36
|
+
## Modifiers
|
|
37
|
+
|
|
38
|
+
### Error state
|
|
39
|
+
|
|
40
|
+
Use the `error-message` prop to display an inline error message and apply error styling:
|
|
41
|
+
|
|
42
|
+
```html
|
|
43
|
+
<bento-input-time label="Start time" error-message="Invalid time format" />
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Disabled
|
|
47
|
+
|
|
48
|
+
Renders the time input in a disabled state.
|
|
49
|
+
|
|
50
|
+
### Readonly
|
|
51
|
+
|
|
52
|
+
Renders the time input in a readonly state.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import { action } from '@storybook/addon-actions'; import { isVue2 } from 'vue-demi'; import { ref } from 'vue'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import BentoInputTime from './input-time.vue'; import type { Meta, StoryObj } from '@storybook/vue'; import defaultCode from './__tests__/input-time-default-example.vue?raw'; const meta: Meta = { title: 'Input Time', component: BentoInputTime, }; const DEFAULT_PROPS = { label: 'Input time', }; export default meta; type Story = StoryObj<typeof BentoInputTime>; export const Default: Story = { args: DEFAULT_PROPS, render: (_args, { argTypes }) => ({ components: { BentoInputTime }, props: Object.keys(argTypes), setup(props) { const modelValue = ref(''); const onUpdateModelValue = (value: string) => { modelValue.value = value; action('update:model-value')(value); }; return { args: isVue2 ? props : _args, modelValue, onUpdateModelValue, onInputValid: action('input:valid'), onInputError: action('input:error'), onInputKeydown: action('keydown'), }; }, template: ` <bento-input-time v-bind="args" :model-value="modelValue" @update:model-value="onUpdateModelValue" @input:valid="onInputValid" @input:error="onInputError" @keydown="onInputKeydown" /> `, }), parameters: storybookDocsParameter(defaultCode), };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import type { BentoInputFieldValue } from '@/components/input-field/input-field.types'; import { type FormField } from '../../types/form-field'; export interface BentoInputTimeProps extends FormField { /** * Reduces height of the input */ condensed?: boolean; /** * Text displayed as the value of the input field */ modelValue?: BentoInputFieldValue; }
|
|
@@ -0,0 +1 @@
|
|
|
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
|
-
<template> <div ref="listboxRef" class="b-listbox-multi-select"> <!-- Selected options --> <div v-if="showSelectedItems"> <ul v-bento-keyboard-navigation-directive class="b-listbox-multi-select__chips" v-on="chipsSectionListeners" > <li v-for="item in selectedOptions" :key="item.value"> <bento-chip ref="selectedListboxItemsRef" :disabled="isOptionDisabled(item)" :label="item.label" condensed @click="item.items ? unselectAllCategory(item) : toggleCheckboxSelection('select', item)" /> </li> </ul> <hr class="b-listbox-multi-select__divider" aria-hidden="true" /> </div> <div v-bento-keyboard-navigation-directive v-bind="ariaAttributes" :role="computedRole" v-on="listboxListeners"> <!-- Select all --> <bento-listbox-option v-if="showSelectAllCheckbox" ref="selectAllListboxItemsRef" class="b-listbox-multi-select__option" :tabindex="focusedIndex === 0 ? 0 : -1" :aria-selected="ariaCheckedComputed !== 'false'" :aria-checked="ariaCheckedComputed" :role="computedOptionRole" @click="handleSelectAll" @enter-pressed="handleSelectAll" @space-pressed.prevent="handleSelectAll" > <decorative-checkbox :checked="isAllSelected" :indeterminate="isIndeterminate" /> <bento-typography el="span"> {{ selectAllCheckboxText }} </bento-typography> </bento-listbox-option> <hr v-if="showSelectAllCheckbox" class="b-listbox-multi-select__divider" aria-hidden="true" /> <!-- Available options --> <listbox-multi-select-options ref="listboxItemsRef" :items="items" :is-option-disabled="isOptionDisabled" :is-multi-level="isMultiLevelSelect" :static-categories="staticCategories" :searching="searching" :selected-items-list="selectedItemsList" :focused-index="focusedIndex" :items-index="computedItemsIndex" :role="computedOptionRole" @toggle-checkbox-selection="toggleCheckboxSelection('select', $event)" @select-category="selectAllCategory" @unselect-category="unselectAllCategory" @toggle-category-visibility="toggleCategoryVisibility" @space-pressed="toggleCheckboxSelection(BentoListboxEvent.SPACE, $event)" @tab-pressed="toggleCheckboxSelection(BentoListboxEvent.TAB, $event)" @enter-pressed="toggleCheckboxSelection(BentoListboxEvent.ENTER, $event)" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </listbox-multi-select-options> </div> </div> </template> <script setup lang="ts"> import { computed, type HTMLAttributes, nextTick, ref, toRef, useAttrs, useSlots, watch } from 'vue'; import { isVue2 } from 'vue-demi'; import { DecorativeCheckbox } from '@/internal/decorative-checkbox'; import { BentoTypography } from '@/components/typography'; import { BentoChip } from '@/components/chip'; import { BentoListboxOption } from '../listbox-option'; import { ListboxMultiSelectOptions } from './components/listbox-multi-select-options'; import { useCheckboxSelectAll } from '@/composables'; import { useListboxKeyboardNavigation } from '../../composables/useListboxKeyboardNavigation'; import { useMultiLevelItems } from '../../composables/use-multi-level-items/use-multi-level-items'; import { useVisibleDomOptions } from '@/internal/listbox/composables/use-visible-dom-options/use-visible-dom-options'; import { useI18n } from '@/utils/ts/i18n'; import { BentoKeyboardNavigationDirective as vBentoKeyboardNavigationDirective } from '@/directives'; import { BentoListboxEvent, type BentoListboxMultiSelectValue, type BentoListboxOptionItem, type BentoListboxOptions, type BentoListboxValue, } from '@/types/listbox'; import { type Booleanish } from '@/types/prop-types'; import { BentoKeyboardNavigationKeyDownEvent } from '@/types/keyboard-navigation'; import { type ListboxMultiSelectProps } from './listbox-multi-select.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const props = withDefaults(defineProps<ListboxMultiSelectProps>(), { hasMoreItems: false, isOptionDisabled: isVue2 ? () => () => false : () => false, lazyLoadType: 'none', searching: false, staticCategories: false, }); const emit = defineEmits<{ /** * Emits when the selection changes */ (e: 'select', value: Array<BentoListboxOptionItem>): void; }>(); const { t, n } = useI18n<{ message: MessageSchema }>({ messages }); const slots = useSlots(); const attrs = useAttrs(); const ariaAttributes = computed( () => ({ 'aria-label': attrs['aria-label']?.toString() as string, 'aria-busy': attrs['aria-busy']?.toString() as Booleanish, 'aria-atomic': attrs['aria-atomic']?.toString() as Booleanish, 'aria-live': attrs['aria-live']?.toString() as string, 'aria-multiselectable': true, }) as HTMLAttributes ); const listboxRef = ref(null); const selectAllListboxItemsRef = ref(null); const selectedListboxItemsRef = ref(null); const listboxItemsRef = ref(null); const selectedItemsList = computed<Array<BentoListboxValue>>(() => props.selectedValues.map(({ value }) => value)); const { isMultiLevelSelect, flattenedItemsList } = useMultiLevelItems(toRef(props, 'items')); const isTreePattern = computed(() => isMultiLevelSelect.value && !props.staticCategories); const computedRole = computed(() => (isTreePattern.value ? 'tree' : 'listbox')); const computedOptionRole = computed(() => (isTreePattern.value ? 'treeitem' : 'option')); const optionStates = (option: BentoListboxOptionItem) => { const isDisabled = props.isOptionDisabled(option); const isPreSelected = (selectedItemsList.value as BentoListboxMultiSelectValue)?.includes(option.value) || false; return { isDisabled, isPreSelected }; }; // Items that are not disabled and pre-selected disabled items const enabledItems = computed(() => { if (props.searching && isMultiLevelSelect) { return props.items; } const fullItemsList = flattenedItemsList.value.filter(option => { const { isDisabled, isPreSelected } = optionStates(option); return (isDisabled && isPreSelected) || !isDisabled; }); props.selectedValues.forEach(selectedValue => { if (!fullItemsList.find(item => item.value === selectedValue.value)) { fullItemsList.push(selectedValue); } }); return fullItemsList; }); const visibleSelectedList = computed( () => enabledItems.value?.filter(option => { if (option.items && isMultiLevelSelect.value && props.searching) { const amountOfSelectedCategoryOptions = option.items.filter(categoryOption => (props.selectedValues ?? []).includes(categoryOption.value) ).length; return amountOfSelectedCategoryOptions === option.items.length; } return selectedItemsList.value?.includes(option.value); // Check for category }) as BentoListboxMultiSelectValue ); const calculateTotalItems = () => { if (isMultiLevelSelect.value && !props.searching) { return flattenedItemsList.value.length; } return props.items.length; }; const selectAllCheckboxText = computed(() => { const possibleSelections = enabledItems.value.length; const total = calculateTotalItems(); const amountSelectedValues = visibleSelectedList.value?.length || 0; if (amountSelectedValues === 0) { return t('selectAll', { total: n(possibleSelections) }); } if (amountSelectedValues === total) { return t('allSelected', { total: n(total) }); } return amountSelectedValues <= total ? t('numberOfTotalSelected', { number: n(amountSelectedValues), total: n(total) }) : t('numbersSelected', { number: n(amountSelectedValues) }); }); const selectAll = (selectableItems: BentoListboxOptions) => { const filterOutDisabledAndNotPreselectedOptions = option => { const { isDisabled, isPreSelected } = optionStates(option); return (isDisabled && isPreSelected) || !isDisabled; }; // Adding or removing this list of values const addOrRemoveList = selectableItems .filter(filterOutDisabledAndNotPreselectedOptions) .filter(item => !selectedItemsList.value.includes(item.value)); // MAKE SURE NOT TO ADD DUPLICATES emit('select', [...props.selectedValues, ...addOrRemoveList]); }; const unselectAll = (selectableItems: BentoListboxOptions) => { const findEnabledUnselectedItems = option => { const { isDisabled, isPreSelected } = optionStates(option); return !(isDisabled && isPreSelected); }; const unselectedItemsList = selectableItems.filter(findEnabledUnselectedItems).map(({ value }) => value); const newSelectedList = props.selectedValues.filter(item => !unselectedItemsList?.includes(item.value)); emit('select', newSelectedList); }; const selectAllItems = () => { selectAll(flattenedItemsList.value); }; const unselectAllItems = () => { unselectAll(flattenedItemsList.value); }; const { isAllSelected, isIndeterminate, toggleSelectAll } = useCheckboxSelectAll( visibleSelectedList, enabledItems, selectAllItems, unselectAllItems ); const ariaCheckedComputed = computed(() => { if (isIndeterminate.value) { return 'mixed'; } return `${isAllSelected.value}`; }); const isSelectAllOn = ref(isAllSelected.value); const handleSelectAll = () => { isSelectAllOn.value = !isSelectAllOn.value; toggleSelectAll(isSelectAllOn.value); }; watch(isAllSelected, () => { isSelectAllOn.value = isAllSelected.value; }); const selectAllCategory = (category: BentoListboxOptionItem) => { selectAll(category.items); }; const unselectAllCategory = (category: BentoListboxOptionItem) => { unselectAll(category.items); }; const toggleCheckboxSelection = (eventName, selectedOption: BentoListboxOptionItem) => { let exists = false; const newSelectedList = props.selectedValues.filter(({ value }) => { if (selectedOption.value === value) { exists = true; return false; } return true; }); if (!exists) { newSelectedList.push(selectedOption); } emit(eventName, newSelectedList); }; const selectedOptions = computed(() => { if (!isMultiLevelSelect.value) { return props.selectedValues; } const isOptionSelected = (option: BentoListboxOptionItem): boolean => (selectedItemsList.value as unknown as Array<string | number>).includes(option.value); const categoryOptionsSelected = (categoryOptions: Array<BentoListboxOptionItem>): boolean => !props.staticCategories && categoryOptions.every(option => isOptionSelected(option)); const optionsFlat = props.items.flatMap(option => option.items && !categoryOptionsSelected(option.items) ? option.items : option ); const options = optionsFlat.filter(option => { if (option.items) { return true; } return isOptionSelected(option); }); return options; }); const showSelectedItems = computed(() => { const isLazyLoad = ['automatic', 'button'].includes(props.lazyLoadType); const hasAllItemsSelected = props.selectedValues?.length > 0 && visibleSelectedList.value?.length !== flattenedItemsList.value?.length; if (isLazyLoad && props.hasMoreItems) { return !!props.selectedValues?.length; } return hasAllItemsSelected; }); const showSelectAllCheckbox = computed(() => { const isLazyLoad = ['automatic', 'button'].includes(props.lazyLoadType); if (!isLazyLoad) { return true; } return !props.hasMoreItems; }); // Keyboard navigation const expandedCategories = ref([]); const { visibleDomOptions, updateVisibleDomOptions } = useVisibleDomOptions( listboxRef, toRef(props, 'items'), computedOptionRole ); const toggleCategoryVisibility = category => { if (expandedCategories.value.includes(category)) { expandedCategories.value = expandedCategories.value.filter(item => item !== category); } else { expandedCategories.value.push(category); } nextTick(() => { updateVisibleDomOptions(); }); }; const focusItem = (nextFocusedItemIndex: number) => { visibleDomOptions.value[nextFocusedItemIndex]?.focus(); }; const { moveToNextElement, moveToPreviousElement, moveToFirstElement, moveToLastElement, preventDefault, focusedIndex, setListboxFocusedIndex, } = useListboxKeyboardNavigation( { items: visibleDomOptions, }, focusItem ); const listboxListeners = { [BentoKeyboardNavigationKeyDownEvent.ARROW_UP]: (event: CustomEvent<KeyboardEvent>) => moveToPreviousElement(event), [BentoKeyboardNavigationKeyDownEvent.ARROW_DOWN]: (event: CustomEvent<KeyboardEvent>) => moveToNextElement(event), [BentoKeyboardNavigationKeyDownEvent.HOME]: moveToFirstElement, [BentoKeyboardNavigationKeyDownEvent.END]: moveToLastElement, }; defineExpose({ /** * Method to set the index of the item to be focused on * Used for focus management to be kept in-sync with parent e.g. dropdowns */ setListboxFocusedIndex, }); const chipsSectionListeners = { [BentoKeyboardNavigationKeyDownEvent.ARROW_UP]: preventDefault, [BentoKeyboardNavigationKeyDownEvent.ARROW_DOWN]: preventDefault, }; const computedItemsIndex = computed(() => { let itemIndex = 1; const indexMap = {}; props.items.forEach(item => { let subItemsVisible = expandedCategories.value.includes(item.value); if (item.items) { if (props.staticCategories) { subItemsVisible = true; } else { indexMap[item.value] = itemIndex++; } if (subItemsVisible) { item.items.forEach(subItem => (indexMap[subItem.value] = itemIndex++)); } } else { indexMap[item.value] = itemIndex++; } }); return indexMap; }); </script> <script lang="ts"> /** * Listbox multi-select options list * * @example * import { BentoListboxMultiSelect } from '@adyen/bento-vue2'; * * export default { * components: { BentoListboxMultiSelect }, * template: ` * <bento-listbox-multi-select * :items="[{ label: 'Option 1', value: 'option-1' }]" * :selected-values="['option-1']" * /> * ` * } */ export default { i18n: { messages } }; </script> <style lang="scss" scoped src="./listbox-multi-select.scss" />
|
|
1
|
+
<template> <div ref="listboxRef" class="b-listbox-multi-select"> <!-- Selected options --> <div v-if="showSelectedItems"> <ul v-bento-keyboard-navigation-directive class="b-listbox-multi-select__chips" v-on="chipsSectionListeners" > <li v-for="item in selectedOptions" :key="item.value"> <bento-chip ref="selectedListboxItemsRef" :disabled="isOptionDisabled(item)" :label="item.label" condensed @click="item.items ? unselectAllCategory(item) : toggleCheckboxSelection('select', item)" /> </li> </ul> <hr class="b-listbox-multi-select__divider" aria-hidden="true" /> </div> <div v-bento-keyboard-navigation-directive v-bind="ariaAttributes" :role="computedRole" v-on="listboxListeners"> <!-- Select all --> <bento-listbox-option v-if="showSelectAllCheckbox" ref="selectAllListboxItemsRef" class="b-listbox-multi-select__option" :tabindex="focusedIndex === 0 ? 0 : -1" :aria-selected="ariaCheckedComputed !== 'false'" :aria-checked="ariaCheckedComputed" :role="computedOptionRole" @click="handleSelectAll" @enter-pressed="handleSelectAll" @space-pressed.prevent="handleSelectAll" > <decorative-checkbox :checked="isAllSelected" :indeterminate="isIndeterminate" /> <bento-typography el="span"> {{ selectAllCheckboxText }} </bento-typography> </bento-listbox-option> <hr v-if="showSelectAllCheckbox" class="b-listbox-multi-select__divider" aria-hidden="true" /> <!-- Available options --> <listbox-multi-select-options ref="listboxItemsRef" :items="items" :is-option-disabled="isOptionDisabled" :is-multi-level="isMultiLevelSelect" :static-categories="staticCategories" :searching="searching" :selected-items-list="selectedItemsList" :focused-index="focusedIndex" :items-index="computedItemsIndex" :role="computedOptionRole" @toggle-checkbox-selection="toggleCheckboxSelection('select', $event)" @select-category="selectAllCategory" @unselect-category="unselectAllCategory" @toggle-category-visibility="toggleCategoryVisibility" @space-pressed="toggleCheckboxSelection(BentoListboxEvent.SPACE, $event)" @tab-pressed="toggleCheckboxSelection(BentoListboxEvent.TAB, $event)" @enter-pressed="toggleCheckboxSelection(BentoListboxEvent.ENTER, $event)" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </listbox-multi-select-options> </div> </div> </template> <script setup lang="ts"> import { computed, type HTMLAttributes, nextTick, ref, toRef, useAttrs, useSlots, watch } from 'vue'; import { isVue2 } from 'vue-demi'; import { DecorativeCheckbox } from '@/internal/decorative-checkbox'; import { BentoTypography } from '@/components/typography'; import { BentoChip } from '@/components/chip'; import { BentoListboxOption } from '../listbox-option'; import { ListboxMultiSelectOptions } from './components/listbox-multi-select-options'; import { useCheckboxSelectAll } from '@/composables'; import { useListboxKeyboardNavigation } from '../../composables/useListboxKeyboardNavigation'; import { useMultiLevelItems } from '../../composables/use-multi-level-items/use-multi-level-items'; import { useVisibleDomOptions } from '@/internal/listbox/composables/use-visible-dom-options/use-visible-dom-options'; import { useI18n } from '@/utils/ts/i18n'; import { BentoKeyboardNavigationDirective as vBentoKeyboardNavigationDirective } from '@/directives'; import { BentoListboxEvent, type BentoListboxMultiSelectValue, type BentoListboxOptionItem, type BentoListboxOptions, type BentoListboxValue, } from '@/types/listbox'; import { type Booleanish } from '@/types/prop-types'; import { BentoKeyboardNavigationKeyDownEvent } from '@/types/keyboard-navigation'; import { type ListboxMultiSelectProps } from './listbox-multi-select.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const props = withDefaults(defineProps<ListboxMultiSelectProps>(), { hasMoreItems: false, isOptionDisabled: isVue2 ? () => () => false : () => false, lazyLoadType: 'none', searching: false, staticCategories: false, }); const emit = defineEmits<{ /** * Emits when the selection changes */ (e: 'select', value: Array<BentoListboxOptionItem>): void; }>(); const { t, n } = useI18n<{ message: MessageSchema }>({ messages }); const slots = useSlots(); const attrs = useAttrs(); const ariaAttributes = computed( () => ({ 'aria-label': attrs['aria-label']?.toString() as string, 'aria-busy': attrs['aria-busy']?.toString() as Booleanish, 'aria-atomic': attrs['aria-atomic']?.toString() as Booleanish, 'aria-live': attrs['aria-live']?.toString() as string, 'aria-multiselectable': true, }) as HTMLAttributes ); const listboxRef = ref(null); const selectAllListboxItemsRef = ref(null); const selectedListboxItemsRef = ref(null); const listboxItemsRef = ref(null); const selectedItemsList = computed<Array<BentoListboxValue>>(() => props.selectedValues.map(({ value }) => value)); const { isMultiLevelSelect, flattenedItemsList } = useMultiLevelItems(toRef(props, 'items')); const isTreePattern = computed(() => isMultiLevelSelect.value && !props.staticCategories); const computedRole = computed(() => (isTreePattern.value ? 'tree' : 'listbox')); const computedOptionRole = computed(() => (isTreePattern.value ? 'treeitem' : 'option')); const optionStates = (option: BentoListboxOptionItem) => { const isDisabled = props.isOptionDisabled(option); const isPreSelected = (selectedItemsList.value as BentoListboxMultiSelectValue)?.includes(option.value) || false; return { isDisabled, isPreSelected }; }; // Items that are not disabled and pre-selected disabled items const enabledItems = computed(() => { if (props.searching && isMultiLevelSelect) { return props.items; } const fullItemsList = flattenedItemsList.value.filter(option => { const { isDisabled, isPreSelected } = optionStates(option); return (isDisabled && isPreSelected) || !isDisabled; }); props.selectedValues.forEach(selectedValue => { if (!fullItemsList.find(item => item.value === selectedValue.value)) { fullItemsList.push(selectedValue); } }); return fullItemsList; }); const visibleSelectedList = computed( () => enabledItems.value?.filter(option => { if (option.items && isMultiLevelSelect.value && props.searching) { const amountOfSelectedCategoryOptions = option.items.filter(categoryOption => (props.selectedValues ?? []).includes(categoryOption.value) ).length; return amountOfSelectedCategoryOptions === option.items.length; } return selectedItemsList.value?.includes(option.value); // Check for category }) as BentoListboxMultiSelectValue ); const calculateTotalItems = () => { if (isMultiLevelSelect.value && !props.searching) { return flattenedItemsList.value.length; } return props.items.length; }; const selectAllCheckboxText = computed(() => { const possibleSelections = enabledItems.value.length; const total = calculateTotalItems(); const amountSelectedValues = visibleSelectedList.value?.length || 0; if (amountSelectedValues === 0) { return t('selectAll', { total: n(possibleSelections) }); } if (amountSelectedValues === total) { return t('allSelected', { total: n(total) }); } return amountSelectedValues <= total ? t('numberOfTotalSelected', { number: n(amountSelectedValues), total: n(total) }) : t('numbersSelected', { number: n(amountSelectedValues) }); }); const selectAll = (selectableItems: BentoListboxOptions) => { const filterOutDisabledAndNotPreselectedOptions = option => { const { isDisabled, isPreSelected } = optionStates(option); return (isDisabled && isPreSelected) || !isDisabled; }; // Adding or removing this list of values const addOrRemoveList = selectableItems .filter(filterOutDisabledAndNotPreselectedOptions) .filter(item => !selectedItemsList.value.includes(item.value)); // MAKE SURE NOT TO ADD DUPLICATES emit('select', [...props.selectedValues, ...addOrRemoveList]); }; const unselectAll = (selectableItems: BentoListboxOptions) => { const findEnabledUnselectedItems = option => { const { isDisabled, isPreSelected } = optionStates(option); return !(isDisabled && isPreSelected); }; const unselectedItemsList = selectableItems.filter(findEnabledUnselectedItems).map(({ value }) => value); const newSelectedList = props.selectedValues.filter(item => !unselectedItemsList?.includes(item.value)); emit('select', newSelectedList); }; const selectAllItems = () => { selectAll(flattenedItemsList.value); }; const unselectAllItems = () => { unselectAll(flattenedItemsList.value); }; const { isAllSelected, isIndeterminate, toggleSelectAll } = useCheckboxSelectAll( visibleSelectedList, enabledItems, selectAllItems, unselectAllItems ); const ariaCheckedComputed = computed(() => { if (isIndeterminate.value) { return 'mixed'; } return `${isAllSelected.value}`; }); const isSelectAllOn = ref(isAllSelected.value); const handleSelectAll = () => { isSelectAllOn.value = !isSelectAllOn.value; toggleSelectAll(isSelectAllOn.value); }; watch(isAllSelected, () => { isSelectAllOn.value = isAllSelected.value; }); const selectAllCategory = (category: BentoListboxOptionItem) => { selectAll(category.items); }; const unselectAllCategory = (category: BentoListboxOptionItem) => { unselectAll(category.items); }; const toggleCheckboxSelection = (eventName, selectedOption: BentoListboxOptionItem) => { let exists = false; const newSelectedList = props.selectedValues.filter(({ value }) => { if (selectedOption.value === value) { exists = true; return false; } return true; }); if (!exists) { newSelectedList.push(selectedOption); } emit(eventName, newSelectedList); }; const selectedOptions = computed(() => { if (!isMultiLevelSelect.value) { return props.selectedValues; } const isOptionSelected = (option: BentoListboxOptionItem): boolean => (selectedItemsList.value as unknown as Array<string | number>).includes(option.value); const categoryOptionsSelected = (categoryOptions: Array<BentoListboxOptionItem>): boolean => !props.staticCategories && categoryOptions.every(option => isOptionSelected(option)); const optionsFlat = props.items.flatMap(option => option.items && !categoryOptionsSelected(option.items) ? option.items : option ); const options = optionsFlat.filter(option => { if (option.items) { return true; } return isOptionSelected(option); }); return options; }); const showSelectedItems = computed(() => { const isLazyLoad = ['automatic', 'button'].includes(props.lazyLoadType); const hasAllItemsSelected = props.selectedValues?.length > 0 && visibleSelectedList.value?.length !== flattenedItemsList.value?.length; if (isLazyLoad && props.hasMoreItems) { return !!props.selectedValues?.length; } return hasAllItemsSelected; }); const showSelectAllCheckbox = computed(() => { const isLazyLoad = ['automatic', 'button'].includes(props.lazyLoadType); if (!isLazyLoad) { return true; } return !props.hasMoreItems; }); // Keyboard navigation const expandedCategories = ref([]); const { visibleDomOptions, updateVisibleDomOptions } = useVisibleDomOptions( listboxRef, toRef(props, 'items'), computedOptionRole ); const toggleCategoryVisibility = category => { if (expandedCategories.value.includes(category)) { expandedCategories.value = expandedCategories.value.filter(item => item !== category); } else { expandedCategories.value.push(category); } nextTick(() => { updateVisibleDomOptions(); }); }; const focusItem = (nextFocusedItemIndex: number) => { visibleDomOptions.value[nextFocusedItemIndex]?.focus(); }; const { moveToNextElement, moveToPreviousElement, moveToFirstElement, moveToLastElement, preventDefault, focusedIndex, setListboxFocusedIndex, } = useListboxKeyboardNavigation( { items: visibleDomOptions, }, focusItem ); const listboxListeners = { [BentoKeyboardNavigationKeyDownEvent.ARROW_UP]: (event: CustomEvent<KeyboardEvent>) => moveToPreviousElement(event), [BentoKeyboardNavigationKeyDownEvent.ARROW_DOWN]: (event: CustomEvent<KeyboardEvent>) => moveToNextElement(event), [BentoKeyboardNavigationKeyDownEvent.HOME]: moveToFirstElement, [BentoKeyboardNavigationKeyDownEvent.END]: moveToLastElement, }; defineExpose({ /** * Method to set the index of the item to be focused on * Used for focus management to be kept in-sync with parent e.g. dropdowns */ setListboxFocusedIndex, }); const chipsSectionListeners = { [BentoKeyboardNavigationKeyDownEvent.ARROW_UP]: preventDefault, [BentoKeyboardNavigationKeyDownEvent.ARROW_DOWN]: preventDefault, }; const computedItemsIndex = computed(() => { let itemIndex = showSelectAllCheckbox.value ? 1 : 0; const indexMap = {}; props.items.forEach(item => { let subItemsVisible = expandedCategories.value.includes(item.value); if (item.items) { if (props.staticCategories) { subItemsVisible = true; } else { indexMap[item.value] = itemIndex++; } if (subItemsVisible) { item.items.forEach(subItem => (indexMap[subItem.value] = itemIndex++)); } } else { indexMap[item.value] = itemIndex++; } }); return indexMap; }); </script> <script lang="ts"> /** * Listbox multi-select options list * * @example * import { BentoListboxMultiSelect } from '@adyen/bento-vue2'; * * export default { * components: { BentoListboxMultiSelect }, * template: ` * <bento-listbox-multi-select * :items="[{ label: 'Option 1', value: 'option-1' }]" * :selected-values="['option-1']" * /> * ` * } */ export default { i18n: { messages } }; </script> <style lang="scss" scoped src="./listbox-multi-select.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <section class="b-secondary-nav-category" :aria-label="label"> <bento-typography el="h1" class="b-secondary-nav-category__label" variant="caption" stronger>{{ label }}</bento-typography> <ul class="b-secondary-nav__list"> <li v-for="item in items" :key="item.value"> <secondary-nav-item :label="item.label" :value="item.value" /> </li> </ul> </section> </template> <script lang="ts" setup> import { BentoTypography } from '@/components/typography'; import { SecondaryNavItem } from '../secondary-nav-item'; import { type BentoSecondaryNavItemsItem } from '../../secondary-nav.types'; defineProps<{ /** * The display name of the category */ label: string; /** * A list of {@see BentoSecondaryNavItemsItem} that belongs to this category */ items: Array<BentoSecondaryNavItemsItem>; }>(); </script> <script lang="ts"> /** * This component shouldn't be used directly. It is internally used by {@see BentoSecondaryNav} */ export default {}; </script> <style lang="scss" scoped src="./secondary-nav-category.scss" />
|
|
1
|
+
<template> <section class="b-secondary-nav-category" :aria-label="label"> <bento-typography el="h1" class="b-secondary-nav-category__label" variant="caption" stronger>{{ label }}</bento-typography> <ul class="b-secondary-nav__list"> <li v-for="item in items" :key="item.value"> <secondary-nav-item :label="item.label" :value="item.value" :items="item.items" /> </li> </ul> </section> </template> <script lang="ts" setup> import { BentoTypography } from '@/components/typography'; import { SecondaryNavItem } from '../secondary-nav-item'; import { type BentoSecondaryNavItemsItem } from '../../secondary-nav.types'; defineProps<{ /** * The display name of the category */ label: string; /** * A list of {@see BentoSecondaryNavItemsItem} that belongs to this category */ items: Array<BentoSecondaryNavItemsItem>; }>(); </script> <script lang="ts"> /** * This component shouldn't be used directly. It is internally used by {@see BentoSecondaryNav} */ export default {}; </script> <style lang="scss" scoped src="./secondary-nav-category.scss" />
|
package/dist/assets/components/secondary-nav/components/secondary-nav-item/secondary-nav-item.vue
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <
|
|
1
|
+
<template> <div class="b-secondary-nav-item"> <div class="b-secondary-nav-item__wrapper"> <button class="b-secondary-nav-item__option" :class="conditionalClasses" :aria-current="isItemSelected ? 'page' : undefined" :aria-expanded="expandleItemIsToggleOnly ? `${isOpen}` : undefined" :aria-controls="expandleItemIsToggleOnly ? subItemsId : undefined" @click="expandleItemIsToggleOnly ? onToggleClick() : onClick($event)" > <bento-typography class="b-secondary-nav-item__title" variant="body" stronger> {{ label }} </bento-typography> </button> <bento-button v-if="hasSubItems" :aria-hidden="expandleItemIsToggleOnly" :tabindex="expandleItemIsToggleOnly ? -1 : undefined" class="b-secondary-nav-item__toggle" variant="tertiary" :aria-label="isOpen ? `Collapse ${label}` : `Expand ${label}`" :aria-expanded="!expandleItemIsToggleOnly ? `${isOpen}` : undefined" :aria-controls="!expandleItemIsToggleOnly ? subItemsId : undefined" @click.stop="onToggleClick" > <template #iconLeft> <chevron-up-icon v-if="isOpen" class="b-secondary-nav-item__icon" aria-hidden="true" /> <chevron-down-icon v-else class="b-secondary-nav-item__icon" aria-hidden="true" /> </template> </bento-button> </div> <Transition :name="CONTENT_TRANSITION_NAME"> <div v-if="hasSubItems && isOpen" :id="subItemsId" :style="subItemsMaxHeight"> <ul ref="subItemsRef" class="b-secondary-nav-item__sub-items"> <li v-for="item in items" :key="item.value"> <secondary-nav-item :label="item.label" :value="item.value" :items="item.items" is-sub-item /> </li> </ul> </div> </Transition> </div> </template> <script lang="ts" setup> import { SECONDARY_NAV_STATE } from '../../secondary-nav.keys'; import { BentoButton } from '@/components/button'; import { BentoTypography } from '@/components/typography'; import ChevronDownIcon from '@adyen/ui-assets-icons-16/vue/chevron-down'; import ChevronUpIcon from '@adyen/ui-assets-icons-16/vue/chevron-up'; import { computed, inject, ref, watch } from 'vue'; import { generateUid } from '@/core/utils/ts'; import { type BentoSecondaryNavItemsItem } from '../../secondary-nav.types'; import { observeSizeOfElement } from '@/utils/ts/resize'; import { useExpandableContentHeight } from '@/composables/use-expandable-content-height/use-expandable-content-height'; const { activeItemValue, updateActiveItemValue, allowExpandableItemNavigation } = inject(SECONDARY_NAV_STATE); const CONTENT_TRANSITION_NAME = 'b-secondary-nav-item__animation--content'; const props = defineProps<{ /** * The display name of the navigation item */ label: string; /** * The value of this item. */ value: string; /** * Nested sub-items for this item. */ items?: Array<BentoSecondaryNavItemsItem>; /** * Defines if the item is a subitem */ isSubItem?: boolean; }>(); const isOpen = ref(false); const subItemsId = generateUid('secondary-nav-sub-items'); const subItemsRef = ref<HTMLUListElement>(null); const hasSubItems = computed(() => !!props.items?.length); const isItemSelected = computed(() => activeItemValue.value === props.value); const expandleItemIsToggleOnly = computed(() => hasSubItems.value && !allowExpandableItemNavigation.value); const { updateContentHeight, contentHeight } = useExpandableContentHeight(subItemsRef, isOpen); const subItemsMaxHeight = computed(() => (contentHeight.value ? { 'max-height': contentHeight.value } : {})); const conditionalClasses = computed(() => ({ 'b-secondary-nav-item__option--active': isItemSelected.value, 'b-secondary-nav-item__option--expandable': hasSubItems.value, 'b-secondary-nav-item__option--sub-item': props.isSubItem, })); const emit = defineEmits<{ /** * Event emitted when this element is clicked */ (e: 'click', clickEvent: MouseEvent): void; }>(); const onClick = (e: MouseEvent) => { emit('click', e); updateActiveItemValue(props.value); }; const onToggleClick = () => { isOpen.value = !isOpen.value; }; const hasActiveDescendant = (items: Array<BentoSecondaryNavItemsItem>, value: string): boolean => { return items.some( item => item.value === value || (item.items ? hasActiveDescendant(item.items, value) : false) ); }; watch( () => subItemsRef.value, () => { let observer: ResizeObserver | null = null; if (!observer && subItemsRef.value) { observer = observeSizeOfElement(subItemsRef.value, () => { if (subItemsRef.value) { updateContentHeight(); } }); } } ); watch( isOpen, () => { updateContentHeight(); }, { immediate: true } ); watch( activeItemValue, value => { if (hasSubItems.value && props.items && hasActiveDescendant(props.items, value)) { isOpen.value = true; } }, { immediate: true } ); </script> <script lang="ts"> /** * This component shouldn't be used directly. It is internally used by {@see BentoSecondaryNav} */ export default {}; </script> <style lang="scss" scoped src="./secondary-nav-item.scss" />
|
|
@@ -14,7 +14,7 @@ understand and navigate through various information.
|
|
|
14
14
|
|
|
15
15
|
Use a SecondaryNav if:
|
|
16
16
|
|
|
17
|
-
-
|
|
17
|
+
- You need to organize information in a way that offers users structural guidance for complex pages like settings.
|
|
18
18
|
|
|
19
19
|
## Variations
|
|
20
20
|
|
|
@@ -22,6 +22,36 @@ Use a SecondaryNav if:
|
|
|
22
22
|
|
|
23
23
|
<Canvas of={SecondaryNavStories.WithCategories} />
|
|
24
24
|
|
|
25
|
+
### With sub-items
|
|
26
|
+
|
|
27
|
+
Sub-items can be used to add an extra navigation level. Depending on the use case, items sith sub-items may link to
|
|
28
|
+
their own pages or just be used to organise items further.
|
|
29
|
+
|
|
30
|
+
<Canvas of={SecondaryNavStories.WithSubItems} />
|
|
31
|
+
|
|
32
|
+
#### Parent items without a page
|
|
33
|
+
|
|
34
|
+
By default (`allowExpandableItemNavigation` is `false`), clicking anywhere on a parent item expands or collapses its
|
|
35
|
+
sub-items. The parent item itself is not selectable and does not emit a `change` event.
|
|
36
|
+
|
|
37
|
+
#### Parent items with a page
|
|
38
|
+
|
|
39
|
+
When `allowExpandableItemNavigation` is `true`, parent items link to their own pages. Clicking the item label navigates
|
|
40
|
+
to that page and emits a `change` event, while the chevron button exclusively controls expanding and collapsing
|
|
41
|
+
sub-items.
|
|
42
|
+
|
|
43
|
+
## Behavior
|
|
44
|
+
|
|
45
|
+
### Desktop
|
|
46
|
+
|
|
47
|
+
- For items with sub-items, expanding/collapsing is controlled by the caret button.
|
|
48
|
+
- If `value` points to a sub-item, parent items auto-expand so the selected item is visible.
|
|
49
|
+
|
|
50
|
+
### Mobile
|
|
51
|
+
|
|
52
|
+
- On mobile, `bento-secondary-nav` renders a `bento-dropdown`.
|
|
53
|
+
- Categories are shown as static category labels and nested items are shown with visual indentation.
|
|
54
|
+
|
|
25
55
|
## Accessibility
|
|
26
56
|
|
|
27
57
|
### Keyboard interaction
|
|
@@ -37,4 +67,4 @@ page. The following keys trigger the following actions:
|
|
|
37
67
|
|
|
38
68
|
## Resources
|
|
39
69
|
|
|
40
|
-
-
|
|
70
|
+
- [Figma link](https://www.figma.com/design/uLabwF3243jdMDsNSP7U9I/Bento---Components?node-id=25227-18624&t=2F6m9PIJUQlVcUJj-0)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import { action } from '@storybook/addon-actions'; import { isVue2 } from 'vue-demi'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import BentoSecondaryNav from './secondary-nav.vue'; import type { Meta, StoryObj } from '@storybook/vue'; import { type BentoSecondaryNavItems } from '@/components/secondary-nav/secondary-nav.types'; const meta: Meta = { title: 'Secondary Nav', component: BentoSecondaryNav, }; export default meta; type Story = StoryObj<typeof BentoSecondaryNav>; const DEFAULT_ITEMS: BentoSecondaryNavItems = [ { label: 'Option 1', value: 'option_1', }, { label: 'Option 2', value: 'option_2', }, { label: 'Option 3', value: 'option_3', }, ]; const ITEMS_WITH_CATEGORIES: BentoSecondaryNavItems = [ { label: 'Category 1', items: [ { label: 'Option 1', value: 'option_1', }, { label: 'Option 2', value: 'option_2', }, ], }, { label: 'Category 2', items: [ { label: 'Option 3', value: 'option_3', }, { label: 'Option 4', value: 'option_4', }, { label: 'Option 5', value: 'option_5', }, ], }, ]; const DEFAULT_STORY_CODE_SAMPLE = ` <template> <bento-secondary-nav :items="items" :value="activeItem" @change="onChange" /> <template> <script lang="ts" setup> const items = [ { label: 'Option 1', value: 'option_1', }, { label: 'Option 2', value: 'option_2', }, { label: 'Option 3', value: 'option_3', }, ]; const activeItem = ref(items[0].value); const onChange = (newActiveItem: string) => { activeItem.value = newActiveItem; }; </script> `; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoSecondaryNav, }, props: Object.keys(argTypes), template: ` <bento-secondary-nav v-bind="args" @change="clickedAction" /> `, setup(props) { const clickedAction = value => action('change')(value); return { // Values args: isVue2 ? props : _args, // Events clickedAction, }; }, }), args: { items: DEFAULT_ITEMS, value: 'option_1', }, parameters: storybookDocsParameter(DEFAULT_STORY_CODE_SAMPLE), }; const WITH_CATEGORIES_STORY_CODE_SAMPLE = ` <template> <bento-secondary-nav :items="items" :value="activeItem" @change="onChange" /> <template> <script lang="ts" setup> const items = [ { label: 'Category 1', items: [ { label: 'Option 1', value: 'option_1', }, { label: 'Option 2', value: 'option_2', }, ], }, { label: 'Category 2', items: [ { label: 'Option 3', value: 'option_3', }, { label: 'Option 4', value: 'option_4', }, { label: 'Option 5', value: 'option_5', }, ], }, ]; const activeItem = ref(items[0].value); const onChange = (newActiveItem: string) => { activeItem.value = newActiveItem; }; </script> `; export const WithCategories: Story = { render: (_args, { argTypes }) => ({ components: { BentoSecondaryNav, }, props: Object.keys(argTypes), template: ` <bento-secondary-nav v-bind="args" @change="clickedAction" /> `, setup(props) { const clickedAction = value => action('change')(value); return { // Values args: isVue2 ? props : _args, // Events clickedAction, }; }, }), args: { items: ITEMS_WITH_CATEGORIES, }, parameters: storybookDocsParameter(WITH_CATEGORIES_STORY_CODE_SAMPLE), };
|
|
1
|
+
import { action } from '@storybook/addon-actions'; import { isVue2 } from 'vue-demi'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import BentoSecondaryNav from './secondary-nav.vue'; import WithSubItemsCodeSample from './__tests__/secondary-nav-with-sub-items.vue?raw'; import type { Meta, StoryObj } from '@storybook/vue'; import { type BentoSecondaryNavItems } from '@/components/secondary-nav/secondary-nav.types'; const meta: Meta = { title: 'Secondary Nav', component: BentoSecondaryNav, }; export default meta; type Story = StoryObj<typeof BentoSecondaryNav>; const DEFAULT_ITEMS: BentoSecondaryNavItems = [ { label: 'Option 1', value: 'option_1', }, { label: 'Option 2', value: 'option_2', }, { label: 'Option 3', value: 'option_3', }, ]; const ITEMS_WITH_CATEGORIES: BentoSecondaryNavItems = [ { label: 'Category 1', items: [ { label: 'Option 1', value: 'option_1', }, { label: 'Option 2', value: 'option_2', }, ], }, { label: 'Category 2', items: [ { label: 'Option 3', value: 'option_3', }, { label: 'Option 4', value: 'option_4', }, { label: 'Option 5', value: 'option_5', }, ], }, ]; const ITEMS_WITH_SUB_ITEMS: BentoSecondaryNavItems = ITEMS_WITH_CATEGORIES.map(category => ({ ...category, items: category.items.map(item => { if (item.value === 'option_2') { return { ...item, items: [ { label: 'Option 2.1', value: 'option_2_1', }, { label: 'Option 2.2', value: 'option_2_2', }, ], }; } if (item.value === 'option_4') { return { ...item, items: [ { label: 'Option 4.1', value: 'option_4_1', }, { label: 'Option 4.2', value: 'option_4_2', }, ], }; } return item; }), })); const DEFAULT_STORY_CODE_SAMPLE = ` <template> <bento-secondary-nav :items="items" :value="activeItem" @change="onChange" /> <template> <script lang="ts" setup> const items = [ { label: 'Option 1', value: 'option_1', }, { label: 'Option 2', value: 'option_2', }, { label: 'Option 3', value: 'option_3', }, ]; const activeItem = ref(items[0].value); const onChange = (newActiveItem: string) => { activeItem.value = newActiveItem; }; </script> `; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoSecondaryNav, }, props: Object.keys(argTypes), template: ` <bento-secondary-nav v-bind="args" @change="clickedAction" /> `, setup(props) { const clickedAction = value => action('change')(value); return { // Values args: isVue2 ? props : _args, // Events clickedAction, }; }, }), args: { items: DEFAULT_ITEMS, value: 'option_1', }, parameters: storybookDocsParameter(DEFAULT_STORY_CODE_SAMPLE), }; const WITH_CATEGORIES_STORY_CODE_SAMPLE = ` <template> <bento-secondary-nav :items="items" :value="activeItem" @change="onChange" /> <template> <script lang="ts" setup> const items = [ { label: 'Category 1', items: [ { label: 'Option 1', value: 'option_1', }, { label: 'Option 2', value: 'option_2', }, ], }, { label: 'Category 2', items: [ { label: 'Option 3', value: 'option_3', }, { label: 'Option 4', value: 'option_4', }, { label: 'Option 5', value: 'option_5', }, ], }, ]; const activeItem = ref(items[0].value); const onChange = (newActiveItem: string) => { activeItem.value = newActiveItem; }; </script> `; export const WithCategories: Story = { render: (_args, { argTypes }) => ({ components: { BentoSecondaryNav, }, props: Object.keys(argTypes), template: ` <bento-secondary-nav v-bind="args" @change="clickedAction" /> `, setup(props) { const clickedAction = value => action('change')(value); return { // Values args: isVue2 ? props : _args, // Events clickedAction, }; }, }), args: { items: ITEMS_WITH_CATEGORIES, }, parameters: storybookDocsParameter(WITH_CATEGORIES_STORY_CODE_SAMPLE), }; export const WithSubItems: Story = { render: (_args, { argTypes }) => ({ components: { BentoSecondaryNav, }, props: Object.keys(argTypes), template: ` <bento-secondary-nav v-bind="args" @change="clickedAction" /> `, setup(props) { const clickedAction = value => action('change')(value); return { args: isVue2 ? props : _args, clickedAction, }; }, }), args: { value: 'option_2_1', items: ITEMS_WITH_SUB_ITEMS, }, parameters: storybookDocsParameter(WithSubItemsCodeSample), };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import type { Ref } from 'vue'; export interface BentoSecondaryNavState { activeItemValue: Ref<string>; updateActiveItemValue: (id: string) => void; } export interface BentoSecondaryNavItemsItem { label: string; value: string; } export interface BentoSecondaryNavItemsCategory { label: string; items: Array<BentoSecondaryNavItemsItem>; } export type BentoSecondaryNavItems = Array<BentoSecondaryNavItemsItem | BentoSecondaryNavItemsCategory>;
|
|
1
|
+
import type { Ref } from 'vue'; export interface BentoSecondaryNavState { activeItemValue: Ref<string>; updateActiveItemValue: (id: string) => void; allowExpandableItemNavigation: Ref<boolean>; } export interface BentoSecondaryNavItemsItem { label: string; value: string; items?: Array<BentoSecondaryNavItemsItem>; } export interface BentoSecondaryNavItemsCategory { label: string; items: Array<BentoSecondaryNavItemsItem>; } export type BentoSecondaryNavItems = Array<BentoSecondaryNavItemsItem | BentoSecondaryNavItemsCategory>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <nav class="b-secondary-nav"> <bento-dropdown v-if="isMobile" :aria-label="t('mobileDropdownAriaLabel')" :items="
|
|
1
|
+
<template> <nav class="b-secondary-nav"> <bento-dropdown v-if="isMobile" :aria-label="t('mobileDropdownAriaLabel')" :items="mobileDropdownItems" :static-categories="hasCategories" :model-value="activeItemValue" :is-option-disabled="isMobileOptionDisabled" @update:model-value="updateActiveItemValue" > <template #display-value="{ label }"> <bento-typography el="span"> {{ label }} </bento-typography> </template> <template #default="{ label, data }"> <bento-typography el="span" :class="{ 'b-secondary-nav__nested-option': !!data?.level }"> {{ label }} </bento-typography> </template> </bento-dropdown> <ul v-else class="b-secondary-nav__list"> <li v-for="item in items" :key="item.label"> <secondary-nav-category v-if="'items' in item && !('value' in item)" :label="item.label" :items="item.items" /> <secondary-nav-item v-else :label="item.label" :value="item.value" :items="item.items" /> </li> </ul> </nav> </template> <script setup lang="ts"> import { computed, provide, readonly, ref, toRef, watch } from 'vue'; import { SecondaryNavItem } from './components/secondary-nav-item'; import { SecondaryNavCategory } from './components/secondary-nav-category'; import { SECONDARY_NAV_STATE } from './secondary-nav.keys'; import { type BentoSecondaryNavItems, type BentoSecondaryNavItemsCategory, type BentoSecondaryNavItemsItem, type BentoSecondaryNavState, } from './secondary-nav.types'; import { BentoDropdown } from '@/components/dropdown'; import { BentoTypography } from '@/components/typography'; import { useBreakpoints } from '@vueuse/core'; import { useI18n } from '@/utils/ts/i18n'; import { type BentoListboxOptionItem, type BentoListboxOptions } from '@/types/listbox'; import { BMediaQuerySMax } from '@adyen/bento-design-tokens/dist/js/bento/es6'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const { t } = useI18n<{ message: MessageSchema }>({ messages }); const breakpoints = useBreakpoints({ s: BMediaQuerySMax, }); const isMobile = breakpoints.smallerOrEqual('s'); const props = defineProps<{ /** * An array containing all the items on the nav. The array can have either {@see BentoSecondaryNavItemsCategory }, * which is used to group different items or {@see BentoSecondaryNavItemsItem } if you don't need to group them. */ items: BentoSecondaryNavItems; /** * The value of the selected item. * If nothing is passed, it defaults to the first item value in items. */ value?: string; /** * When true, items that contain subitems are also clickable and can be selected. * When false, those items only act as expand/collapse toggles. */ allowExpandableItemNavigation?: boolean; }>(); const emit = defineEmits<{ /** * Emmited when the current selected item changes */ (e: 'change', category: string): void; }>(); const isCategory = (item: BentoSecondaryNavItems[number]): item is BentoSecondaryNavItemsCategory => 'items' in item && !('value' in item); const mapNestedItems = (items: Array<BentoSecondaryNavItemsItem>, level = 0): BentoListboxOptions => items.flatMap((item): BentoListboxOptions => { const currentItem: BentoListboxOptionItem = { label: item.label, value: item.value, data: { level, hasSubItems: !!item.items?.length }, }; if (item.items?.length) { return [currentItem, ...mapNestedItems(item.items, level + 1)]; } return [currentItem]; }); const isMobileOptionDisabled = (option: BentoListboxOptionItem): boolean => !props.allowExpandableItemNavigation && (option.data as { hasSubItems?: boolean })?.hasSubItems; const hasCategories = computed(() => props.items.some(item => isCategory(item))); const mobileDropdownItems = computed<BentoListboxOptions>(() => { return props.items.flatMap(item => { if (isCategory(item)) { return [ { label: item.label, value: `category-${item.label}`, items: mapNestedItems(item.items), } satisfies BentoListboxOptionItem, ]; } if (item.items?.length) { return mapNestedItems([item]); } return [ { label: item.label, value: item.value, data: { level: 0 }, } satisfies BentoListboxOptionItem, ]; }); }); const flattenNestedItems = (items: Array<BentoSecondaryNavItemsItem>): Array<BentoSecondaryNavItemsItem> => items.flatMap(item => [item, ...(item.items?.length ? flattenNestedItems(item.items) : [])]); const findFirstSelectableNestedItemValue = (items: Array<BentoSecondaryNavItemsItem>): string => flattenNestedItems(items).find(item => props.allowExpandableItemNavigation || !item.items?.length)?.value ?? ''; const activeItemValueFallback = computed(() => { if (!props.items.length) { return ''; } return findFirstSelectableNestedItemValue( props.items.flatMap(item => isCategory(item) ? flattenNestedItems(item.items) : flattenNestedItems([item]) ) ); }); const activeItemValue = ref(props.value ?? activeItemValueFallback.value); const updateActiveItemValue = (value: string) => { activeItemValue.value = value; emit('change', value); }; // The component's state is inject on `secondary-nav-item` to determine the // current active item and to change its value const state: BentoSecondaryNavState = { activeItemValue: readonly(activeItemValue), updateActiveItemValue, allowExpandableItemNavigation: toRef(props, 'allowExpandableItemNavigation'), }; watch( () => props.value, value => { if (value !== undefined) { activeItemValue.value = value; } } ); provide(SECONDARY_NAV_STATE, state); </script> <script lang="ts"> /** * Secondary Nav offers users a structured way to access information. * It also provides a clear hierarchy for users to understand and navigate through various information. * * @example * import { BentoSecondaryNav } from '@adyen/bento-vue2'; * * export default { * components: { BentoSecondaryNav }, * template: ` * <bento-secondary-nav :value="value" :items="items" /> * `, * setup() { * const value = ref(2) // Active item value * const items: BentoSecondaryNavItemInfo[] = [ * { label: 'Option 1', value: 1 }, * { label: 'Option 2', value: 2 }, * ]; * return { items, value }; * } * } */ export default { i18n: { messages }, }; </script> <style lang="scss" scoped src="./secondary-nav.scss" />
|