diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index 28b00e28..b438c642 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -1,5 +1,11 @@ # @soramitsu-ui/ui +## 0.13.13 + +### Patch Changes + +- b5029a5b: **feat**(`SSelectDropdown`): add mandatory prop + ## 0.13.12 ### Patch Changes diff --git a/packages/ui/cypress/component/Select.spec.cy.ts b/packages/ui/cypress/component/Select.spec.cy.ts index 25af419c..7eba67a6 100644 --- a/packages/ui/cypress/component/Select.spec.cy.ts +++ b/packages/ui/cypress/component/Select.spec.cy.ts @@ -244,6 +244,127 @@ it('SSelect - clicking options, checking auto-transformations', () => { assertValue('2') }) +it('SSelect simple options mandatory mode works', () => { + cy.mount({ + setup() { + const model = ref(null) + + const options = [ + { label: 'Opt 1', value: 1 }, + { label: 'Opt 2', value: 2 }, + { label: 'Opt 3', value: 3 }, + ] + + const [multiple, toggleMultiple] = useToggle(false) + + return { model, options, multiple, toggleMultiple } + }, + template: ` +
Value: {{ model }}
+ + + `, + }) + + function assertValue(val: string) { + cy.contains(`Value: ${val}`) + } + + cy.contains('Dap').click() + cy.get('.s-select-option__content').eq(0).click() + assertValue('1') + + cy.contains('Dap').click() + cy.get('.s-select-option__content').eq(0).click() + assertValue('1') + + cy.get('button').contains('multiple').click() + cy.contains('Dap').click() + cy.get('.s-select-option__content').eq(1).click() + assertValue('[ 1, 2 ]') + + cy.get('.s-select-option__content').eq(1).click() + cy.get('.s-select-option__content').eq(0).click() + assertValue('[ 1 ]') +}) + +it('SSelect group options mandatory mode works', () => { + cy.mount({ + setup() { + const model = ref(null) + + const options = [ + { + header: '1st group', + selectAllBtn: true, + items: [ + { + label: 'Germany', + value: 'du', + }, + { + label: 'England', + value: 'en', + }, + { + label: 'United Arab Emirates', + value: 'ae', + }, + ], + }, + { + header: '2nd group', + selectAllBtn: false, + items: [ + { + label: 'Iceland', + value: 'is', + }, + { + label: 'Japan', + value: 'jp', + }, + ], + }, + ] + + return { model, options } + }, + template: ` +
Value: {{ model }}
+ + `, + }) + + function assertValue(val: string) { + cy.contains(`Value: ${val}`) + } + + cy.contains('Dap').click() + + cy.get('.s-select-dropdown__action').click() + assertValue(`[ "du", "en", "ae" ]`) + cy.get('.s-select-dropdown__action').click() + assertValue(`[ "du", "en", "ae" ]`) + cy.get('.s-select-option__content').eq(3).click() + assertValue(`[ "du", "en", "ae", "is" ]`) + cy.get('.s-select-dropdown__action').click() + assertValue(`[ "is" ]`) + cy.get('.s-select-option__content').eq(3).click() + assertValue(`[ "is" ]`) +}) + it('SDropdown - model usage works', () => { cy.mount({ setup() { diff --git a/packages/ui/etc/api/ui.api.md b/packages/ui/etc/api/ui.api.md index 39257739..5caef6ad 100644 --- a/packages/ui/etc/api/ui.api.md +++ b/packages/ui/etc/api/ui.api.md @@ -648,6 +648,7 @@ loading?: boolean | undefined; dropdownSearch?: boolean | undefined; remoteSearch?: boolean | undefined; maxShownOptions?: string | number | undefined; +mandatory?: boolean | undefined; }>, {}, unknown, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {}, string, VNodeProps & AllowedComponentProps & ComponentCustomProps, Readonly[] | SelectOptionGroup[] | undefined; @@ -662,12 +663,14 @@ loading?: boolean | undefined; dropdownSearch?: boolean | undefined; remoteSearch?: boolean | undefined; maxShownOptions?: string | number | undefined; +mandatory?: boolean | undefined; }>>>, {}, {}>, { label?(_: { options: UnwrapRef[] | SelectOptionGroup[]>; multiple: boolean; disabled: boolean; loading: boolean; + mandatory: boolean; label: string | null; size: SelectSize; noAutoClose: boolean; @@ -703,6 +706,8 @@ export interface SelectApi extends UnwrapRef> { readonly label: string | null; // (undocumented) readonly loading: boolean; + // (undocumented) + readonly mandatory: boolean; menuToggle: (value?: boolean) => void; // (undocumented) readonly multiple: boolean; @@ -1284,6 +1289,7 @@ triggerSearch?: boolean | undefined; dropdownSearch?: boolean | undefined; remoteSearch?: boolean | undefined; maxShownOptions?: string | number | undefined; +mandatory?: boolean | undefined; }>, {}, unknown, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {}, string, VNodeProps & AllowedComponentProps & ComponentCustomProps, Readonly[] | SelectOptionGroup[] | undefined; @@ -1298,12 +1304,14 @@ triggerSearch?: boolean | undefined; dropdownSearch?: boolean | undefined; remoteSearch?: boolean | undefined; maxShownOptions?: string | number | undefined; +mandatory?: boolean | undefined; }>>>, {}, {}>, { label?(_: { options: UnwrapRef[] | SelectOptionGroup[]>; multiple: boolean; disabled: boolean; loading: boolean; + mandatory: boolean; label: string | null; size: SelectSize; noAutoClose: boolean; @@ -1358,6 +1366,7 @@ sameWidthPopper: boolean; triggerSearch: boolean; dropdownSearch: boolean; remoteSearch: boolean; +mandatory: boolean; }>, {}, unknown, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, { "update:modelValue": (value: any) => void; search: (value: string) => void; @@ -1390,6 +1399,7 @@ sameWidthPopper: boolean; triggerSearch: boolean; dropdownSearch: boolean; remoteSearch: boolean; +mandatory: boolean; }>>> & { "onUpdate:modelValue"?: ((value: any) => any) | undefined; onSearch?: ((value: string) => any) | undefined; @@ -1401,6 +1411,7 @@ size: SelectSize; disabled: boolean; loading: boolean; options: SelectOption[] | SelectOptionGroup[]; +mandatory: boolean; syncMenuAndInputWidths: boolean; noAutoClose: boolean; sameWidthPopper: boolean; @@ -1437,6 +1448,7 @@ type: SelectButtonType; multiple: boolean; disabled: boolean; loading: boolean; + mandatory: boolean; label: string | null; size: SelectSize; noAutoClose: boolean; @@ -1482,6 +1494,7 @@ search: boolean; multiple: boolean; disabled: boolean; loading: boolean; + mandatory: boolean; label: string | null; size: SelectSize; noAutoClose: boolean; diff --git a/packages/ui/package.json b/packages/ui/package.json index 2dc03454..212eb0ae 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@soramitsu-ui/ui", - "version": "0.13.12", + "version": "0.13.13", "main": "dist/lib.cjs", "module": "dist/lib.mjs", "types": "dist/lib.d.ts", diff --git a/packages/ui/src/components/Select/SDropdown.vue b/packages/ui/src/components/Select/SDropdown.vue index b3057382..2d25ec45 100644 --- a/packages/ui/src/components/Select/SDropdown.vue +++ b/packages/ui/src/components/Select/SDropdown.vue @@ -19,6 +19,7 @@ const props = defineProps<{ dropdownSearch?: boolean remoteSearch?: boolean maxShownOptions?: string | number | undefined + mandatory?: boolean }>() const buttonType = computed(() => (props.inline ? SelectButtonType.Inline : SelectButtonType.Default)) diff --git a/packages/ui/src/components/Select/SSelect.vue b/packages/ui/src/components/Select/SSelect.vue index ed65b154..77110e21 100644 --- a/packages/ui/src/components/Select/SSelect.vue +++ b/packages/ui/src/components/Select/SSelect.vue @@ -19,6 +19,7 @@ const props = defineProps<{ dropdownSearch?: boolean remoteSearch?: boolean maxShownOptions?: string | number | undefined + mandatory?: boolean }>() const defaultOptionType = computed(() => (props.multiple ? SelectOptionType.Checkbox : SelectOptionType.Radio)) diff --git a/packages/ui/src/components/Select/SSelectBase.vue b/packages/ui/src/components/Select/SSelectBase.vue index 19f96f7d..acec8b9b 100644 --- a/packages/ui/src/components/Select/SSelectBase.vue +++ b/packages/ui/src/components/Select/SSelectBase.vue @@ -23,10 +23,9 @@ const props = withDefaults( label?: string | null /** - * TODO - * * - Doesn't allow to unselect value in single mode * - Doesn't allow to unselect last the only one picked value in multiple mode + * TODO * - Picks some value automatically (the first one) if `modelValue` is null and there are some available options */ mandatory?: boolean @@ -83,6 +82,7 @@ const props = withDefaults( triggerSearch: false, dropdownSearch: false, remoteSearch: false, + mandatory: false, }, ) @@ -92,7 +92,7 @@ const emit = defineEmits<{ }>() const model = useVModel(props, 'modelValue', emit) -const { multiple, disabled, loading, options, size, label, noAutoClose, remoteSearch } = toRefs(props) +const { multiple, disabled, loading, options, size, label, noAutoClose, remoteSearch, mandatory } = toRefs(props) const modeling = useSelectModel({ model, @@ -101,6 +101,7 @@ const modeling = useSelectModel({ storeSelectedOptions: remoteSearch, singleModeAutoClose: not(noAutoClose), onAutoClose: () => togglePopper(false), + mandatory, }) const [showPopper, togglePopper] = useToggle(false) @@ -125,6 +126,7 @@ const api: SelectApi = reactive({ options, disabled, loading, + mandatory, label, isMenuOpened: showPopper, menuToggle: togglePopper, diff --git a/packages/ui/src/components/Select/api.ts b/packages/ui/src/components/Select/api.ts index 5c139e8a..c34e2b82 100644 --- a/packages/ui/src/components/Select/api.ts +++ b/packages/ui/src/components/Select/api.ts @@ -8,6 +8,7 @@ export interface SelectApi extends UnwrapRef> { readonly multiple: boolean readonly disabled: boolean readonly loading: boolean + readonly mandatory: boolean readonly label: string | null readonly size: SelectSize readonly noAutoClose: boolean diff --git a/packages/ui/src/components/Select/use-model.ts b/packages/ui/src/components/Select/use-model.ts index 1d1941ae..a1c5c4ee 100644 --- a/packages/ui/src/components/Select/use-model.ts +++ b/packages/ui/src/components/Select/use-model.ts @@ -42,6 +42,7 @@ export interface UseSelectModelParams { * Should be used to actually perform menu closing */ onAutoClose: () => void + mandatory?: Ref } export function useSelectModel({ @@ -51,6 +52,7 @@ export function useSelectModel({ singleModeAutoClose, onAutoClose, storeSelectedOptions, + mandatory, }: UseSelectModelParams): UseSelectModelReturn { const triggerAutoClose = () => singleModeAutoClose.value && onAutoClose() @@ -177,13 +179,25 @@ export function useSelectModel({ } function toggleSelection(value: T): void { - isValueSelected(value) ? unselect(value) : select(value) + if (!isValueSelected(value)) { + select(value) + } else { + if ( + mandatory?.value && + ((Array.isArray(model.value) && model.value.length === 1) || (model.value && !multiple.value)) + ) + return + + unselect(value) + } } function toggleGroupSelection(optionGroup: SelectOptionGroup): void { const optionGroupValues = optionGroup.items.map((x) => x.value) if (isGroupSelected(optionGroup)) { + if (mandatory?.value && Array.isArray(model.value) && model.value.length === optionGroup.items.length) return + const optionGroupSet = new Set(optionGroupValues) const newModel = modelAsArray.value.filter((x) => !optionGroupSet.has(x)) modelChangedManually = true diff --git a/packages/ui/stories/components/select/common.ts b/packages/ui/stories/components/select/common.ts index 1920e9a0..ee193ceb 100644 --- a/packages/ui/stories/components/select/common.ts +++ b/packages/ui/stories/components/select/common.ts @@ -69,6 +69,7 @@ export const COMMON_ARGS = { noAutoClose: false, dropdownSearch: false, maxShownOptions: 0, + mandatory: false, } export const COMMON_ARG_TYPES = {