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

feat(comp:select): add 'overlay' to searchable #729

Merged
merged 1 commit into from
Jan 19, 2022
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
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
32 changes: 26 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,21 @@ 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