Skip to content
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
11 changes: 11 additions & 0 deletions packages/components/cascader/README.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ style | Object | - | CSS(Cascading Style Sheets) | N
custom-style | Object | - | CSS(Cascading Style Sheets),used to set style on virtual component | N
check-strictly | Boolean | false | \- | N
close-btn | Boolean | true | \- | N
filterable | Boolean | false | Enable searching. When on, a search box is shown at the top; typing switches the panel to a flat list of matched paths | N
filter | Function | - | Custom filter function. Typescript: `(keyword: string, option: CascaderOption, path: CascaderOption[]) => boolean`. Falls back to a case-insensitive built-in matcher on label/text | N
filter-placeholder | String | - | Placeholder text of the search box, falls back to global locale | N
keys | Object | - | Typescript: `CascaderKeysType` `type CascaderKeysType = TreeKeysType`。[see more ts definition](https://github.com/Tencent/tdesign-miniprogram/blob/develop/packages/components/common/common.ts)。[see more ts definition](https://github.com/Tencent/tdesign-miniprogram/blob/develop/packages/components/cascader/type.ts) | N
options | Array | [] | Typescript: `Array<CascaderOption>` | N
placeholder | String | - | \- | N
Expand Down Expand Up @@ -47,6 +50,14 @@ Name | Default Value | Description
--td-cascader-border-color | @component-stroke | -
--td-cascader-content-height | 78vh | -
--td-cascader-disabled-color | @text-color-disabled | -
--td-cascader-filter-empty-color | @text-color-placeholder | -
--td-cascader-filter-empty-padding | 96rpx @spacer-2 | -
--td-cascader-filter-highlight-color | @brand-color | -
--td-cascader-filter-item-color | @text-color-primary | -
--td-cascader-filter-item-disabled-color | @text-color-disabled | -
--td-cascader-filter-item-hover-bg | @bg-color-secondarycontainer | -
--td-cascader-filter-item-padding | 24rpx 32rpx | -
--td-cascader-filter-padding | 0 @spacer-2 @spacer-1 | -
--td-cascader-options-height | calc(100% - @cascader-step-height) | -
--td-cascader-options-title-color | @text-color-placeholder | -
--td-cascader-step-arrow-color | @text-color-placeholder | -
Expand Down
15 changes: 15 additions & 0 deletions packages/components/cascader/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ isComponent: true

{{ check-strictly }}

#### 支持搜索

{{ filterable }}

## API

### Cascader Props
Expand All @@ -67,6 +71,9 @@ style | Object | - | 样式 | N
custom-style | Object | - | 样式,一般用于开启虚拟化组件节点场景 | N
check-strictly | Boolean | false | 父子节点选中状态不再关联,可各自选中或取消 | N
close-btn | Boolean | true | 关闭按钮 | N
filterable | Boolean | false | 是否可搜索。开启后顶部展示搜索框,输入关键字将层级面板切换为扁平的匹配路径列表 | N
filter | Function | - | 自定义过滤函数,返回 true 表示匹配。TS 类型:`(keyword: string, option: CascaderOption, path: CascaderOption[]) => boolean`。缺省时使用大小写不敏感的内置匹配规则(命中路径中任一 label 或叶子节点 text) | N
filter-placeholder | String | - | 搜索框占位文案,缺省回退到全局语言包 | N
keys | Object | - | 用来定义 value / label / children / disabled 在 `options` 中对应的字段别名。TS 类型:`CascaderKeysType` `type CascaderKeysType = TreeKeysType`。[通用类型定义](https://github.com/Tencent/tdesign-miniprogram/blob/develop/packages/components/common/common.ts)。[详细类型定义](https://github.com/Tencent/tdesign-miniprogram/blob/develop/packages/components/cascader/type.ts) | N
options | Array | [] | 可选项数据源。TS 类型:`Array<CascaderOption>` | N
placeholder | String | - | 未选中时的提示文案。组件内置默认值为:'选择选项' | N
Expand Down Expand Up @@ -104,6 +111,14 @@ title | 自定义 `title` 显示内容
--td-cascader-border-color | @component-stroke | -
--td-cascader-content-height | 78vh | -
--td-cascader-disabled-color | @text-color-disabled | -
--td-cascader-filter-empty-color | @text-color-placeholder | -
--td-cascader-filter-empty-padding | 96rpx @spacer-2 | -
--td-cascader-filter-highlight-color | @brand-color | -
--td-cascader-filter-item-color | @text-color-primary | -
--td-cascader-filter-item-disabled-color | @text-color-disabled | -
--td-cascader-filter-item-hover-bg | @bg-color-secondarycontainer | -
--td-cascader-filter-item-padding | 24rpx 32rpx | -
--td-cascader-filter-padding | 0 @spacer-2 @spacer-1 | -
--td-cascader-options-height | calc(100% - @cascader-step-height) | -
--td-cascader-options-title-color | @text-color-placeholder | -
--td-cascader-step-arrow-color | @text-color-placeholder | -
Expand Down
112 changes: 112 additions & 0 deletions packages/components/cascader/__test__/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,116 @@ describe('cascader', () => {
expect($cascader.dom.getAttribute('style').includes(`${comp.data.customStyle}`)).toBeTruthy();
}
});

describe(': filterable', () => {
const options = [
{
label: '北京市',
value: '110000',
children: [
{
label: '北京市',
value: '110100',
children: [
{ label: '海淀区', value: '110108' },
{ label: '朝阳区', value: '110105' },
],
},
],
},
{
label: '上海市',
value: '310000',
children: [{ label: '上海市', value: '310100', children: [{ label: '浦东新区', value: '310115' }] }],
},
{
label: '广东省',
value: '440000',
children: [{ label: '珠海市', value: '440400' }],
},
];

const renderCascader = (overrides = {}) => {
const id = simulate.load({
template: `<t-cascader id="cas" filterable filter="{{filter}}" options="{{options}}" />`,
data: {
options,
filter: null,
...overrides,
},
usingComponents: { 't-cascader': cascader },
});
const comp = simulate.render(id);
comp.attach(document.createElement('parent-wrapper'));
return comp;
};

it('reflects filterable on instance data', () => {
const comp = renderCascader();
const $cascader = comp.querySelector('#cas');
expect($cascader.instance.data.filterable).toBe(true);
expect($cascader.instance.data.isSearching).toBe(false);
});

it('default filter matches full-path label (case-insensitive)', async () => {
const comp = renderCascader();
const $cascader = comp.querySelector('#cas');
$cascader.instance.applyFilter('海');
await simulate.sleep();
expect($cascader.instance.data.isSearching).toBe(true);
const keys = $cascader.instance.data.filterResults.map((r) => r.key);
expect(keys).toEqual(expect.arrayContaining(['110000/110100/110108', '440000/440400']));
});

it('shows empty state when no path matches', async () => {
const comp = renderCascader();
const $cascader = comp.querySelector('#cas');
$cascader.instance.applyFilter('xxxxx');
await simulate.sleep();
expect($cascader.instance.data.isSearching).toBe(true);
expect($cascader.instance.data.filterResults).toHaveLength(0);
});

it('clear restores layered view', async () => {
const comp = renderCascader();
const $cascader = comp.querySelector('#cas');
$cascader.instance.applyFilter('北');
await simulate.sleep();
expect($cascader.instance.data.isSearching).toBe(true);
$cascader.instance.resetFilter();
await simulate.sleep();
expect($cascader.instance.data.isSearching).toBe(false);
expect($cascader.instance.data.filterKeyword).toBe('');
});

it('uses custom filter function when provided', async () => {
const comp = renderCascader({
filter: (keyword, option) => option.label === keyword,
});
const $cascader = comp.querySelector('#cas');
$cascader.instance.applyFilter('浦东新区');
await simulate.sleep();
expect($cascader.instance.data.filterResults).toHaveLength(1);
expect($cascader.instance.data.filterResults[0].key).toBe('310000/310100/310115');
});

it('selecting a flat result writes selectedIndexes and clears search state', async () => {
const comp = renderCascader();
const $cascader = comp.querySelector('#cas');

$cascader.instance.applyFilter('海淀');
await simulate.sleep();
const target = $cascader.instance.data.filterResults[0];
$cascader.instance.onFilterResultTap({ currentTarget: { dataset: { key: target.key } } });
await simulate.sleep();

expect($cascader.instance.data.isSearching).toBe(false);
expect($cascader.instance.data.filterKeyword).toBe('');
expect($cascader.instance.data.selectedIndexes).toEqual(target.indexes);
const { items, selectedIndexes } = $cascader.instance.data;
const leaf = items[selectedIndexes.length - 1][selectedIndexes[selectedIndexes.length - 1]];
expect(leaf.label).toBe('海淀区');
expect(leaf.value).toBe('110108');
});
});
});
3 changes: 2 additions & 1 deletion packages/components/cascader/_example/cascader.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"keys": "./keys",
"with-value": "./with-value",
"with-title": "./with-title",
"check-strictly": "./check-strictly"
"check-strictly": "./check-strictly",
"filterable": "./filterable"
}
}
4 changes: 4 additions & 0 deletions packages/components/cascader/_example/cascader.wxml
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,8 @@
<t-demo desc="选择任意一项">
<check-strictly />
</t-demo>

