Skip to content

Commit

Permalink
feat(comp:cascader): add disableData to dynamically disable options (#…
Browse files Browse the repository at this point in the history
…1408)

* feat(comp:cascader): add disableData to dynamically disable options

* feat(comp:cascader): add separator prop
  • Loading branch information
danranVm committed Jan 13, 2023
1 parent 477ac44 commit 65328e7
Show file tree
Hide file tree
Showing 15 changed files with 286 additions and 45 deletions.
16 changes: 16 additions & 0 deletions packages/components/cascader/__tests__/cascader.spec.ts
Expand Up @@ -212,6 +212,22 @@ describe('Cascader', () => {

expect(getAllOptionGroup(wrapper).length).toBe(3)
})

test('disableData work', async () => {
const wrapper = CascaderMount({
props: {
open: true,
disableData: data => data.key === 'button',
},
})

expect(getAllOptionGroup(wrapper)[2].find('.ix-cascader-option-disabled').text()).toBe('Button')

await wrapper.setProps({ disableData: (data: CascaderData) => data.key === 'pro' })

expect(getAllOptionGroup(wrapper)[2].find('.ix-cascader-option-disabled').exists()).toBe(false)
expect(getAllOptionGroup(wrapper)[0].find('.ix-cascader-option-disabled').text()).toBe('Pro')
})
})

