Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(kselect): add item-creation-validator prop [KHCP-13862] #2485

Merged
merged 8 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -423,7 +423,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 @@ -442,6 +443,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 @@ -485,7 +491,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