<t-demo desc="支持搜索">
<filterable />
</t-demo>
</view>
108 changes: 108 additions & 0 deletions packages/components/cascader/_example/filterable/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
const data = {
areaList: [
{
label: '北京市',
value: '110000',
children: [
{
value: '110100',
label: '北京市',
children: [
{ value: '110101', label: '东城区' },
{ value: '110102', label: '西城区' },
{ value: '110105', label: '朝阳区' },
{ value: '110106', label: '丰台区' },
{ value: '110107', label: '石景山区' },
{ value: '110108', label: '海淀区' },
{ value: '110109', label: '门头沟区' },
{ value: '110111', label: '房山区' },
{ value: '110112', label: '通州区' },
{ value: '110113', label: '顺义区' },
{ value: '110114', label: '昌平区' },
{ value: '110115', label: '大兴区' },
{ value: '110116', label: '怀柔区' },
{ value: '110117', label: '平谷区' },
{ value: '110118', label: '密云区' },
{ value: '110119', label: '延庆区' },
],
},
],
},
{
label: '上海市',
value: '310000',
children: [
{
value: '310100',
label: '上海市',
children: [
{ value: '310101', label: '黄浦区' },
{ value: '310104', label: '徐汇区' },
{ value: '310105', label: '长宁区' },
{ value: '310106', label: '静安区' },
{ value: '310107', label: '普陀区' },
{ value: '310109', label: '虹口区' },
{ value: '310110', label: '杨浦区' },
{ value: '310112', label: '闵行区' },
{ value: '310113', label: '宝山区' },
{ value: '310114', label: '嘉定区' },
{ value: '310115', label: '浦东新区' },
],
},
],
},
{
label: '广东省',
value: '440000',
children: [
{
value: '440100',
label: '广州市',
children: [
{ value: '440103', label: '荔湾区' },
{ value: '440104', label: '越秀区' },
{ value: '440105', label: '海珠区' },
{ value: '440106', label: '天河区' },
{ value: '440111', label: '白云区' },
],
},
{
value: '440300',
label: '深圳市',
children: [
{ value: '440303', label: '罗湖区' },
{ value: '440304', label: '福田区' },
{ value: '440305', label: '南山区' },
{ value: '440306', label: '宝安区' },
{ value: '440307', label: '龙岗区' },
],
},
],
},
],
};