describe('multiple work', () => {
Expand Down
18 changes: 18 additions & 0 deletions packages/components/cascader/demo/Disabled.md
@@ -0,0 +1,18 @@
---
title:
zh: 禁用状态
en: Disabled status
order: 3
---

## zh

可以设置 `disabled` 来禁用整个组件

也可以设置 `disableData` 来动态禁用某些选项,它的优先级低于 `dataSource``disabled`

## en

You can set `disabled` to disable the cascader component.

You can also set `disableData` to dynamically disable some options, which have a lower priority than `disabled` of `dataSource`.
130 changes: 130 additions & 0 deletions packages/components/cascader/demo/Disabled.vue
@@ -0,0 +1,130 @@
<template>
<IxSpace vertical>
<IxCascader v-model:value="valueRef" :dataSource="dataSource" disabled :fullPath="false" />
<IxCascader v-model:value="valueRef" :dataSource="dataSource" :disableData="disableData" :fullPath="false" />
</IxSpace>
</template>

<script setup lang="ts">
import { computed, ref } from 'vue'
import { CascaderData } from '@idux/components/cascader'
const valueRef = ref('button')
const disableData = computed(() => (data: CascaderData) => valueRef.value === data.key)
const dataSource: CascaderData[] = [
{
key: 'components',
label: 'Components',
children: [
{
key: 'general',
label: 'General',
children: [
{
key: 'button',
label: 'Button',
},
{
key: 'header',
label: 'Header',
},
{
key: 'icon',
label: 'Icon',
},
],
},
{
key: 'layout',
label: 'Layout',
children: [
{
key: 'divider',
label: 'Divider',
},
{
key: 'grid',
label: 'Grid',
},
{
key: 'space',
label: 'Space',
},
],
},
{
key: 'navigation',
label: 'Navigation',
children: [
{
key: 'breadcrumb',
label: 'Breadcrumb',
},
{
key: 'dropdown',
label: 'Dropdown',
},
{
key: 'menu',
label: 'Menu',
},
{
key: 'pagination',
label: 'Pagination',
},
],
},
],
},
{
key: 'pro',
label: 'Pro',
children: [
{
key: 'pro-layout',
label: 'Layout',
},
{
key: 'pro-table',
label: 'Table',
disabled: true,
},
{
key: 'pro-transfer',
label: 'Transfer',
},
],
},
{
key: 'cdk',
label: 'CDK',
disabled: true,
children: [
{
key: 'a11y',
label: 'Accessibility',
},
{
key: 'breakpoint',
label: 'Breakpoint',
},
{
key: 'click-outside',
label: 'ClickOutside',
},
{
key: 'clipboard',
label: 'Clipboard',
},
{
key: 'forms',
label: 'Forms',
},
],
},
]
</script>

<style scoped lang="less"></style>
2 changes: 2 additions & 0 deletions packages/components/cascader/docs/Api.zh.md
Expand Up @@ -19,6 +19,7 @@
| `customAdditional` | 自定义下拉选项的额外属性 | `CascaderCustomAdditional` | - | - | 例如 `class`, 或者原生事件 |
| `dataSource` | 树型数据数组,参见[CascaderData](#CascaderData) | `CascaderData[]` | `[]` | - | - |
| `disabled` | 禁用选择器 | `boolean` | - | - | - |
| `disableData` | 动态禁用某些项 | `(data: CascaderData) => boolean` | - | - | - |
| `empty` | 空数据时的内容 | `'default' \| 'simple' \| EmptyProps` | `'simple'` | - | - |
| `expandIcon` | 展开图标 | `string \| #expandIcon="{key: VKey, expanded: boolean, data: CascaderData}"` | `right` || - |
| `expandTrigger` | 触发展开的方式 | `'click' \| 'hover'` | `click` | - | - |
Expand All @@ -37,6 +38,7 @@
| `readonly` | 只读模式 | `boolean` | - | - | - |
| `searchable` | 是否可搜索 | `boolean \| 'overlay'` | `false` | - | 当为 `true` 时搜索功能集成在选择器上,当为 `overlay` 时,搜索功能集成在悬浮层上 |
| `searchFn` | 根据搜索的文本进行筛选 | `boolean \| SelectSearchFn` | `true` | - |`true` 时使用默认的搜索规则, 如果使用远程搜索,应该设置为 `false` |
| `separator` | 设置分割符 | `string` | `/` | - | - |
| `size` | 设置选择器大小 | `'sm' \| 'md' \| 'lg'` | `md` || - |
| `status` | 手动指定校验状态 | `valid \| invalid \| validating` | - | - | - |
| `strategy` | 设置级联策略 | `'all' \| 'parent' \| 'child' \| 'off'` | `'all'` | - | 具体用法参见 [级联策略](#components-cascader-demo-Strategy) |
Expand Down
16 changes: 13 additions & 3 deletions packages/components/cascader/src/Cascader.tsx
Expand Up @@ -14,7 +14,7 @@ import { ɵSelector, type ɵSelectorInstance } from '@idux/components/_private/s
import { useGlobalConfig } from '@idux/components/config'
import { useFormItemRegister, useFormSize, useFormStatus } from '@idux/components/form'
import { ɵUseOverlayState } from '@idux/components/select'
import { useGetKey } from '@idux/components/utils'
import { useGetDisabled, useGetKey } from '@idux/components/utils'

import { useActiveState } from './composables/useActiveState'
import { useDataSource } from './composables/useDataSource'
Expand All @@ -41,6 +41,7 @@ export default defineComponent({
const mergedExpandIcon = computed(() => props.expandIcon ?? config.expandIcon)
const mergedFullPath = computed(() => props.fullPath ?? config.fullPath)
const mergedGetKey = useGetKey(props, config, 'components/cascader')
const mergedGetDisabled = useGetDisabled(props)
const mergedLabelKey = computed(() => props.labelKey ?? config.labelKey)

const triggerRef = ref<ɵSelectorInstance>()
Expand Down Expand Up @@ -68,11 +69,19 @@ export default defineComponent({
mergedFullPath,
)
const activeStateContext = useActiveState(props, mergedDataMap)
const selectedStateContext = useSelectedState(props, accessor, mergedDataMap, mergedFullPath)
const { searchedData } = useSearchable(props, mergedLabelKey, mergedDataMap, inputValue)
const selectedStateContext = useSelectedState(props, accessor, mergedDataMap, mergedFullPath, mergedGetDisabled)
const { searchedData } = useSearchable(
props,
mergedData,
mergedDataMap,
mergedLabelKey,
inputValue,
mergedGetDisabled,
)
const expandableContext = useExpandable(
props,
mergedGetKey,
mergedGetDisabled,
mergedChildrenKey,
mergedLabelKey,
mergedFullPath,
Expand Down Expand Up @@ -107,6 +116,7 @@ export default defineComponent({
mergedExpandIcon,
mergedFullPath,
mergedGetKey,
mergedGetDisabled,
mergedLabelKey,
accessor,
inputValue,
Expand Down
11 changes: 7 additions & 4 deletions packages/components/cascader/src/composables/useDataSource.ts
Expand Up @@ -65,9 +65,9 @@ export function convertMergedData(
parentKey?: VKey,
parentLabel?: string,
): MergedData[] {
const { loadChildren } = props
const { loadChildren, separator } = props
return data.map(item =>
convertMergedItem(item, getKey, childrenKey, labelKey, fullPath, !!loadChildren, parentKey, parentLabel),
convertMergedItem(item, getKey, childrenKey, labelKey, separator, fullPath, !!loadChildren, parentKey, parentLabel),
)
}

Expand All @@ -76,16 +76,19 @@ function convertMergedItem(
getKey: GetKeyFn,
childrenKey: string,
labelKey: string,
separator: string,
fullPath: boolean,
hasLoad: boolean,
parentKey?: VKey,
parentLabel?: string,
): MergedData {
const key = getKey(rawData)
const label = (fullPath && !isNil(parentLabel) ? `${parentLabel}/${rawData[labelKey]}` : rawData[labelKey]) as string
const label = (
fullPath && !isNil(parentLabel) ? `${parentLabel}${separator}${rawData[labelKey]}` : rawData[labelKey]
) as string
const subData = rawData[childrenKey] as CascaderData[] | undefined
const children = subData?.map(item =>
convertMergedItem(item, getKey, childrenKey, labelKey, fullPath, hasLoad, key, label),
convertMergedItem(item, getKey, childrenKey, labelKey, separator, fullPath, hasLoad, key, label),
)
return {
children,
Expand Down
7 changes: 4 additions & 3 deletions packages/components/cascader/src/composables/useExpandable.ts
Expand Up @@ -10,7 +10,7 @@ import { type ComputedRef, type Ref, ref } from 'vue'
import { isNil } from 'lodash-es'

import { type VKey, callEmit, useControlledProp } from '@idux/cdk/utils'
import { type GetKeyFn } from '@idux/components/utils'
import { type GetDisabledFn, type GetKeyFn } from '@idux/components/utils'

import { type MergedData, convertMergedData, convertMergedDataMap } from './useDataSource'
import { type CascaderData, type CascaderProps } from '../types'
Expand All @@ -26,6 +26,7 @@ export interface ExpandableContext {
export function useExpandable(
props: CascaderProps,
mergedGetKey: ComputedRef<GetKeyFn>,
mergedGetDisabled: ComputedRef<GetDisabledFn>,
mergedChildrenKey: ComputedRef<string>,
mergedLabelKey: ComputedRef<string>,
mergedFullPath: ComputedRef<boolean>,
Expand All @@ -38,7 +39,7 @@ export function useExpandable(
}
const dataMap = mergedDataMap.value
const currData = dataMap.get(firstSelectedKey)
return getParentKeys(dataMap, currData, false)
return getParentKeys(dataMap, currData, false, mergedGetDisabled.value)
}
const [expandedKeys, setExpandedKeys] = useControlledProp(props, 'expandedKeys', () =>
getDefaultExpandedKeys(selectedKeys.value[0]),
Expand Down Expand Up @@ -91,7 +92,7 @@ export function useExpandable(

if (!currExpanded) {
const dataMap = mergedDataMap.value
const newKeys = getParentKeys(dataMap, dataMap.get(key), false)
const newKeys = getParentKeys(dataMap, dataMap.get(key), false, mergedGetDisabled.value)
newKeys.push(key)
handleChange(newKeys, !currExpanded, currData!.rawData)
}
Expand Down
47 changes: 31 additions & 16 deletions packages/components/cascader/src/composables/useSearchable.ts
Expand Up @@ -10,6 +10,7 @@ import { type ComputedRef, computed } from 'vue'
import { isFunction } from 'lodash-es'

import { NoopArray, type VKey } from '@idux/cdk/utils'
import { type GetDisabledFn } from '@idux/components/utils'

import { type MergedData } from './useDataSource'
import { type CascaderData, type CascaderProps, type CascaderSearchFn } from '../types'
Expand All @@ -20,9 +21,11 @@ export interface SearchableContext {

export function useSearchable(
props: CascaderProps,
mergedLabelKey: ComputedRef<string>,
mergedData: ComputedRef<MergedData[]>,
mergedDataMap: ComputedRef<Map<VKey, MergedData>>,
mergedLabelKey: ComputedRef<string>,
inputValue: ComputedRef<string>,
mergedGetDisabled: ComputedRef<GetDisabledFn>,
): SearchableContext {
const mergedSearchFn = useSearchFn(props, mergedLabelKey)
const parentEnabled = computed(() => props.multiple || props.strategy === 'off')
Expand All @@ -34,19 +37,9 @@ export function useSearchable(
return NoopArray as unknown as VKey[]
}
const _parentEnabled = parentEnabled.value
const getDisabledFn = mergedGetDisabled.value
const keySet = new Set<VKey>()
mergedDataMap.value.forEach(data => {
const { key, rawData } = data
if (keySet.has(key)) {
return
}
if (searchFn(rawData, searchValue)) {
if (_parentEnabled || data.isLeaf) {
keySet.add(key)
}
processChildren(keySet, data, _parentEnabled)
}
})
mergedData.value.forEach(data => doSearch(keySet, data, searchFn, searchValue, _parentEnabled, getDisabledFn))
return [...keySet]
})

Expand Down Expand Up @@ -75,20 +68,42 @@ function getDefaultSearchFn(labelKey: string): CascaderSearchFn {
}
}

function processChildren(keySet: Set<VKey>, data: MergedData, parentEnabled: boolean) {
function doSearch(
keySet: Set<VKey>,
data: MergedData,
searchFn: CascaderSearchFn,
searchValue: string,
_parentEnabled: boolean,
getDisabledFn: GetDisabledFn,
) {
const { key, rawData } = data
if (keySet.has(key) || getDisabledFn(rawData)) {
return
}
if (searchFn(rawData, searchValue)) {
if (_parentEnabled || data.isLeaf) {
keySet.add(key)
}
processChildren(keySet, data, _parentEnabled, getDisabledFn)
} else if (data.children) {
data.children.forEach(child => doSearch(keySet, child, searchFn, searchValue, _parentEnabled, getDisabledFn))
}
}

function processChildren(keySet: Set<VKey>, data: MergedData, parentEnabled: boolean, getDisabledFn: GetDisabledFn) {
if (!data || !data.children) {
return
}

data.children.forEach(child => {
if (child.rawData.disabled || keySet.has(child.key)) {
if (keySet.has(child.key) || getDisabledFn(child.rawData)) {
return
}

if (parentEnabled || child.isLeaf) {
keySet.add(child.key)
}

processChildren(keySet, child, parentEnabled)
processChildren(keySet, child, parentEnabled, getDisabledFn)
})
}

0 comments on commit 65328e7

Please sign in to comment.