Skip to content

Commit

Permalink
feat(comp:select): add 'overlay' to searchable
Browse files Browse the repository at this point in the history
input is now rendered within overlay when searchable is 'overlay'

fix(comp:selectable): fix selected options render when using IxOption

refactor(comp:selectable): replace slot label with selectedLabel
replace slot maxLabel with overflowedLabel
  • Loading branch information
sallerli1 committed Jan 18, 2022
1 parent 91f501a commit be59d57
Show file tree
Hide file tree
Showing 13 changed files with 120 additions and 28 deletions.
4 changes: 2 additions & 2 deletions packages/components/select/demo/CustomLabel.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<IxSelect v-model:value="value" :options="options" multiple :maxLabelCount="3" :multipleLimit="5" @change="onChange">
<template #label="option"><IxIcon :name="option.value" />{{ option.label }}</template>
<template #maxLabel="moreOptions">and {{ moreOptions.length }} more selected</template>
<template #selectedLabel="option"><IxIcon :name="option.value" />{{ option.label }}</template>
<template #overflowedLabel="moreOptions">and {{ moreOptions.length }} more selected</template>
</IxSelect>
</template>
<script setup lang="ts">
Expand Down
23 changes: 21 additions & 2 deletions packages/components/select/demo/Searchable.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,29 @@
<template>
<IxSelect v-model:value="value" :options="options" searchable placeholder="Searchable" @change="onChange"> </IxSelect>
<IxSpace direction="vertical">
<IxSpace> searchable:<IxRadioGroup v-model:value="searchableValue" :options="searchableOptions" /></IxSpace>
<IxSelect
v-model:value="value"
:options="options"
:searchable="searchableValue"
placeholder="Searchable"
@change="onChange"
>
</IxSelect>
</IxSpace>
</template>
<script setup lang="ts">
import type { RadioOption } from '@idux/components/radio'
import type { SelectData } from '@idux/components/select'
import { ref } from 'vue'
import { SelectData } from '@idux/components/select'
const searchableValue = ref(true)
const searchableOptions: RadioOption[] = [
{ label: 'true', value: true },
{ label: 'overlay', value: 'overlay' },
{ label: 'false', value: false },
]
const options: SelectData[] = [
{ key: 1, label: 'Tom', value: 'tom' },
Expand Down
6 changes: 3 additions & 3 deletions packages/components/select/docs/Index.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ order: 0
| `overlayRender` | 自定义下拉菜单内容的渲染 | `(children:VNode[]) => VNodeTypes` | - | - | - |
| `placeholder` | 选择框默认文本 | `string \| #placeholder` | - | - | - |
| `readonly` | 只读模式 | `boolean` | - | - | - |
| `searchable` | 是否可搜索 | `boolean` | `false` | - | - |
| `searchable` | 是否可搜索 | `boolean \| 'overlay'` | `false` | - | 当为 `true` 时搜索功能集成在选择器上,当为 `overlay` 时,搜索功能集成在悬浮层上 |
| `searchFilter` | 根据搜索的文本进行筛选 | `boolean \| SelectFilterFn` | `true` | - |`true` 时使用默认的搜索规则, 如果使用远程搜索,应该设置为 `false` |
| `size` | 设置选择器大小 | `'sm' \| 'md' \| 'lg'` | `md` || - |
| `suffix` | 设置后缀图标 | `string \| #suffix` | `down` || - |
Expand Down Expand Up @@ -83,8 +83,8 @@ export type SelectFilterFn = (searchValue: string, data: SelectData) => boolean
| 名称 | 说明 | 参数类型 | 备注 |
| -- | -- | -- | -- |
| `default` | 选项内容 | - | - |
| `label` | 自定义选中的标签 | `data: SelectOption` | |
| `maxLabel` | 自定义超出最多显示多少个标签的内容 | `data: SelectOption[]` | 参数为超出的数组 |
| `selectedLabel` | 自定义选中的标签 | `data: SelectOption` | |
| `overflowedLabel` | 自定义超出最多显示多少个标签的内容 | `data: SelectOption[]` | 参数为超出的数组 |
| `optionLabel` | 自定义选项的文本 | `data: SelectOption` | - |
| `optionGroupLabel` | 自定义选项组的文本 | `data: SelectOptionGroup` | - |
Expand Down
28 changes: 27 additions & 1 deletion packages/components/select/src/content/Content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { defineComponent, inject, onMounted } from 'vue'
import { CdkVirtualScroll, VirtualItemRenderFn } from '@idux/cdk/scroll'
import { callEmit } from '@idux/cdk/utils'
import { ɵEmpty } from '@idux/components/_private/empty'
import { ɵInput } from '@idux/components/_private/input'

import { selectToken } from '../token'
import ListBox from './ListBox'
Expand All @@ -20,7 +21,17 @@ import OptionGroup from './OptionGroup'

export default defineComponent({
setup() {
const { props, slots, flattedOptions, virtualScrollRef, scrollToActivated } = inject(selectToken)!
const {
props,
slots,
mergedPrefixCls,
flattedOptions,
virtualScrollRef,
scrollToActivated,
inputValue,
handleInput,
handleClear,
} = inject(selectToken)!

onMounted(() => scrollToActivated())

Expand Down Expand Up @@ -64,6 +75,21 @@ export default defineComponent({
children.push(<ɵEmpty v-slots={slots} empty={props.empty} />)
}

if (props.searchable === 'overlay') {
children.unshift(
<div class={`${mergedPrefixCls.value}-overlay-search-wrapper`}>
<ɵInput
clearable
size="sm"
suffix="search"
value={inputValue.value}
onInput={handleInput}
onClear={handleClear}
/>
</div>,
)
}

return overlayRender ? overlayRender(children) : <div>{children}</div>
}
},
Expand Down
14 changes: 8 additions & 6 deletions packages/components/select/src/content/ListBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,28 @@
import type { MergedOption } from '../composables/useOptions'
import type { FunctionalComponent } from 'vue'

import { inject } from 'vue'
import { type Slots, inject } from 'vue'

import { selectToken } from '../token'
import { renderOptionLabel } from '../utils/renderOptionLabel'

const defaultStyle = { height: 0, width: 0, overflow: 'hidden' }

const ListBox: FunctionalComponent = () => {
const { props, selectedValue, mergedOptions, activeIndex, activeOption } = inject(selectToken)!
const { props, slots, selectedValue, mergedOptions, activeIndex, activeOption } = inject(selectToken)!
const currSelectedValue = selectedValue.value
const { compareWith } = props
return (
<div role="listbox" style={defaultStyle}>
{renderOption(mergedOptions.value[activeIndex.value - 1], currSelectedValue, compareWith)}
{renderOption(activeOption.value, currSelectedValue, compareWith)}
{renderOption(mergedOptions.value[activeIndex.value + 1], currSelectedValue, compareWith)}
{renderOption(slots, mergedOptions.value[activeIndex.value - 1], currSelectedValue, compareWith)}
{renderOption(slots, activeOption.value, currSelectedValue, compareWith)}
{renderOption(slots, mergedOptions.value[activeIndex.value + 1], currSelectedValue, compareWith)}
</div>
)
}

const renderOption = (
slots: Slots,
option: MergedOption | undefined,
selectedValue: any,
compareWith: (o1: any, o2: any) => boolean,
Expand All @@ -41,7 +43,7 @@ const renderOption = (
const selected = compareWith(selectedValue, value)
return (
<div key={key} role="option" aria-label={label} aria-selected={selected}>
{rawOption.slots?.default?.(rawOption) ?? label}
{renderOptionLabel(slots, rawOption, label)}
</div>
)
}
Expand Down
8 changes: 3 additions & 5 deletions packages/components/select/src/content/Option.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@

import { computed, defineComponent, inject } from 'vue'

import { isString } from 'lodash-es'

import { IxCheckbox } from '@idux/components/checkbox'

import { selectToken } from '../token'
import { optionProps } from '../types'
import { renderOptionLabel } from '../utils/renderOptionLabel'

export default defineComponent({
props: optionProps,
Expand Down Expand Up @@ -61,8 +60,7 @@ export default defineComponent({
const { multiple } = selectProps
const selected = isSelected.value
const prefixCls = `${mergedPrefixCls.value}-option`
const labelRender = rawOption.customLabel ?? 'optionLabel'
const labelSlot = isString(labelRender) ? slots[labelRender] : labelRender

return (
<div
class={classes.value}
Expand All @@ -73,7 +71,7 @@ export default defineComponent({
aria-selected={selected}
>
{multiple && <IxCheckbox checked={isSelected.value} disabled={disabled} />}
<span class={`${prefixCls}-label`}>{labelSlot ? labelSlot(rawOption) : label}</span>
<span class={`${prefixCls}-label`}>{renderOptionLabel(slots, rawOption, label)}</span>
</div>
)
}
Expand Down
2 changes: 1 addition & 1 deletion packages/components/select/src/trigger/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default defineComponent({

const innerStyle = computed(() => {
const { allowInput, searchable } = props
const isOpacity = allowInput || searchable
const isOpacity = allowInput || searchable === true
return { opacity: isOpacity ? undefined : 0 }
})

Expand Down
35 changes: 29 additions & 6 deletions packages/components/select/src/trigger/Selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@
*/

import type { MergedOption } from '../composables/useOptions'
import type { VNodeTypes } from 'vue'
import type { VNodeChild } from 'vue'

import { computed, defineComponent, inject } from 'vue'

import { Logger } from '@idux/cdk/utils'
import { IxIcon } from '@idux/components/icon'

import { selectToken } from '../token'
import { selectorProps } from '../types'
import { renderOptionLabel } from '../utils/renderOptionLabel'
import Input from './Input'
import Item from './Item'

Expand Down Expand Up @@ -47,11 +49,20 @@ export default defineComponent({
})

const showItems = computed(() => {
return selectProps.multiple || (selectedValue.value.length > 0 && !isComposing.value && !inputValue.value)
return (
selectProps.multiple ||
(selectedValue.value.length > 0 &&
!isComposing.value &&
(!inputValue.value || selectProps.searchable === 'overlay'))
)
})

const showPlaceholder = computed(() => {
return selectedValue.value.length === 0 && !isComposing.value && !inputValue.value
return (
selectedValue.value.length === 0 &&
!isComposing.value &&
(!inputValue.value || selectProps.searchable === 'overlay')
)
})

return () => {
Expand All @@ -74,12 +85,24 @@ export default defineComponent({
handleItemRemove,
title: label,
}
let labelNode: VNodeTypes | undefined
let labelNode: VNodeChild | undefined
if (isMax) {
labelNode = slots.maxLabel?.(item.value) ?? label
if (__DEV__ && slots.maxLabel) {
Logger.warn(
'components/select',
'slot `maxLabel` is deprecated, please use slot `overflowedLabel` instead',
)
}
const overflowedLabelSlot = slots.overflowedLabel ?? slots.maxLabel
labelNode = overflowedLabelSlot?.(item.value) ?? label
} else {
labelNode = slots.label?.(item.rawOption) ?? rawOption.slots?.default?.() ?? label
if (__DEV__ && slots.label) {
Logger.warn('components/select', 'slots `label` is deprecated, please use slots `selectedLabel` instead')
}
const selectedLabelSlot = slots.label ?? slots.selectedLabel
labelNode = selectedLabelSlot ? selectedLabelSlot(rawOption) : renderOptionLabel(slots, rawOption, label)
}

return <Item {...itemProps}>{labelNode}</Item>
})

Expand Down
2 changes: 1 addition & 1 deletion packages/components/select/src/trigger/Trigger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export default defineComponent({
if (suffix) {
return suffix
}
return props.searchable && isFocused.value ? 'search' : config.suffix
return props.searchable === true && isFocused.value ? 'search' : config.suffix
})

const classes = computed(() => {
Expand Down
2 changes: 1 addition & 1 deletion packages/components/select/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const selectProps = {
overlayRender: IxPropTypes.func<(children: VNode[]) => VNodeTypes>(),
placeholder: IxPropTypes.string,
readonly: IxPropTypes.bool.def(false),
searchable: IxPropTypes.bool.def(false),
searchable: IxPropTypes.oneOfType([Boolean, IxPropTypes.oneOf(['overlay'])]).def(false),
searchFilter: IxPropTypes.oneOfType([Boolean, IxPropTypes.func<SelectFilterFn>()]).def(true),
size: IxPropTypes.oneOf<FormSize>(['sm', 'md', 'lg']),
suffix: IxPropTypes.string,
Expand Down
18 changes: 18 additions & 0 deletions packages/components/select/src/utils/renderOptionLabel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* @license
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
*/

import type { SelectOptionProps } from '../types'
import type { Slots, VNodeChild } from 'vue'

import { isString } from 'lodash-es'

export function renderOptionLabel(slots: Slots, rawOption: SelectOptionProps, label?: string): VNodeChild {
const labelRender = rawOption.customLabel ?? 'optionLabel'
const labelSlot = isString(labelRender) ? slots[labelRender] : labelRender

return labelSlot?.(rawOption) ?? label
}
4 changes: 4 additions & 0 deletions packages/components/select/style/index.less
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@
.@{select-option-prefix}-group:not(:first-child) {
border-top: @select-option-group-border;
}

&-search-wrapper {
padding: @select-overlay-input-padding;
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions packages/components/select/style/themes/default.variable.less
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
@select-option-container-border-radius: @border-radius-sm;
@select-option-container-box-shadow: @shadow-bottom-md;

@select-overlay-input-padding: 0 @spacing-md @spacing-sm;

@select-icon-font-size: @font-size-sm;
@select-icon-margin-right: @spacing-xs;
@select-icon-color: @select-placeholder-color;
Expand Down

0 comments on commit be59d57

Please sign in to comment.