feat: cascader-select 组件升级 (#3450)

* feat: cascader-select 组件升级

* fix:无结果文案参数展示

* fix: review建议,优化节点选中的判断逻辑,移动端样式适配

* fix: 代码rebase问题,clear icon覆盖
This commit is contained in:
VitoBeijing 2022-02-18 14:04:48 +08:00 committed by GitHub
parent ea50677a50
commit ae905e12a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 256 additions and 23 deletions

View File

@ -1179,6 +1179,8 @@
--ResultBox-value--onHover-bg: rgba(0, 145, 255, 0.1);
--ResultBox-value-bg: #f5f5f5;
--ResultBox-value-color: #000;
--ResultBox-value-clear-bg: #d4d6d9;
--ResultBox-value-clear-hover-bg: #f5f5f5;
--Rating-inactive-color: #e6e6e8;
--Rating-star-margin: #{px2rem(8px)};

View File

@ -2,9 +2,15 @@
@include input-input();
@include input-border();
flex-wrap: wrap;
padding: 0 px2rem(3px);
padding: 0 px2rem(26px) 0 px2rem(3px);
min-height: var(--Form-input-height);
align-items: center;
border-radius: 3px;
position: relative;
&.is-clearable {
padding-right: px2rem(55px);
}
&.is-error {
border-color: var(--Form-input-onError-borderColor);
@ -49,8 +55,46 @@
text-overflow: ellipsis;
}
&-pc-arrow {
height: 100%;
margin: auto 0;
position: absolute;
right: 10px;
display: flex;
align-items: center;
transition: transform var(--animation-duration) ease;
> svg {
width: px2rem(10px);
height: px2rem(10px);
top: 0.125em;
}
}
&.is-opened &-pc-arrow {
transform: rotate(180deg);
}
&-clear {
@include input-clear();
height: 100%;
padding: px2rem(4px);
margin: auto 0;
position: absolute;
right: 10px;
background-color: unset;
&-wrap {
width: px2rem(16px);
height: px2rem(16px);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
&-with-arrow {
right: 33px;
}
}
> svg {
@ -86,6 +130,9 @@
> svg {
width: px2rem(10px);
height: px2rem(10px);
&.icon {
top: 0;
}
}
}
@ -134,6 +181,20 @@
border: none;
justify-content: flex-end;
.#{$ns}ResultBox-clear {
@include input-clear();
width: px2rem(26px);
height: px2rem(26px);
margin: 0 px2rem(-2px);
margin-left: auto;
padding: px2rem(4px);
position: unset;
right: 0;
display: flex;
align-items: center;
background-color: unset;
}
.#{$ns}ResultBox-arrow {
margin-right: var(--gap-xs);
// margin-left: var(--gap-xs);

View File

@ -7,7 +7,7 @@
@include input-border();
&-optionArrowRight {
display: inline-block;
padding-right: px2rem(10px);
padding-right: px2rem(40px);
svg {
width: px2rem(12px);
@ -61,12 +61,15 @@
> .#{$ns}NestedSelect-optionLabel {
flex: 1;
height: px2rem(32px);
&.is-disabled {
cursor: not-allowed;
color: var(--text--muted-color);
}
}
.#{$ns}NestedSelect-optionLabel-highlight {
color: var(--Form-select-menu-onActive-color);
}
&.is-active {
color: var(--Form-select-menu-onActive-color);
@ -82,8 +85,13 @@
display: block;
}
&.checkall {
border-bottom: px2rem(1px) solid #eceff8;
&.no-result {
justify-content: center;
cursor: default;
&:hover {
color: unset;
background: unset;
}
}
}
}

View File

@ -121,7 +121,7 @@
padding: 0 var(--gap-xs);
overflow: hidden;
vertical-align: top;
text-overflow:ellipsis;
text-overflow: ellipsis;
white-space: nowrap;
}
}

View File

