diff --git a/packages/documentation/pages/usage/components/filters.vue b/packages/documentation/pages/usage/components/filters.vue
index 8a35fbd186..923b279beb 100644
--- a/packages/documentation/pages/usage/components/filters.vue
+++ b/packages/documentation/pages/usage/components/filters.vue
@@ -273,6 +273,7 @@ export default defineComponent({
const component: ComponentValue = {
hasActions: false,
hasHelpTextSlot: false,
+ hasOptionSlot: false,
name: 'KtFilters',
props: cloneDeep(componentProps.value),
validation: 'empty',
diff --git a/packages/documentation/pages/usage/components/form-fields.vue b/packages/documentation/pages/usage/components/form-fields.vue
index 26d6e272ca..3ad2c7c5ff 100644
--- a/packages/documentation/pages/usage/components/form-fields.vue
+++ b/packages/documentation/pages/usage/components/form-fields.vue
@@ -19,7 +19,11 @@
Component
-
+
-
Supports
HTML via
-
<template v-slot:helpText>
+
<template #helpText>
-
+
+
+
+
+ {{ option.label }}
+
+
Default Slot
@@ -137,6 +153,111 @@
]"
type="switch"
/>
+
+
+
+
Texts
+
+
+
+
+
+
+
+
+
+
+
Decoration
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
Texts
-
-
-
-
-
-
-
-
-
-
Decoration
-
-
-
-
-
-
-
-
+
@@ -430,6 +489,7 @@ import {
KtFieldDateTime,
KtFieldDateTimeRange,
KtFieldMultiSelect,
+ KtFieldMultiSelectRemote,
KtFieldNumber,
KtFieldPassword,
KtFieldRadioGroup,
@@ -441,6 +501,7 @@ import {
KtFieldToggleGroup,
} from '@3yourmind/kotti-ui'
import { Yoco } from '@3yourmind/yoco'
+import { TimeConversion } from '@metatypes/units'
import { defineComponent, ref, computed } from '@vue/composition-api'
import cloneDeep from 'lodash/cloneDeep'
@@ -508,11 +569,31 @@ const components: Array<{
supports: KtFieldDateTimeRange.meta.supports,
},
{
- additionalProps: ['actions', 'collapseTagsAfter', 'maximumSelectable'],
+ additionalProps: [
+ 'actions',
+ 'collapseTagsAfter',
+ 'hasOptionSlot',
+ 'isUnsorted',
+ 'maximumSelectable',
+ ],
formKey: 'multiSelectValue',
name: 'KtFieldMultiSelect',
supports: KtFieldMultiSelect.meta.supports,
},
+ {
+ additionalProps: [
+ 'actions',
+ 'collapseTagsAfter',
+ 'hasOptionSlot',
+ 'isLoadingOptions',
+ 'isUnsorted',
+ 'maximumSelectable',
+ 'query',
+ ],
+ formKey: 'multiSelectValue',
+ name: 'KtFieldMultiSelectRemote',
+ supports: KtFieldMultiSelectRemote.meta.supports,
+ },
{
additionalProps: [
'numberDecimalPlaces',
@@ -539,13 +620,19 @@ const components: Array<{
supports: KtFieldRadioGroup.meta.supports,
},
{
- additionalProps: ['actions'],
+ additionalProps: ['actions', 'hasOptionSlot', 'isUnsorted'],
formKey: 'singleSelectValue',
name: 'KtFieldSingleSelect',
supports: KtFieldSingleSelect.meta.supports,
},
{
- additionalProps: ['actions', 'isLoadingOptions', 'query'],
+ additionalProps: [
+ 'actions',
+ 'hasOptionSlot',
+ 'isLoadingOptions',
+ 'isUnsorted',
+ 'query',
+ ],
formKey: 'singleSelectValue',
name: 'KtFieldSingleSelectRemote',
supports: KtFieldSingleSelectRemote.meta.supports,
@@ -635,10 +722,15 @@ const radioGroupOptions: Kotti.FieldRadioGroup.Props['options'] = [
]
const singleOrMultiSelectOptions: Kotti.FieldSingleSelect.Props['options'] = [
- { label: 'Key 1', value: 'value1' },
{ label: 'Key 2', value: 'value2' },
+ { label: 'Key 1', value: 'value1' },
{ isDisabled: true, label: 'Key 3', value: 'value3' },
+ { label: 'Key 7', value: 'value7' },
{ label: 'Key 4', value: 'value4' },
+ { label: 'Key 9', value: 'value9' },
+ { label: 'Key 6', value: 'value6' },
+ { label: 'Key 8', value: 'value8' },
+ { label: 'Key 5', value: 'value5' },
]
const toggleGroupOptions: Kotti.FieldToggleGroup.Props['options'] = [
@@ -674,9 +766,11 @@ export default defineComponent({
collapseTagsAfter: Kotti.FieldNumber.Value
currencyCurrency: string
hasActions: boolean
+ hasOptionSlot: boolean
hideChangeButtons: boolean
isInline: boolean
isLoadingOptions: boolean
+ isUnsorted: boolean
maximumDate: Kotti.FieldDate.Value
maximumSelectable: Kotti.FieldNumber.Value
minimumDate: Kotti.FieldDate.Value
@@ -708,8 +802,9 @@ export default defineComponent({
placeholder2: Kotti.FieldText.Value
prefix: Kotti.FieldText.Value
rightIcon: Yoco.Icon | null
- size: 'small' | 'medium' | 'large'
+ size: Kotti.Field.Size
suffix: Kotti.FieldText.Value
+ tabIndex: Kotti.FieldNumber.Value
validation: Kotti.Field.Validation.Result['type']
}>({
additionalProps: {
@@ -717,9 +812,11 @@ export default defineComponent({
collapseTagsAfter: null,
currencyCurrency: 'USD',
hasActions: false,
+ hasOptionSlot: false,
hideChangeButtons: false,
isInline: false,
isLoadingOptions: false,
+ isUnsorted: false,
maximumDate: null,
maximumSelectable: null,
minimumDate: null,
@@ -751,8 +848,9 @@ export default defineComponent({
placeholder2: null,
prefix: null,
rightIcon: null,
- size: 'medium',
+ size: Kotti.Field.Size.MEDIUM,
suffix: null,
+ tabIndex: null,
validation: 'empty',
})
@@ -818,6 +916,11 @@ export default defineComponent({
placeholder: settings.value.placeholder,
})
+ if (componentDefinition.value.supports.tabIndex)
+ Object.assign(additionalProps, {
+ tabIndex: settings.value.tabIndex,
+ })
+
if (
componentDefinition.value.additionalProps.includes('toggleType') &&
settings.value.additionalProps.toggleType !== 'checkbox' // Exclude Default Value
@@ -895,13 +998,21 @@ export default defineComponent({
isLoadingOptions: settings.value.additionalProps.isLoadingOptions,
})
+ if (componentDefinition.value.additionalProps.includes('isUnsorted'))
+ Object.assign(additionalProps, {
+ isUnsorted: settings.value.additionalProps.isUnsorted,
+ })
+
if (componentDefinition.value.additionalProps.includes('maximumDate'))
Object.assign(additionalProps, {
maximumDate: settings.value.additionalProps.maximumDate,
})
if (
- componentDefinition.value.additionalProps.includes('maximumSelectable')
+ componentDefinition.value.additionalProps.includes(
+ 'maximumSelectable',
+ ) &&
+ settings.value.additionalProps.maximumSelectable !== null
)
Object.assign(additionalProps, {
maximumSelectable: settings.value.additionalProps.maximumSelectable,
@@ -925,12 +1036,15 @@ export default defineComponent({
if (
[
'KtFieldMultiSelect',
+ 'KtFieldMultiSelectRemote',
'KtFieldSingleSelect',
'KtFieldSingleSelectRemote',
].includes(component)
) {
const options = (
- component === 'KtFieldSingleSelectRemote'
+ ['KtFieldMultiSelectRemote', 'KtFieldSingleSelectRemote'].includes(
+ component,
+ )
? singleOrMultiSelectOptions.filter((option) =>
option.label
.toLowerCase()
@@ -939,9 +1053,9 @@ export default defineComponent({
),
)
: singleOrMultiSelectOptions
- ).map((option) => ({
+ ).map((option, index) => ({
...option,
- dataTest: `${String(option.value)}`,
+ dataTest: index % 2 === 0 ? `${String(option.value)}` : undefined,
}))
Object.assign(additionalProps, {
@@ -949,7 +1063,11 @@ export default defineComponent({
})
}
- if (['KtFieldSingleSelectRemote'].includes(component)) {
+ if (
+ ['KtFieldMultiSelectRemote', 'KtFieldSingleSelectRemote'].includes(
+ component,
+ )
+ ) {
Object.assign(additionalProps, { query: remoteSingleSelectQuery.value })
}
@@ -987,6 +1105,7 @@ export default defineComponent({
(): ComponentValue => ({
hasActions: settings.value.additionalProps.hasActions,
hasHelpTextSlot: settings.value.hasHelpTextSlot,
+ hasOptionSlot: settings.value.additionalProps.hasOptionSlot,
name: settings.value.component,
props: cloneDeep(componentProps.value),
validation: settings.value.validation,
@@ -1003,6 +1122,7 @@ export default defineComponent({
KtFieldDateTimeRange,
KtFieldNumber,
KtFieldMultiSelect,
+ KtFieldMultiSelectRemote,
KtFieldPassword,
KtFieldRadioGroup,
KtFieldSingleSelect,
@@ -1034,6 +1154,8 @@ export default defineComponent({
}),
),
isRangeComponent,
+ onSubmit: (values: Record) =>
+ window.alert(`@submit: ${JSON.stringify(values, null, '\t')}`),
reset: () => {
values.value = INITIAL_VALUES
},
@@ -1065,6 +1187,16 @@ export default defineComponent({
newQuery: Kotti.FieldSingleSelectRemote.Events.UpdateQuery,
) => {
remoteSingleSelectQuery.value = newQuery
+ settings.value.additionalProps = {
+ ...settings.value.additionalProps,
+ isLoadingOptions: true,
+ }
+ setTimeout(() => {
+ settings.value.additionalProps = {
+ ...settings.value.additionalProps,
+ isLoadingOptions: false,
+ }
+ }, TimeConversion.MILLISECONDS_PER_SECOND)
},
values,
yocoIconOptions: Object.values(Yoco.Icon).map((icon) => ({
diff --git a/packages/documentation/pages/utilities.ts b/packages/documentation/pages/utilities.ts
index c8da0b26e6..d0384a849f 100644
--- a/packages/documentation/pages/utilities.ts
+++ b/packages/documentation/pages/utilities.ts
@@ -6,6 +6,7 @@ export type ComponentNames =
| 'KtFieldDateTime'
| 'KtFieldDateTimeRange'
| 'KtFieldMultiSelect'
+ | 'KtFieldMultiSelectRemote'
| 'KtFieldNumber'
| 'KtFieldPassword'
| 'KtFieldRadioGroup'
@@ -20,6 +21,7 @@ export type ComponentNames =
export type ComponentValue = {
hasActions: boolean
hasHelpTextSlot: boolean
+ hasOptionSlot: boolean
name: ComponentNames
props: Record
validation: Kotti.Field.Validation.Result['type']
diff --git a/packages/kotti-ui/source/index.ts b/packages/kotti-ui/source/index.ts
index 51aa6cee28..35062447fa 100644
--- a/packages/kotti-ui/source/index.ts
+++ b/packages/kotti-ui/source/index.ts
@@ -44,8 +44,9 @@ export * from './kotti-field-password'
import { KtFieldRadioGroup } from './kotti-field-radio-group'
export * from './kotti-field-radio-group'
import {
- KtFieldSingleSelect,
KtFieldMultiSelect,
+ KtFieldMultiSelectRemote,
+ KtFieldSingleSelect,
KtFieldSingleSelectRemote,
} from './kotti-field-select'
export * from './kotti-field-select'
@@ -132,6 +133,7 @@ export default {
KtFieldDateTime,
KtFieldDateTimeRange,
KtFieldMultiSelect,
+ KtFieldMultiSelectRemote,
KtFieldNumber,
KtFieldPassword,
KtFieldRadioGroup,
diff --git a/packages/kotti-ui/source/kotti-avatar/KtAvatar.vue b/packages/kotti-ui/source/kotti-avatar/KtAvatar.vue
index a91c310099..45d4986a13 100644
--- a/packages/kotti-ui/source/kotti-avatar/KtAvatar.vue
+++ b/packages/kotti-ui/source/kotti-avatar/KtAvatar.vue
@@ -19,10 +19,10 @@ import { Yoco } from '@3yourmind/yoco'
import { computed, defineComponent, Ref, ref } from '@vue/composition-api'
import { roundArrow } from 'tippy.js'
+import { TIPPY_LIGHT_BORDER_ARROW_HEIGHT } from '../constants'
import { makeProps } from '../make-props'
import { KottiAvatar } from './types'
-const ARROW_HEIGHT = 7
const useTooltip = (name: Ref) => {
const tooltipTriggerRef = ref(null)
@@ -33,7 +33,7 @@ const useTooltip = (name: Ref) => {
appendTo: () => document.body,
arrow: roundArrow,
content: name.value,
- offset: [0, ARROW_HEIGHT],
+ offset: [0, TIPPY_LIGHT_BORDER_ARROW_HEIGHT],
theme: 'light-border',
...(name.value === null ? { trigger: 'manual' } : {}),
})),
diff --git a/packages/kotti-ui/source/kotti-field-select/KtFieldMultiSelect.vue b/packages/kotti-ui/source/kotti-field-select/KtFieldMultiSelect.vue
index 6b621c32cb..7a8e1b1f5f 100644
--- a/packages/kotti-ui/source/kotti-field-select/KtFieldMultiSelect.vue
+++ b/packages/kotti-ui/source/kotti-field-select/KtFieldMultiSelect.vue
@@ -1,229 +1,37 @@
-
-
-
-
-
(isDropdownOpen = showPopper)"
- >
-
-
-
-
-
-
-
-
+
+
-
+
-
-
diff --git a/packages/kotti-ui/source/kotti-field-select/KtFieldMultiSelectRemote.vue b/packages/kotti-ui/source/kotti-field-select/KtFieldMultiSelectRemote.vue
new file mode 100644
index 0000000000..19586d0c6d
--- /dev/null
+++ b/packages/kotti-ui/source/kotti-field-select/KtFieldMultiSelectRemote.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/kotti-ui/source/kotti-field-select/KtFieldSingleSelect.vue b/packages/kotti-ui/source/kotti-field-select/KtFieldSingleSelect.vue
index 4017ef066f..0502a68a3b 100644
--- a/packages/kotti-ui/source/kotti-field-select/KtFieldSingleSelect.vue
+++ b/packages/kotti-ui/source/kotti-field-select/KtFieldSingleSelect.vue
@@ -1,163 +1,36 @@
-
-
-
-
-
(isDropdownOpen = showPopper)"
- >
-
-
-
-
-
-
-
+
+
-
+
-
-
diff --git a/packages/kotti-ui/source/kotti-field-select/KtFieldSingleSelectRemote.vue b/packages/kotti-ui/source/kotti-field-select/KtFieldSingleSelectRemote.vue
index 7c8d67e515..72b622a9d6 100644
--- a/packages/kotti-ui/source/kotti-field-select/KtFieldSingleSelectRemote.vue
+++ b/packages/kotti-ui/source/kotti-field-select/KtFieldSingleSelectRemote.vue
@@ -1,280 +1,37 @@
-
-
-
-
- selectOption(option)"
- />
-
-
+
+
+
+
+
-
-
diff --git a/packages/kotti-ui/source/kotti-field-select/components/ActionIcon.vue b/packages/kotti-ui/source/kotti-field-select/components/ActionIcon.vue
index a068a81fd8..c0be40bf74 100644
--- a/packages/kotti-ui/source/kotti-field-select/components/ActionIcon.vue
+++ b/packages/kotti-ui/source/kotti-field-select/components/ActionIcon.vue
@@ -2,66 +2,45 @@
-
+
diff --git a/packages/kotti-ui/source/kotti-field-select/components/GenericSelectField.vue b/packages/kotti-ui/source/kotti-field-select/components/GenericSelectField.vue
new file mode 100644
index 0000000000..5b0f19546f
--- /dev/null
+++ b/packages/kotti-ui/source/kotti-field-select/components/GenericSelectField.vue
@@ -0,0 +1,398 @@
+
+
+
+
+
+
+
diff --git a/packages/kotti-ui/source/kotti-field-select/components/Options.vue b/packages/kotti-ui/source/kotti-field-select/components/Options.vue
new file mode 100644
index 0000000000..9a04293708
--- /dev/null
+++ b/packages/kotti-ui/source/kotti-field-select/components/Options.vue
@@ -0,0 +1,252 @@
+
+
+
+
+
selectOption(option)"
+ @scrollTo="scrollTo"
+ >
+
+
+
+
onAction(action)"
+ @scrollTo="scrollTo"
+ />
+
+
+
+
+
+
diff --git a/packages/kotti-ui/source/kotti-field-select/components/OptionsItem.vue b/packages/kotti-ui/source/kotti-field-select/components/OptionsItem.vue
new file mode 100644
index 0000000000..4b7e8bc4a9
--- /dev/null
+++ b/packages/kotti-ui/source/kotti-field-select/components/OptionsItem.vue
@@ -0,0 +1,99 @@
+
+ $emit('click', e)"
+ >
+
+ {{ label }}
+
+
+
+
+
+
+
diff --git a/packages/kotti-ui/source/kotti-field-select/constants.ts b/packages/kotti-ui/source/kotti-field-select/constants.ts
index 92d53e8655..10c9e9da42 100644
--- a/packages/kotti-ui/source/kotti-field-select/constants.ts
+++ b/packages/kotti-ui/source/kotti-field-select/constants.ts
@@ -1,15 +1,8 @@
import { KottiField } from '../kotti-field/types'
-export const KOTTI_FIELD_REMOTE_SELECT_SUPPORTS: KottiField.Supports = {
- clear: true,
- decoration: true,
- placeholder: true,
- tabIndex: true,
-}
-
export const KOTTI_FIELD_SELECT_SUPPORTS: KottiField.Supports = {
clear: true,
decoration: true,
placeholder: true,
- tabIndex: false,
+ tabIndex: true,
}
diff --git a/packages/kotti-ui/source/kotti-field-select/hooks.ts b/packages/kotti-ui/source/kotti-field-select/hooks.ts
deleted file mode 100644
index 8468cd1877..0000000000
--- a/packages/kotti-ui/source/kotti-field-select/hooks.ts
+++ /dev/null
@@ -1,221 +0,0 @@
-import { onMounted, Ref, watchEffect } from '@vue/composition-api'
-import { Select as ElSelect } from 'element-ui'
-
-import { KottiField } from '../kotti-field/types'
-
-import { KottiFieldSingleSelect, KottiFieldMultiSelect } from './types'
-
-export type ElSelectWithInternalAPI = ElSelect & {
- inputWidth: number
- query: string
- setSoftFocus(): void
- visible: boolean
-}
-
-type HookParameters = {
- elSelectRef: Ref
- field: KottiField.Hook.Returns
- inputSelectors: string[]
- ktFieldRef: Ref
-}
-
-type Values = KottiFieldSingleSelect.Value | KottiFieldMultiSelect.Value
-
-const getComponents = ({
- elSelectRef,
- ktFieldRef,
-}: Pick, 'elSelectRef' | 'ktFieldRef'>) => {
- const elSelectComponent = elSelectRef.value
- const ktFieldComponent = ktFieldRef.value
-
- if (elSelectComponent === null) throw new Error('el-select not available')
-
- if (ktFieldComponent === null) throw new Error('kt-field not available')
-
- return {
- elSelectComponent: elSelectComponent as ElSelectWithInternalAPI,
- ktFieldComponent: ktFieldComponent as Vue,
- }
-}
-
-/**
- * Fixes the dropdown from appearing behind `KtPopover`
- */
-const useNestedPopperZIndexFix = ({
- elSelectRef,
- ktFieldRef,
-}: Pick, 'elSelectRef' | 'ktFieldRef'>) => {
- onMounted(() => {
- watchEffect(() => {
- const { elSelectComponent } = getComponents({ elSelectRef, ktFieldRef })
-
- if (elSelectComponent.visible) {
- const TIPPY_Z_INDEX = 9999
- const popperComponent = elSelectComponent.$refs.popper as Vue
- const popperElement = popperComponent.$el as HTMLElement
- popperElement.style.zIndex = String(TIPPY_Z_INDEX + 1)
- }
- })
- })
-}
-
-/**
- * If the field is loading, we want to unfocus in case the popper is open
- * so that when isLoading changes, the popper isn't misplaced
- */
-const usePopperMisplacementFix = ({
- elSelectRef,
- field,
- ktFieldRef,
-}: Pick, 'elSelectRef' | 'field' | 'ktFieldRef'>) => {
- onMounted(() => {
- watchEffect(() => {
- const { elSelectComponent } = getComponents({ elSelectRef, ktFieldRef })
- /**
- * [select.vue]{@link https://github.com/ElemeFE/element/blob/649670c55a45c7343eb7148565e2d873bc3d52dd/src/utils/vue-popper.js#L72 }
- * showPopper triggers on change of `visible`, which emits `updatePopper` which recomputes width/placement
- */
- elSelectComponent.visible
- if (field.isLoading || field.isDisabled) return elSelectComponent.blur()
- })
- })
-}
-
-/**
- * `popperComponent` is an internal `element-ui` component that computes the placement
- * of the dropdown based on the input element of `el-select`.
- *
- * [select.vue]{@link ./node_modules/element-ui/packages/select/src/select.vue} adds `ref="reference"`
- * to the input.
- *
- * [vue-popper.js]{@link ./node_modules/element-ui/src/utils/vue-popper.js} uses `parent.$ref.reference`
- * to obtain the `referenceElm`.
- *
- * So, here, we overwrite the internal property `referenceElm` of the component, to place the dropdown
- * in accordance to our input component instead (which is accessed by the `$refs.inputContainerRef`)
- */
-const usePopperPlacementFix = ({
- elSelectRef,
- ktFieldRef,
-}: Pick, 'elSelectRef' | 'ktFieldRef'>) => {
- onMounted(() => {
- const { elSelectComponent, ktFieldComponent } = getComponents({
- elSelectRef,
- ktFieldRef,
- })
-
- const popperComponent = elSelectComponent.$refs.popper as Vue & {
- referenceElm: Element
- }
- const ktFieldContainerElement = ktFieldComponent.$refs
- .inputContainerRef as Element
-
- popperComponent.referenceElm = ktFieldContainerElement
- })
-}
-
-const usePopperWidthFix = ({
- elSelectRef,
- ktFieldRef,
-}: Pick, 'elSelectRef' | 'ktFieldRef' | 'field'>) => {
- onMounted(() => {
- watchEffect(() => {
- const { elSelectComponent, ktFieldComponent } = getComponents({
- elSelectRef,
- ktFieldRef,
- })
-
- // register dependencies
- elSelectComponent.inputWidth
- /**
- * [select.vue]{@link https://github.com/ElemeFE/element/blob/649670c55a45c7343eb7148565e2d873bc3d52dd/src/utils/vue-popper.js#L72 }
- * showPopper triggers on change of `visible`, which emits `updatePopper` which recomputes width/placement
- */
- elSelectComponent.visible
-
- const ktFieldContainerElement = ktFieldComponent.$refs
- .inputContainerRef as Element
- const newWidth = ktFieldContainerElement.getBoundingClientRect().width
-
- const popperComponent = elSelectComponent.$refs.popper as Vue
- const popperElement = popperComponent.$el as HTMLElement
-
- popperElement.style.width = `${newWidth}px`
-
- elSelectComponent.inputWidth = newWidth
- })
- })
-}
-
-/**
- * adds size attribute to avoid overflow of icons
- */
-const useSelectInputSizeFix = ({
- elSelectRef,
- inputSelectors,
- ktFieldRef,
-}: Pick<
- HookParameters,
- 'elSelectRef' | 'ktFieldRef' | 'inputSelectors'
->) => {
- onMounted(() => {
- const { ktFieldComponent } = getComponents({ elSelectRef, ktFieldRef })
-
- const ktFieldContainerElement = ktFieldComponent.$refs
- .inputContainerRef as Element
-
- inputSelectors.forEach((query) =>
- ktFieldContainerElement.querySelector(query)?.setAttribute('size', '1'),
- )
- })
-}
-
-/**
- * @summary HACK: Prevent Google Chrome from autocompleting the search field
- *
- * @description
- * chrome 66+ shows suggestions for saved autofill data,
- * through intelligent detection, recognizing "address" fields, for instance,
- * and suggesting autofills.
- *
- * using `autocomplete="off" on `input` of `type="text"` would not work to disable autofill
- *
- * @see {@link https://gist.github.com/niksumeiko/360164708c3b326bd1c8#gistcomment-2032962} for suggested fixes
- * @see {@link https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill-expectation-mantle} for more information on the autocomplete attribute and accepted values
- * @see {@link https://bugs.chromium.org/p/chromium/issues/detail?id=370363#c7} for more information on the autocomplete tokens for password fields
- */
-const useSelectInputTypeFix = ({
- elSelectRef,
- inputSelectors,
- ktFieldRef,
-}: Pick<
- HookParameters,
- 'elSelectRef' | 'inputSelectors' | 'ktFieldRef'
->) => {
- onMounted(() => {
- const { ktFieldComponent } = getComponents({ elSelectRef, ktFieldRef })
-
- const ktFieldContainerElement = ktFieldComponent.$refs
- .inputContainerRef as Element
-
- inputSelectors.forEach((query) =>
- ktFieldContainerElement
- .querySelector(query)
- ?.setAttribute('type', 'search'),
- )
- })
-}
-
-export const useSelectFixes = ({
- elSelectRef,
- field,
- inputSelectors,
- ktFieldRef,
-}: HookParameters) => {
- useNestedPopperZIndexFix({ elSelectRef, ktFieldRef })
- usePopperMisplacementFix({ elSelectRef, field, ktFieldRef })
- usePopperPlacementFix({ elSelectRef, ktFieldRef })
- usePopperWidthFix({ elSelectRef, field, ktFieldRef })
- useSelectInputSizeFix({ elSelectRef, inputSelectors, ktFieldRef })
- useSelectInputTypeFix({ elSelectRef, inputSelectors, ktFieldRef })
-}
diff --git a/packages/kotti-ui/source/kotti-field-select/hooks/use-select-tippy.ts b/packages/kotti-ui/source/kotti-field-select/hooks/use-select-tippy.ts
new file mode 100644
index 0000000000..689271d642
--- /dev/null
+++ b/packages/kotti-ui/source/kotti-field-select/hooks/use-select-tippy.ts
@@ -0,0 +1,72 @@
+import { useTippy } from '@3yourmind/vue-use-tippy'
+import { computed, ref } from '@vue/composition-api'
+import castArray from 'lodash.castarray'
+import { roundArrow } from 'tippy.js'
+
+import { TIPPY_LIGHT_BORDER_ARROW_HEIGHT } from '../../constants'
+import { sameWidth } from '../utils/tippy-utils'
+
+export const useSelectTippy = () => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const tippyTriggerRef = ref(null)
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const tippyContentRef = ref(null)
+
+ // track in a ref because the `tippy.state.isShown` doesn’t immediately update
+ const isDropdownOpen = ref(false)
+ const isDropdownMounted = ref(true)
+
+ const { tippy } = useTippy(
+ tippyTriggerRef,
+ computed(() => ({
+ appendTo: () => document.body,
+ arrow: roundArrow,
+ content: tippyContentRef.value,
+ // hides the tippy if we click-away from the tippy
+ hideOnClick: false,
+ interactive: true,
+ maxWidth: 'none',
+ offset: [0, TIPPY_LIGHT_BORDER_ARROW_HEIGHT],
+ onClickOutside: () => {
+ setIsDropdownOpen(false)
+ },
+ onShow: () => {
+ // More correct here, don't move to `onShown()`
+ isDropdownMounted.value = true
+
+ isDropdownOpen.value = true
+ },
+ onHide: () => {
+ isDropdownOpen.value = false
+ },
+ onHidden: () => {
+ isDropdownMounted.value = false
+ },
+ placement: 'bottom',
+ popperOptions: {
+ modifiers: [sameWidth],
+ },
+ theme: 'light-border',
+ trigger: 'click focusin',
+ })),
+ )
+
+ const setIsDropdownOpen = (showTippy: boolean) => {
+ if (!tippy.value) return
+
+ const tippys = castArray(tippy.value)
+
+ for (const tippy of tippys) {
+ if (showTippy) tippy.show()
+ else tippy.hide()
+ }
+ }
+
+ return {
+ isDropdownMounted,
+ isDropdownOpen,
+ setIsDropdownOpen,
+ tippyContentRef,
+ tippyTriggerRef,
+ }
+}
diff --git a/packages/kotti-ui/source/kotti-field-select/index.ts b/packages/kotti-ui/source/kotti-field-select/index.ts
index 2c623d45dd..e59c1e47ee 100644
--- a/packages/kotti-ui/source/kotti-field-select/index.ts
+++ b/packages/kotti-ui/source/kotti-field-select/index.ts
@@ -1,13 +1,15 @@
import { FIELD_META_BASE_SLOTS } from '../kotti-field/meta'
-import { MetaDesignType } from '../types/kotti'
+import { Meta, MetaDesignType } from '../types/kotti'
import { attachMeta, makeInstallable } from '../utilities'
import { KOTTI_FIELD_SELECT_SUPPORTS } from './constants'
import KtFieldMultiSelectVue from './KtFieldMultiSelect.vue'
+import KtFieldMultiSelectRemoteVue from './KtFieldMultiSelectRemote.vue'
import KtFieldSingleSelectVue from './KtFieldSingleSelect.vue'
import KtFieldSingleSelectRemoteVue from './KtFieldSingleSelectRemote.vue'
import {
KottiFieldMultiSelect,
+ KottiFieldMultiSelectRemote,
KottiFieldSingleSelect,
KottiFieldSingleSelectRemote,
} from './types'
@@ -15,6 +17,27 @@ import {
const url =
'https://www.figma.com/file/0yFVivSWXgFf2ddEF92zkf/Kotti-Design-System?node-id=428%3A3482'
+const slots: Meta['slots'] = {
+ ...FIELD_META_BASE_SLOTS,
+ option: {
+ description: null,
+ scope: {
+ index: {
+ description: 'option index (after filtering)',
+ type: 'integer',
+ },
+ select: {
+ description: 'select the current option',
+ type: 'function',
+ },
+ option: {
+ description: 'the entire option entry',
+ type: 'object',
+ },
+ },
+ },
+}
+
export const KtFieldSingleSelect = attachMeta(
makeInstallable(KtFieldSingleSelectVue),
{
@@ -24,7 +47,7 @@ export const KtFieldSingleSelect = attachMeta(
type: MetaDesignType.FIGMA,
url,
},
- slots: FIELD_META_BASE_SLOTS,
+ slots,
typeScript: {
namespace: 'Kotti.FieldSingleSelect',
schema: KottiFieldSingleSelect.propsSchema,
@@ -42,7 +65,7 @@ export const KtFieldSingleSelectRemote = attachMeta(
type: MetaDesignType.FIGMA,
url,
},
- slots: FIELD_META_BASE_SLOTS,
+ slots,
typeScript: {
namespace: 'Kotti.FieldSingleSelectRemote',
schema: KottiFieldSingleSelectRemote.propsSchema,
@@ -60,7 +83,7 @@ export const KtFieldMultiSelect = attachMeta(
type: MetaDesignType.FIGMA,
url,
},
- slots: FIELD_META_BASE_SLOTS,
+ slots,
typeScript: {
namespace: 'Kotti.FieldMultiSelect',
schema: KottiFieldMultiSelect.propsSchema,
@@ -69,4 +92,22 @@ export const KtFieldMultiSelect = attachMeta(
{ supports: KOTTI_FIELD_SELECT_SUPPORTS },
)
+export const KtFieldMultiSelectRemote = attachMeta(
+ makeInstallable(KtFieldMultiSelectRemoteVue),
+ {
+ addedVersion: '3.0.0',
+ deprecated: null,
+ designs: {
+ type: MetaDesignType.FIGMA,
+ url,
+ },
+ slots,
+ typeScript: {
+ namespace: 'Kotti.FieldMultiSelectRemote',
+ schema: KottiFieldMultiSelectRemote.propsSchema,
+ },
+ },
+ { supports: KOTTI_FIELD_SELECT_SUPPORTS },
+)
+
export * from './constants'
diff --git a/packages/kotti-ui/source/kotti-field-select/styles.scss b/packages/kotti-ui/source/kotti-field-select/styles.scss
deleted file mode 100644
index 1d8a743e3b..0000000000
--- a/packages/kotti-ui/source/kotti-field-select/styles.scss
+++ /dev/null
@@ -1,220 +0,0 @@
-@import '../kotti-field/mixins.scss';
-
-.kt-field-select {
- .el-select {
- position: relative;
- display: flex;
-
- .el-input {
- display: flex;
-
- &__inner {
- padding: 0;
- margin: 0;
- border: 0;
- }
- }
- }
-
- &--single {
- .el-input {
- flex: 1;
- }
-
- .el-input__inner {
- display: flex;
- align-self: center;
- width: 100%;
- line-height: 1.6;
- }
- }
-
- &__actions {
- &:before {
- display: block;
- height: 1px;
- margin: 0 0.6rem;
- content: '';
- background: var(--gray-20);
- }
-
- &__item {
- color: var(--interactive-01);
- cursor: pointer;
-
- &:hover {
- background-color: var(--ui-01);
- }
- }
- }
-
- &--multiple {
- $vertical-tag-gap: 2px;
- $horizontal-tag-gap: 4px;
- $tag-padding: 0.4em;
- $tag-border: 1px;
-
- .el-select {
- flex-direction: row-reverse;
- flex-wrap: wrap-reverse;
-
- &__tags {
- /* has the input we care about in multiple */
- display: inline-flex;
- flex: 1;
- line-height: normal;
- white-space: normal;
-
- > span {
- /* remove el-tag container */
- display: none;
- }
-
- // only in case of multiple, we have this input
- .el-select__input {
- padding: 0;
- margin: 0;
- margin-left: calc(#{$tag-padding} + #{$tag-border});
- line-height: 1.6;
- border: 0;
- appearance: none;
- }
- }
-
- .el-input {
- &__inner {
- // display: none would break focusing
- width: 0 !important;
- height: 0 !important;
- }
-
- &__prefix {
- width: 100%;
- }
- }
- }
-
- .kt-tags {
- display: flex;
- flex-wrap: wrap;
-
- // HACK: use negative margins to align multi-line grids of tags
- margin: #{-$vertical-tag-gap + 4px} #{-$horizontal-tag-gap};
-
- &__tag {
- display: flex;
- align-items: center;
- padding: $tag-padding;
- margin: $vertical-tag-gap $horizontal-tag-gap;
-
- font-size: 0.875em;
-
- color: var(--text-02);
- text-transform: capitalize;
- white-space: nowrap;
- background-color: var(--interactive-02);
- border: $tag-border solid var(--ui-02);
- border-radius: var(--field-border-radius);
-
- &-icon {
- $size: 1.25em;
-
- display: flex;
- align-items: center;
- justify-content: center;
- width: $size;
- height: $size;
- margin-left: 4px;
- cursor: pointer;
-
- background-color: var(--ui-02);
- border-radius: 50%;
-
- // clipping also affects the clickable area
- @supports (clip-path: circle(#{$size / 2} at center)) {
- clip-path: circle(#{$size / 2} at center);
- border-radius: 0;
- }
-
- &:hover {
- background-color: var(--interactive-02-hover);
- }
- }
- }
- }
-
- .kt-field-select__placeholder {
- color: var(--text-03);
- opacity: 0.54; // appears to be the default user-agent opacity from the browser, in this case Firefox Developer Edition
- }
- }
-}
-
-.kt-field__wrapper--disabled {
- .kt-tags__tag {
- color: var(--text-05);
- }
-}
-
-.el-select-dropdown,
-.el-select-dropdown.is-multiple {
- &__empty {
- width: 100%;
- padding: 10px 0;
- margin: 0;
- color: var(--text-05);
- text-align: center;
- }
-
- @include el-popper {
- @include el-scrollbar {
- .el-select-dropdown__list {
- box-sizing: border-box;
- display: flex;
- flex-direction: column;
- width: 100%;
- padding: 0;
- margin: 0;
- list-style: none;
-
- .el-select-dropdown__item {
- position: relative;
- box-sizing: border-box;
- flex: 1;
- padding: 0.4rem 0.6rem;
- margin: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- cursor: pointer;
-
- &.is-disabled {
- color: var(--text-05);
- cursor: not-allowed;
- }
-
- &.selected {
- font-weight: 700;
- color: var(--interactive-03);
-
- &.hover,
- &:hover {
- color: var(--interactive-01-hover);
- }
- }
-
- &:hover,
- &.hover {
- &:not(.is-disabled) {
- background-color: var(--ui-01);
- }
- }
- }
- }
- }
-
- .el-scrollbar {
- max-height: 40vh;
- }
- }
-}
diff --git a/packages/kotti-ui/source/kotti-field-select/types.ts b/packages/kotti-ui/source/kotti-field-select/types.ts
index 4ea84f5e48..04d08f333a 100644
--- a/packages/kotti-ui/source/kotti-field-select/types.ts
+++ b/packages/kotti-ui/source/kotti-field-select/types.ts
@@ -8,39 +8,43 @@ export namespace Shared {
.nullable()
export type Value = z.output
- export const entrySchema = z.object({
+ export const optionSchema = z.object({
dataTest: z.string().optional(),
isDisabled: z.boolean().optional(),
label: z.string(),
value: valueSchema,
})
- export type Entry = z.output
+ export type Option = z.output
- const actionSchema = z.object({
+ export const actionSchema = z.object({
label: z.string(),
onClick: z.function(z.tuple([]), z.void()),
})
export type Action = z.output
- export const propsSchema = z
- .object({
+ export const propsSchema = KottiField.propsSchema
+ .merge(KottiField.potentiallySupportedPropsSchema)
+ .extend({
actions: z.array(actionSchema).default(() => []),
- options: z.array(entrySchema),
+ isUnsorted: z.boolean().default(false),
+ options: z.array(optionSchema),
placeholder: z.string().nullable().default(null),
})
- /**
- * tabIndex is not supported due to element-ui limitation
- * TODO: support with new select components
- **/
- .merge(
- KottiField.potentiallySupportedPropsSchema.pick({
- hideClear: true,
- leftIcon: true,
- prefix: true,
- rightIcon: true,
- suffix: true,
- }),
- )
+
+ export const isMultipleSchema = z.object({
+ collapseTagsAfter: z.number().int().min(0).default(Number.MAX_SAFE_INTEGER),
+ maximumSelectable: z.number().int().min(0).default(Number.MAX_SAFE_INTEGER),
+ value: z.array(Shared.valueSchema).default(() => []),
+ })
+
+ export const isRemoteSchema = z.object({
+ isLoadingOptions: z.boolean().default(false),
+ query: z.string().nullable().default(null),
+ })
+
+ export const isSingleSchema = z.object({
+ value: Shared.valueSchema.default(null),
+ })
export type Props = z.input
export type PropsInternal = z.output
@@ -54,16 +58,10 @@ export namespace Shared {
}
export namespace KottiFieldMultiSelect {
- export const valueSchema = z.array(Shared.valueSchema)
+ export const valueSchema = Shared.isMultipleSchema.shape.value._def.innerType
export type Value = z.output
- export const propsSchema = KottiField.propsSchema
- .merge(Shared.propsSchema)
- .extend({
- collapseTagsAfter: z.number().default(Number.MAX_SAFE_INTEGER),
- maximumSelectable: z.number().default(Number.MAX_SAFE_INTEGER),
- value: valueSchema.default(() => []),
- })
+ export const propsSchema = Shared.propsSchema.merge(Shared.isMultipleSchema)
export type Props = z.input
export type PropsInternal = z.output
@@ -71,15 +69,29 @@ export namespace KottiFieldMultiSelect {
export type Translations = Shared.Translations
}
+export namespace KottiFieldMultiSelectRemote {
+ export const valueSchema = Shared.isMultipleSchema.shape.value._def.innerType
+ export type Value = z.output
+
+ export const propsSchema = Shared.propsSchema
+ .merge(Shared.isMultipleSchema)
+ .merge(Shared.isRemoteSchema)
+
+ export type Props = z.input
+ export type PropsInternal = z.output
+
+ export type Translations = Shared.Translations
+
+ export namespace Events {
+ export type UpdateQuery = KottiFieldMultiSelectRemote.Props['query']
+ }
+}
+
export namespace KottiFieldSingleSelect {
export const valueSchema = Shared.valueSchema
export type Value = z.output
- export const propsSchema = KottiField.propsSchema
- .merge(Shared.propsSchema)
- .extend({
- value: valueSchema.default(null),
- })
+ export const propsSchema = Shared.propsSchema.merge(Shared.isSingleSchema)
export type Props = z.input
export type PropsInternal = z.output
@@ -88,17 +100,12 @@ export namespace KottiFieldSingleSelect {
}
export namespace KottiFieldSingleSelectRemote {
- export const valueSchema = KottiFieldSingleSelect.valueSchema
+ export const valueSchema = Shared.valueSchema
export type Value = z.output
- export const propsSchema = KottiFieldSingleSelect.propsSchema
- // TODO: no need for this merge when element-ui is not used under the hood anymore
- // since we rely on the KottiFieldSingleSelect merge which would include tabIndex
- .merge(KottiField.potentiallySupportedPropsSchema.pick({ tabIndex: true }))
- .extend({
- isLoadingOptions: z.boolean().default(false),
- query: z.string().nullable().default(null),
- })
+ export const propsSchema = Shared.propsSchema
+ .merge(Shared.isSingleSchema)
+ .merge(Shared.isRemoteSchema)
export type Props = z.input
export type PropsInternal = z.output
diff --git a/packages/kotti-ui/source/kotti-field-toggle/KtFieldToggleGroup.vue b/packages/kotti-ui/source/kotti-field-toggle/KtFieldToggleGroup.vue
index 928e312da2..859936a897 100644
--- a/packages/kotti-ui/source/kotti-field-toggle/KtFieldToggleGroup.vue
+++ b/packages/kotti-ui/source/kotti-field-toggle/KtFieldToggleGroup.vue
@@ -43,7 +43,7 @@ export default defineComponent({
ToggleInner,
},
props: makeProps(KottiFieldToggleGroup.propsSchema),
- setup(props: KottiFieldToggleGroup.Props, { emit }) {
+ setup(props: KottiFieldToggleGroup.PropsInternal, { emit }) {
const field = useField({
emit,
isEmpty: (value) =>
diff --git a/packages/kotti-ui/source/kotti-field/mixins.scss b/packages/kotti-ui/source/kotti-field/mixins.scss
index ddc43771fd..3cce327fd0 100644
--- a/packages/kotti-ui/source/kotti-field/mixins.scss
+++ b/packages/kotti-ui/source/kotti-field/mixins.scss
@@ -406,6 +406,7 @@
&::-webkit-scrollbar {
width: 5px;
+ height: 5px;
opacity: 1;
transition: opacity var(--transition-medium) ease-out;
}
diff --git a/packages/kotti-ui/source/kotti-filters/KtFilters.vue b/packages/kotti-ui/source/kotti-filters/KtFilters.vue
index 76de9803d6..d8cc395a3d 100644
--- a/packages/kotti-ui/source/kotti-filters/KtFilters.vue
+++ b/packages/kotti-ui/source/kotti-filters/KtFilters.vue
@@ -50,6 +50,7 @@ import { Yoco } from '@3yourmind/yoco'
import { computed, defineComponent, ref } from '@vue/composition-api'
import { roundArrow } from 'tippy.js'
+import { TIPPY_LIGHT_BORDER_ARROW_HEIGHT } from '../constants'
import { useTranslationNamespace } from '../kotti-i18n/hooks'
import ButtonLink from './components/ButtonLink.vue'
@@ -59,9 +60,7 @@ import FilterSearch from './components/FilterSearch.vue'
import { KottiFilters } from './types'
import { isValidColumn } from './validators'
-const ARROW_HEIGHT = 7
-
-export default defineComponent({
+export default defineComponent({
name: 'KtFilters',
components: {
ButtonLink,
@@ -73,7 +72,7 @@ export default defineComponent({
columns: {
required: true,
type: Array,
- validator: (value: KottiFilters.InternalProps['columns']) =>
+ validator: (value: KottiFilters.PropsInternal['columns']) =>
value.every((column) => isValidColumn(column)),
},
dataTest: {
@@ -177,7 +176,7 @@ export default defineComponent({
hideOnClick: false,
interactive: true,
maxWidth: 'none',
- offset: [0, ARROW_HEIGHT],
+ offset: [0, TIPPY_LIGHT_BORDER_ARROW_HEIGHT],
onCreate(instance) {
tippyInstanceRef.value = instance
},
diff --git a/packages/kotti-ui/source/kotti-filters/types.ts b/packages/kotti-ui/source/kotti-filters/types.ts
index 003fbf04e9..d3a7cc692b 100644
--- a/packages/kotti-ui/source/kotti-filters/types.ts
+++ b/packages/kotti-ui/source/kotti-filters/types.ts
@@ -203,19 +203,19 @@ export namespace KottiFilters {
export type Value = Filter[]
- export type InternalProps = {
+ export type PropsInternal = {
columns: Column.Any[]
dataTest: string | null
isLoading: boolean
value: Value
}
- export type Props = SpecifyRequiredProps
+ export type Props = SpecifyRequiredProps
export namespace Events {
- export type UpdateColumns = InternalProps['columns']
+ export type UpdateColumns = PropsInternal['columns']
- export type UpdateValue = InternalProps['value']
+ export type UpdateValue = PropsInternal['value']
}
export type Translations = {
diff --git a/packages/kotti-ui/source/types/kotti.ts b/packages/kotti-ui/source/types/kotti.ts
index f7878d7e00..15a2847a21 100644
--- a/packages/kotti-ui/source/types/kotti.ts
+++ b/packages/kotti-ui/source/types/kotti.ts
@@ -32,8 +32,9 @@ export {
KottiFieldToggleGroup as FieldToggleGroup,
} from '../kotti-field-toggle/types'
export {
- KottiFieldSingleSelect as FieldSingleSelect,
KottiFieldMultiSelect as FieldMultiSelect,
+ KottiFieldMultiSelectRemote as FieldMultiSelectRemote,
+ KottiFieldSingleSelect as FieldSingleSelect,
KottiFieldSingleSelectRemote as FieldSingleSelectRemote,
} from '../kotti-field-select/types'
export { KottiFilters as Filters } from '../kotti-filters/types'
diff --git a/tsconfig.json b/tsconfig.json
index d1d7482f4a..44e2f48bc9 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -2,6 +2,7 @@
"compilerOptions": {
"esModuleInterop": true,
"lib": ["ESNext", "DOM", "DOM.Iterable"],
- "resolveJsonModule": true
+ "resolveJsonModule": true,
+ "strict": true
}
}