Component({
data: {
options: data.areaList,
note: '请选择地址',
visible: false,
value: '',
},
methods: {
showCascader() {
this.setData({ visible: true });
},
onPick(e) {
console.log(e.detail);
},
onChange(e) {
const { selectedOptions, value } = e.detail;

this.setData({
value,
note: selectedOptions.map((item) => item.label).join('/'),
});
},
},
});
7 changes: 7 additions & 0 deletions packages/components/cascader/_example/filterable/index.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"component": true,
"usingComponents": {
"t-cell": "tdesign-miniprogram/cell/cell",
"t-cascader": "tdesign-miniprogram/cascader/cascader"
}
}
12 changes: 12 additions & 0 deletions packages/components/cascader/_example/filterable/index.wxml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<t-cell title="地址" note="{{note}}" bind:click="showCascader" arrow />

<t-cascader
visible="{{visible}}"
value="{{value}}"
options="{{options}}"
title="请选择地址"
filterable
filter-placeholder="搜索省/市/区"
bind:change="onChange"
bind:pick="onPick"
/>
Empty file.
3 changes: 2 additions & 1 deletion packages/components/cascader/cascader.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"t-popup": "../popup/popup",
"t-tabs": "../tabs/tabs",
"t-tab-panel": "../tab-panel/tab-panel",
"t-radio-group": "../radio-group/radio-group"
"t-radio-group": "../radio-group/radio-group",
"t-search": "../search/search"
}
}
48 changes: 48 additions & 0 deletions packages/components/cascader/cascader.less
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@
@cascader-border-color: var(--td-cascader-border-color, @component-stroke);
@cascader-content-height: var(--td-cascader-content-height, 78vh);
@cascader-options-height: var(--td-cascader-options-height, calc(100% - @cascader-step-height));
// filter (search)
@cascader-filter-padding: var(--td-cascader-filter-padding, 0 @spacer-2 @spacer-1);
@cascader-filter-item-padding: var(--td-cascader-filter-item-padding, 24rpx 32rpx);
@cascader-filter-item-color: var(--td-cascader-filter-item-color, @text-color-primary);
@cascader-filter-item-hover-bg: var(--td-cascader-filter-item-hover-bg, @bg-color-secondarycontainer);
@cascader-filter-item-disabled-color: var(--td-cascader-filter-item-disabled-color, @text-color-disabled);
@cascader-filter-highlight-color: var(--td-cascader-filter-highlight-color, @brand-color);
@cascader-filter-empty-color: var(--td-cascader-filter-empty-color, @text-color-placeholder);
@cascader-filter-empty-padding: var(--td-cascader-filter-empty-padding, 96rpx @spacer-2);
// steps
@cascader-step-height: var(--td-cascader-step-height, 88rpx);
@cascader-step-dot-size: var(--td-cascader-step-dot-size, 16rpx);
Expand Down Expand Up @@ -116,4 +125,43 @@
margin-left: auto;
}
}

&__filter {
padding: @cascader-filter-padding;
box-sizing: border-box;

&-result {
flex: 1;
width: 100%;
box-sizing: border-box;
}

&-result-item {
padding: @cascader-filter-item-padding;
color: @cascader-filter-item-color;
font: @font-body-medium;
box-sizing: border-box;
.border(bottom, @cascader-border-color);

&--hover {
background-color: @cascader-filter-item-hover-bg;
}

&--disabled {
color: @cascader-filter-item-disabled-color;
}
}

&-highlight {
color: @cascader-filter-highlight-color;
font-weight: 600;
}

&-empty {
padding: @cascader-filter-empty-padding;
text-align: center;
color: @cascader-filter-empty-color;
font: @font-body-medium;
}
}
}
Loading