@ -136,6 +136,7 @@ $L1: 0px 4px 6px 0px rgba(8, 14, 26, 0.06),
--Form-input-onDisabled-bg: #{$G10};
--Form-input-onDisabled-borderColor: #{$G8};
--Form-input-onDisabled-color: #{$G5};
--Form-input-iconColor: #{$G5};
--Form-input-paddingX: #{px2rem(10px)};
--Form-description-color: #999;
--Form--horizontal-label-whiteSpace: normal;
@ -622,4 +623,9 @@ $L1: 0px 4px 6px 0px rgba(8, 14, 26, 0.06),
// Formula
--InputFormula-code-bgColor: #{$G10};
// ResultBox
--ResultBox-value-bg: #{$G10};
--ResultBox-value-clear-bg: #{$G8};
--ResultBox-value-clear-hover-bg: #{$G9};
}

View File

@ -17,9 +17,11 @@ export interface ResultBoxProps
result?: Array<any> | any;
itemRender: (value: any) => JSX.Element | string;
onResultChange?: (value: Array<any>) => void;
onClear?: (e: React.MouseEvent<HTMLElement>) => void;
allowInput?: boolean;
inputPlaceholder: string;
useMobileUI?: boolean;
hasDropDownArrow?: boolean;
}
export class ResultBox extends React.Component<ResultBoxProps> {
@ -52,8 +54,8 @@ export class ResultBox extends React.Component<ResultBoxProps> {
@autobind
clearValue(e: React.MouseEvent<any>) {
e.preventDefault();
const onResultChange = this.props.onResultChange;
onResultChange && onResultChange([]);
this.props.onClear && this.props.onClear(e);
this.props.onResultChange && this.props.onResultChange([]);
}
@autobind
@ -117,6 +119,8 @@ export class ResultBox extends React.Component<ResultBoxProps> {
onBlur,
borderMode,
useMobileUI,
hasDropDownArrow,
onClear,
...rest
} = this.props;
const isFocused = this.state.isFocused;
@ -129,6 +133,7 @@ export class ResultBox extends React.Component<ResultBoxProps> {
'is-disabled': disabled,
'is-error': hasError,
'is-clickable': onResultClick,
'is-clearable': clearable,
'is-mobile': mobileUI,
[`ResultBox--border${ucFirst(borderMode)}`]: borderMode
})}
@ -183,10 +188,22 @@ export class ResultBox extends React.Component<ResultBoxProps> {
{clearable &&
!disabled &&
(Array.isArray(result) ? result.length : result) ? (
<a onClick={this.clearValue} className={cx('ResultBox-clear')}>
<Icon icon="input-clear" className="icon" />
<a
onClick={this.clearValue}
className={cx('ResultBox-clear', {
'ResultBox-clear-with-arrow': hasDropDownArrow
})}
>
<div className={cx('ResultBox-clear-wrap')}>
<Icon icon="input-clear" className="icon" />
</div>
</a>
) : null}
{hasDropDownArrow && !mobileUI && (
<span className={cx('ResultBox-pc-arrow')}>
<Icon icon="caret" className="icon" />
</span>
)}
{!allowInput && mobileUI ? (
<span className={cx('ResultBox-arrow')}>
<Icon icon="caret" className="icon" />

View File

@ -126,6 +126,13 @@ export default class NestedSelectControl extends React.Component<
});
}
@autobind
handleResultClear() {
this.setState({
inputValue: undefined
});
}
@autobind
close() {
this.setState({
@ -162,23 +169,56 @@ export default class NestedSelectControl extends React.Component<
}
@autobind
renderValue(item: Option, key?: any) {
const {classnames: cx, labelField, options, hideNodePathLabel} = this.props;
renderValue(option: Option, key?: any) {
const {
classnames: cx,
labelField,
valueField,
options,
hideNodePathLabel
} = this.props;
const inputValue = this.state.inputValue;
const regexp = string2regExp(inputValue || '');
if (hideNodePathLabel) {
return item[labelField || 'label'];
return option[labelField || 'label'];
}
const ancestors = getTreeAncestors(options, item, true);
const ancestors = getTreeAncestors(options, option, true);
return (
<span className={cx('Select-valueLabel')} key={key}>
{`${
ancestors
? ancestors
.map(item => `${item[labelField || 'label']}`)
.join(' / ')
: item[labelField || 'label']
}`}
<span
className={cx('Select-valueLabel')}
key={key || option[valueField || 'value']}
>
{ancestors
? ancestors.map((item, index) => {
const label = item[labelField || 'label'];
const isEnd = index === ancestors.length - 1;
const unmatchText = label.split(regexp || '');
let pointer = 0;
return (
<span key={index}>
{regexp.test(label)
? unmatchText.map((text: string, textIndex: number) => {
const current = pointer;
pointer += text.length || inputValue?.length || 0;
return (
<span
key={textIndex}
className={cx({
'NestedSelect-optionLabel-highlight': !text
})}
>
{text || label.slice(current, pointer)}
</span>
);
})
: label}
{!isEnd && '>'}
</span>
);
})
: option[labelField || 'label']}
</span>
);
}
@ -553,6 +593,96 @@ export default class NestedSelectControl extends React.Component<
);
}
renderSearchResult() {
const {stack, inputValue} = this.state;
const {
classnames: cx,
options: propOptions,
labelField,
cascade,
selectedOptions,
multiple,
disabled,
onlyChildren,
noResultsText
} = this.props;
const regexp = string2regExp(inputValue || '');
const flattenTreeWithNodes = flattenTree(stack[0]).filter(option => {
return regexp.test(option[labelField || 'label']);
});
// 一个stack一个menu
const resultBody = (
<div className={cx('NestedSelect-menu')}>
{flattenTreeWithNodes.length ? (
flattenTreeWithNodes.map((option, index) => {
const ancestors = getTreeAncestors(propOptions, option as any);
const uncheckable = cascade
? false
: multiple &&
ancestors?.some(item => !!~selectedOptions.indexOf(item));
let isNodeDisabled =
uncheckable ||
option.disabled ||
!!disabled ||
ancestors?.some(item => !!item.disabled);
let isChildrenChecked = !!(
option.children && this.partialChecked(option.children)
);
let isChecked = uncheckable || !!~selectedOptions.indexOf(option);
if (
!isChecked &&
onlyChildren &&
option.children &&
this.allChecked(option.children)
) {
isChecked = true;
}
return (
<div
className={cx('NestedSelect-option', {
'is-active':
!isNodeDisabled &&
(isChecked || (!cascade && isChildrenChecked))
})}
key={index}
>
<div
className={cx('NestedSelect-optionLabel', {
'is-disabled': isNodeDisabled
})}
onClick={() => {
!isNodeDisabled &&
(multiple
? this.handleCheck(option, option.value)
: this.handleOptionClick(option));
}}
>
{this.renderValue(option, option.value)}
</div>
</div>
);
})
) : (
<div
className={cx('NestedSelect-option', {
'no-result': true
})}
>
{noResultsText}
</div>
)}
</div>
);
return resultBody;
}
onMouseEnter(option: Option, index: number, e: MouseEvent) {
let {stack} = this.state;
index = index + 1;
@ -579,6 +709,7 @@ export default class NestedSelectControl extends React.Component<
options,
render
} = this.props;
const isSearch = !!this.state.inputValue;
let noResultsText: any = this.props.noResultsText;
if (noResultsText) {
@ -590,7 +721,9 @@ export default class NestedSelectControl extends React.Component<
{(ref: any) => {
return (
<div className={cx('NestedSelect-menuOuter')} ref={ref}>
{options.length ? (
{isSearch ? (
this.renderSearchResult()
) : options.length ? (
this.renderOptions()
) : (
<div className={cx('NestedSelect-noResult')}>
@ -662,12 +795,14 @@ export default class NestedSelectControl extends React.Component<
value={this.state.inputValue}
onChange={this.handleInputChange}
onResultChange={this.handleResultChange}
onClear={this.handleResultClear}
itemRender={this.renderValue}
onKeyPress={this.handleKeyPress}
onFocus={this.onFocus}
onBlur={this.onBlur}
onKeyDown={this.handleInputKeyDown}
clearable={clearable}
hasDropDownArrow={true}
allowInput={searchable}
inputPlaceholder={''}
>
@ -703,3 +838,7 @@ export default class NestedSelectControl extends React.Component<
type: 'nested-select'
})
export class NestedSelectControlRenderer extends NestedSelectControl {}
@OptionsControl({
type: 'cascader-select'
})
export class CascaderSelectControlRenderer extends NestedSelectControl {}