Skip to content

Commit

Permalink
fix(kselect): add item-creation-validator prop [KHCP-13862] (#2485)
Browse files Browse the repository at this point in the history
  • Loading branch information
portikM authored Oct 31, 2024
1 parent b94dc3b commit bf31855
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 10 deletions.
6 changes: 5 additions & 1 deletion docs/components/multiselect.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,11 @@ const trackNewItems = (item, added) => {

### itemCreationValidator

Prop for passing a function for input validation when item creation is enabled. The function takes query input string as a single parameter and must return a `boolean` value. When a function passed through `itemCreationValidator` returns `false`, the _Add new value_ button will be disabled.
Prop for passing a function for input validation when item creation is enabled.

The function takes the query input string as a single parameter and must return a `boolean` value.

When the function passed through `itemCreationValidator` returns `false`, the "Add new value" button will be disabled.

<KMultiselect
:item-creation-validator="itemCreationValidator"
Expand Down
76 changes: 76 additions & 0 deletions docs/components/select.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,71 @@ When used in conjunction with `enableFiltering` set to `true`, KSelect will sugg
<KSelect enable-item-creation enable-filtering placeholder="Try searching for 'service d'" :items="selectItems" />
```

### itemCreationValidator

Prop for passing a function for input validation when item creation is enabled.

The function takes the query input string as a single parameter and must return a `boolean` value.

When the function passed through `itemCreationValidator` returns `false`, the "Add new value" button will be disabled.

<KSelect
enable-filtering
enable-item-creation
:item-creation-validator="itemCreationValidator"
:items="selectItemsUnselected"
@query-change="onItemCreationQueryChange"
>
<template
v-if="showNewItemValidationError"
#dropdown-footer-text
>
<span class="item-creation-validation-error-message">
New item should be at least 3 characters long.
</span>
</template>
</KSelect>

```vue
<template>
<KSelect
enable-filtering
enable-item-creation
:item-creation-validator="itemCreationValidator"
:items="selectItems"
@query-change="onQueryChange"
>
<template
v-if="showNewItemValidationError"
#dropdown-footer-text
>
<span class="item-creation-validation-error-message">
New item should be at least 3 characters long.
</span>
</template>
</KSelect>
</template>
<script setup lang="ts">
import type { SelectItem } from '@kong/kongponents'
const selectItems: SelectItem[] = [...]
const showNewItemValidationError = ref<boolean>(false)
const itemCreationValidator = (value: string) => value.length >= 3
const onQueryChange = (query: string): void => {
showNewItemValidationError.value = query ? !itemCreationValidator(query) : false
}
</script>
<style lang="scss" scoped>
.item-creation-validation-error-message {
color: $kui-color-text-danger;
}
</style>
```

### loading

Pass `true` to display loader instead of items in the dropdown. KSelect's `item` prop is reactive to changes by design, so that should you need to perform async item fetching/filtering you can execute that logic within the host app and pass items back to KSelect.
Expand Down Expand Up @@ -758,6 +823,13 @@ const fetchAsyncItems = (): void => {
}

const vModel = ref<string>('f')

const showNewItemValidationError = ref<boolean>(false)
const itemCreationValidator = (value: string) => value.length >= 3

const onItemCreationQueryChange = (query: string): void => {
showNewItemValidationError.value = query ? !itemCreationValidator(query) : false
}
</script>

<style lang="scss" scoped>
Expand Down Expand Up @@ -786,4 +858,8 @@ const vModel = ref<string>('f')
font-size: $kui-font-size-20;
}
}

.item-creation-validation-error-message {
color: $kui-color-text-danger;
}
</style>
27 changes: 26 additions & 1 deletion sandbox/pages/SandboxSelect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,27 @@
</KSelect>
</SandboxSectionComponent>
<SandboxSectionComponent
title="enableItemCreation"
title="enableItemCreation & itemCreationValidator"
>
<KSelect
enable-filtering
enable-item-creation
:items="selectItems"
/>
<KSelect
enable-filtering
enable-item-creation
:item-creation-validator="itemCreationValidator"
:items="selectItems"
@query-change="onItemCreationQueryChange"
>
<template
v-if="showNewItemValidationError"
#dropdown-footer-text
>
<span class="item-creation-validation-error-message">New item should be at least 3 characters long.</span>
</template>
</KSelect>
</SandboxSectionComponent>
<SandboxSectionComponent
title="required"
Expand Down Expand Up @@ -419,6 +433,13 @@ const handeAsyncItemRemoved = (item: SelectItem): void => {
selectItemsInitial.value = selectItemsInitial.value.filter(i => i.value !== item.value)
}
const showNewItemValidationError = ref<boolean>(false)
const itemCreationValidator = (value: string) => value.length >= 3
const onItemCreationQueryChange = (query: string): void => {
showNewItemValidationError.value = query ? !itemCreationValidator(query) : false
}
onMounted(() => {
setAsyncItems()
})
Expand Down Expand Up @@ -470,5 +491,9 @@ onMounted(() => {
gap: $kui-space-30;
}
}
.item-creation-validation-error-message {
color: $kui-color-text-danger;
}
}
</style>
2 changes: 1 addition & 1 deletion src/components/KMultiselect/KMultiselect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ const props = defineProps({
* Validator function for item creation.
*/
itemCreationValidator: {
type: Function,
type: Function as PropType<(query: string) => boolean>,
default: () => true,
},
})
Expand Down
29 changes: 28 additions & 1 deletion src/components/KSelect/KSelect.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,8 @@ describe('KSelect', () => {
cy.get('input').clear()
// add new item
cy.get('input').type(newItem)
cy.getTestId('select-add-item').should('contain.text', newItem).click()
cy.getTestId('select-add-item').should('contain.text', newItem)
cy.getTestId('select-add-item').find('button').should('be.enabled').click()
// search is cleared
cy.get('input').should('not.contain.text', newItem)
// displays selected item correctly
Expand All @@ -473,6 +474,32 @@ describe('KSelect', () => {
cy.getTestId('select-add-item').should('be.visible').should('contain.text', newItem)
})

it('renders add new value button disabled when itemCreationValidator returns false', () => {
const labels = ['Label 1', 'Label 2']
const vals = ['val1', 'val2']
const newItem = 'Rock me'

mount(KSelect, {
props: {
items: [{
label: labels[0],
value: vals[0],
}, {
label: labels[1],
value: vals[1],
}],
enableItemCreation: true,
enableFiltering: true,
itemCreationValidator: () => false,
},
})

cy.get('.select-input').click()
cy.get('input').type(newItem)
cy.getTestId('select-add-item').should('contain.text', newItem)
cy.getTestId('select-add-item').find('button').should('be.disabled')
})

it('updates selected status after items are mutated', () => {
const labels = ['Label 1', 'Label 2']
const vals = ['val1', 'val2']
Expand Down
18 changes: 12 additions & 6 deletions src/components/KSelect/KSelect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@
key="select-add-item"
class="select-add-item"
data-testid="select-add-item"
:item="{ label: `${filterQuery} (Add new value)`, value: 'add_item' }"
:item="{ label: `${filterQuery} (Add new value)`, value: 'add_item', disabled: !itemCreationValidator(filterQuery) }"
@selected="handleAddItem"
>
<template #content>
Expand All @@ -163,7 +163,7 @@
</template>
</KSelectItem>
<div
v-if="hasDropdownFooter && dropdownFooterTextPosition === 'static'"
v-if="(dropdownFooterText || $slots['dropdown-footer-text']) && dropdownFooterTextPosition === 'static'"
class="dropdown-footer dropdown-footer-static"
>
<slot name="dropdown-footer-text">
Expand All @@ -180,7 +180,7 @@
</div>
</div>
<div
v-if="hasDropdownFooter && dropdownFooterTextPosition === 'sticky'"
v-if="(dropdownFooterText || $slots['dropdown-footer-text']) && dropdownFooterTextPosition === 'sticky'"
class="dropdown-footer dropdown-footer-sticky"
>
<slot name="dropdown-footer-text">
Expand Down Expand Up @@ -333,6 +333,13 @@ const props = defineProps({
type: Boolean,
default: false,
},
/**
* Validator function for item creation.
*/
itemCreationValidator: {
type: Function as PropType<(query: string) => boolean>,
default: () => true,
},
error: {
type: Boolean,
default: false,
Expand Down Expand Up @@ -364,10 +371,9 @@ const hasLabelTooltip = computed((): boolean => !!(props.labelAttributes?.info |
const isRequired = computed((): boolean => attrs.required !== undefined && String(attrs.required) !== 'false')
const isDisabled = computed((): boolean => attrs.disabled !== undefined && String(attrs.disabled) !== 'false')
const isReadonly = computed((): boolean => attrs.readonly !== undefined && String(attrs.readonly) !== 'false')
const hasDropdownFooter = computed((): boolean => !!(slots['dropdown-footer-text'] || props.dropdownFooterText))
const defaultKPopAttributes = {
popoverClasses: `select-popover ${hasDropdownFooter.value ? `has-${props.dropdownFooterTextPosition}-dropdown-footer` : ''}`,
popoverClasses: `select-popover ${props.dropdownFooterText || slots['dropdown-footer-text'] ? `has-${props.dropdownFooterTextPosition}-dropdown-footer` : ''}`,
popoverTimeout: 0,
placement: 'bottom-start' as PopPlacements,
hideCaret: true,
Expand Down Expand Up @@ -487,7 +493,7 @@ const onInputEnter = (): void => {
}
const handleAddItem = (): void => {
if (!props.enableItemCreation || !filterQuery.value || !uniqueFilterQuery.value) {
if (!props.enableItemCreation || !filterQuery.value || !uniqueFilterQuery.value || !props.itemCreationValidator(filterQuery.value)) {
// do nothing if not enabled or no label or label already exists
return
}
Expand Down

0 comments on commit bf31855

Please sign in to comment.