feat: 移动组件优化:级联选择器、Select选择器、PopUp滑动穿透修复、移动端网络请求修复 (#3281)

* fix: 移动端网络请求修复

* fix: popup滑动穿透修复

* feat: 级联选择器移动端优化

* Update NestedSelect.tsx

* fix: 修复select自定义内容导致复选框错位问题

* fix: PopUp增加底部安全区

* feat: select选择器移动端优化

* Update Cascader.tsx

* Update NestedSelect.tsx

* fix: Select选择器输入框样式优化

* fix: line-height 位置调整

* feat: ResultBox 移动端样式优化

* fix: prettier 格式化

* Update _popup.scss

* Update PopUp.tsx

* Update _popup.scss

* Update TransferDropDown.tsx

fix: 移动端样式调整

* Update PopUp.tsx

* Update Cascader.tsx

* Update NestedSelect.tsx

增加移动端通过接口获取数据

* Update NestedSelect.tsx

Co-authored-by: zhangxulong <zhangxulong@baidu.com>
This commit is contained in:
龙少 2021-12-30 13:12:56 +08:00 committed by GitHub
parent 8bce8a0579
commit a4803196ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 944 additions and 85 deletions

View File

@ -47,7 +47,12 @@ class AMISComponent extends React.Component {
headers //
}) => {
config = {
url,
dataType: 'json',
method,
data,
headers,
responseType,
...config
};
@ -61,12 +66,7 @@ class AMISComponent extends React.Component {
config.validateStatus = function () {
return true;
};
const response = await axios[config.method](
config.url,
config.data,
config
);
const response = await axios(config);
if (response.status >= 400) {
if (response.data) {

View File

@ -0,0 +1,98 @@
.#{$ns}Cascader-tabs {
display: flex;
&.scrollable {
display: block;
overflow-x: auto;
white-space: nowrap;
&::-webkit-scrollbar {
display: none;
}
}
}
.#{$ns}Cascader-tab {
flex: 1;
width: calc((100vw - 20px) / 3);
height: px2rem(370px);
overflow-y: auto;
display: inline-block;
&::-webkit-scrollbar {
display: none;
}
}
.#{$ns}Cascader {
width: 100%;
padding: 0 10px;
&-Nav {
overflow-x: auto;
&Item {
display: inline-block;
margin-right: px2rem(10px);
list-style: none;
cursor: pointer;
user-select: none;
padding: 0 px2rem(6px);
}
}
&-btnGroup {
display: flex;
justify-content: space-between;
align-items: center;
height: px2rem(60px);
}
&-options {
box-sizing: border-box;
height: var(--Cascader-option-height);
padding-top: px2rem(6px);
overflow-y: auto;
-webkit-overflow-scrolling: touch;
margin: 0;
padding: 0;
}
&-option {
display: flex;
align-items: center;
justify-content: space-between;
padding: px2rem(6px) 0;
font-size: var(--fontSizeMd);
line-height: var(--Cascader-option-lineHeight);
cursor: pointer;
position: relative;
&.selected {
span {
color: var(--primary);
}
}
&.disabled {
span {
color: gray;
}
}
&--text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
user-select: none;
}
&-selectedNum {
min-width: px2rem(16px);
height: px2rem(16px);
line-height: px2rem(16px);
border-radius: 100%;
text-align: center;
background: var(--Form-select-menu-onActive-color);
color: var(--white) !important;
font-size: var(--fontSizeSm);
display: inline-block;
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
}
}
&-icon {
color: var(--primary);
}
&-tab {
padding: 0;
}
}

View File

@ -23,6 +23,7 @@
}
.#{$ns}PopUp {
width: 100%;
height: px2rem(400px);
position: fixed;
background: var(--PopOver-bg);
left: 0;
@ -55,38 +56,36 @@
&.in {
animation-name: PopUpIn;
.#{$ns}PopUp-overlay{
.#{$ns}PopUp-overlay {
animation-name: PopUpOpacityIn;
}
}
&.out {
animation-name: PopUpOut;
.#{$ns}PopUp-overlay{
.#{$ns}PopUp-overlay {
animation-name: PopUpOpacityOut;
}
}
&-inner{
&-inner {
position: relative;
overflow: hidden;
height: 100%;
box-sizing: border-box;
background: $white;
padding-top: px2rem(36px);
display: flex;
flex-direction: column;
}
&-closeWrap{
width: 100%;
position: absolute;
left: 0;
top: 0;
&-closeWrap {
position: relative;
text-align: center;
height: px2rem(48px);
line-height: px2rem(48px);
}
&-closeWrap &-close{
&-closeWrap &-close {
position: absolute;
z-index: 1;
color: var(--icon-color);
@ -95,9 +94,29 @@
right: px2rem(15px);
}
&-content{
&-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
height: px2rem(60px);
}
&-title {
font-size: var(--fontSizeMd);
}
&-cancel {
margin-left: var(--gap-sm);
}
&-confirm {
margin-right: var(--gap-sm);
}
&-content {
overflow-y: auto;
height: 100%;
display: flex;
flex: 1;
}
& > * {
@ -112,7 +131,7 @@
right: 0;
z-index: 1;
bottom: 0;
background: rgba(0, 0, 0, 0.3);;
background: rgba(0, 0, 0, 0.3);
opacity: 1;
animation-duration: var(--animation-duration);
animation-fill-mode: both;
@ -124,4 +143,7 @@
&--leftTopLeftBottom {
margin-top: px2rem(-4px);
}
&-safearea {
height: px2rem(16px);
}
}

View File

@ -126,4 +126,35 @@
padding-left: 8px;
min-height: 24px;
}
&.is-mobile {
min-height: calc(var(--Form-input-lineHeight) * var(--fontSizeLg));
border: none;
padding: 0;
font-size: var(--fontSizeLg);
border: none;
justify-content: flex-end;
.#{$ns}ResultBox-arrow {
margin-right: var(--gap-xs);
// margin-left: var(--gap-xs);
width: var(--gap-md);
text-align: center;
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
margin-left: 4px;
> svg {
transition: transform var(--animation-duration);
display: inline-block;
color: var(--Form-select-caret-iconColor);
width: 10px;
height: 10px;
top: 0;
transform: rotate(-90deg);
}
}
}
}

View File

@ -29,6 +29,8 @@
pointer-events: all;
margin-left: var(--Checkbox-gap);
cursor: pointer;
display: inline-block;
vertical-align: middle;
> a {
// float: right;

View File

@ -87,4 +87,7 @@
}
}
}
&-popup {
height: px2rem(460px);
}
}

View File

@ -2,6 +2,7 @@
display: inline-flex;
vertical-align: middle;
text-align: left;
align-items: center;
outline: none;
position: relative;
font-size: var(--Form-input-fontSize);
@ -148,10 +149,27 @@
}
}
&.is-opened &-arrow > svg {
&.is-opened:not(.is-mobile) &-arrow > svg {
transform: rotate(180deg);
}
&.is-mobile {
min-height: calc(var(--Form-input-lineHeight) * var(--fontSizeLg));
border: none;
padding: 0;
font-size: var(--fontSizeLg);
.#{$ns}Select-valueWrap {
text-align: right;
padding-right: 4px;
}
.#{$ns}Select-arrow {
> svg {
transform: rotate(-90deg);
}
}
}
&-menu {
max-height: px2rem(300px);
overflow: auto;
@ -159,6 +177,13 @@
.#{$ns}Checkbox--sm > i {
margin-top: px2rem(-3px);
}
&.is-mobile {
width: 100%;
text-align: center;
.#{$ns}Select-option {
line-height: px2rem(36px);
}
}
}
&--longlist {
overflow: hidden;
@ -282,8 +307,8 @@
}
}
&.is-focused,
&.is-opened {
&.is-focused:not(.is-mobile),
&.is-opened:not(.is-mobile) {
border-color: var(--Form-input-onFocused-borderColor);
color: var(--Form-select-onFocused-color);
}
@ -314,6 +339,10 @@
fill: var(--Form-input-onHover-iconColor);
}
}
&-popup {
height: px2rem(320px);
}
}
.#{$ns}Select-popover {

View File

@ -235,6 +235,9 @@
display: flex;
flex-direction: column;
&.is-mobile {
width: 100%;
}
& > .#{$ns}Transfer-selection {
flex-grow: 1;
max-height: 100%;

View File

@ -112,6 +112,7 @@
@import '../components/form/rating';
@import '../components/form/transfer';
@import '../components/form/nested-select';
@import '../components/cascader';
@import '../components/form/icon-picker';
@import '../components/form/form';
@import '../components/anchor-nav';

565
src/components/Cascader.tsx Normal file
View File

@ -0,0 +1,565 @@
/**
* @file Cascader
* @author fex
*/
import React from 'react';
import {autobind, getTreeAncestors} from '../utils/helper';
import {themeable} from '../theme';
import {NestedSelectProps} from '../renderers/Form/NestedSelect';
import {Option, Options} from './Select';
import intersectionBy from 'lodash/intersectionBy';
import compact from 'lodash/compact';
import find from 'lodash/find';
import uniqBy from 'lodash/uniqBy';
import Button from './Button';
import {flattenTree, findTree, getTreeDepth} from '../utils/helper';
export type CascaderOption = {
text?: string;
value?: string | number;
color?: string;
disabled?: boolean;
children?: Options;
className?: string;
[key: string]: any;
};
export interface CascaderProps extends NestedSelectProps {
value?: (number | string)[];
activeColor?: string;
optionRender?: ({
option,
selected
}: {
option: CascaderOption;
selected: boolean;
}) => React.ReactNode;
onClose?: () => void;
onConfirm?: (param: any) => void;
multiple?: boolean;
}
export type CascaderTab = {
options: Options;
};
export interface CascaderState {
selectedOptions: Options;
activeTab: number;
tabs: Array<{
options: Options;
}>;
}
export class Cascader extends React.Component<CascaderProps, CascaderState> {
static defaultProps = {
labelField: 'label',
valueField: 'value'
};
tabsRef: React.RefObject<HTMLDivElement> = React.createRef();
tabRef: React.RefObject<HTMLDivElement> = React.createRef();
constructor(props: CascaderProps) {
super(props);
this.state = {
selectedOptions: this.props.selectedOptions || [],
activeTab: 0,
tabs: [
{
options: this.props.options.slice() || []
}
]
};
}
componentDidMount() {
const {multiple, options, valueField = 'value', cascade} = this.props;
let selectedOptions = this.props.selectedOptions.slice();
let parentsCount = 0;
let parentTree: Options = [];
selectedOptions.forEach((item: Option) => {
const parents = getTreeAncestors(options, item as any);
// 获取最长路径
if (parents && parents?.length > parentsCount) {
parentTree = parents;
parentsCount = parentTree.length;
}
});
const selectedValues = selectedOptions.map(
(option: Option) => option[valueField]
);
const tabs = parentTree.map((option: Option) => {
if (multiple && !cascade) {
if (
selectedValues.includes(option[valueField]) &&
option?.children?.length
) {
option.children.forEach((option: Option) => (option.disabled = true));
}
}
return multiple
? {
options: [
{
...option,
isCheckAll: true
},
...(option.children ? option.children : [])
]
}
: {
options: option.children ? option.children : []
};
});
this.setState({
selectedOptions,
tabs: [...this.state.tabs, ...tabs]
});
}
@autobind
handleTabSelect(index: number) {
const tabs = this.state.tabs.slice(0, index + 1);
this.setState({
activeTab: index,
tabs
});
}
@autobind
getOptionParent(option: Option) {
const {options, valueField = 'value'} = this.props;
let ancestors: any[] = [];
findTree(options, (item, index, level, paths) => {
if (item[valueField] === option[valueField]) {
ancestors = paths;
return true;
}
return false;
});
return ancestors.length ? ancestors[ancestors.length - 1] : null;
}
@autobind
dealParentSelect(option: Option, selectedOptions: Options): Options {
const {valueField = 'value'} = this.props;
const parentOption = this.getOptionParent(option);
if (parentOption) {
const parentChildren = parentOption?.children;
const equalOption = intersectionBy(
selectedOptions,
parentChildren,
valueField
);
// 包含则选中父节点
const isParentSelected = find(selectedOptions, {
[valueField]: parentOption[valueField]
});
if (equalOption.length === parentChildren?.length && !isParentSelected) {
selectedOptions.push(parentOption);
}
if (equalOption.length !== parentChildren?.length && isParentSelected) {
const index = selectedOptions.findIndex(
(item: Option) => item[valueField] === parentOption[valueField]
);
selectedOptions.splice(index, 1);
}
return this.dealParentSelect(parentOption, selectedOptions);
} else {
return selectedOptions;
}
}
@autobind
flattenTreeWithLeafNodes(option: Option) {
return compact(
flattenTree(Array.isArray(option) ? option : [option], node => node)
);
}
@autobind
adjustOptionSelect(option: Option): boolean {
const {valueField = 'value'} = this.props;
const {selectedOptions} = this.state;
function loop(arr: any[]): boolean {
if (!arr.length) {
return false;
}
return arr.some((item: any) => item[valueField] === option[valueField]);
}
return loop(selectedOptions);
}
@autobind
getSelectedChildNum(option: Option): number {
let count = 0;
const loop = (arr: any[]) => {
if (!arr || !arr.length) {
return;
}
for (let item of arr) {
if (item.children) {
loop(item.children || []);
} else {
if (this.adjustOptionSelect(item)) {
count++;
}
}
}
};
loop(option.children || []);
return count;
}
@autobind
dealOptionDisable(selectedOptions: Options) {
const {
valueField = 'value',
options,
cascade,
multiple,
onlyChildren // 子节点可点击
} = this.props;
if (!multiple || cascade || onlyChildren) {
return;
}
const selectedValues = selectedOptions.map(
(option: Option) => option[valueField]
);
const loop = (option: Option) => {
if (!option.children) {
return;
}
option.children &&
option.children.forEach((childOption: Option) => {
if (
!selectedValues.includes(option[valueField]) &&
!option.disabled
) {
childOption.disabled = false;
}
if (selectedValues.includes(option[valueField]) || option.disabled) {
childOption.disabled = true;
}
loop(childOption);
});
};
options.forEach((option: Option) => loop(option));
}
@autobind
dealChildrenSelect(option: Option, selectedOptions: Options) {
const {valueField = 'value'} = this.props;
let index = selectedOptions.findIndex(
(item: Option) => item[valueField] === option[valueField]
);
if (index !== -1) {
selectedOptions.splice(index, 1);
} else {
selectedOptions.push(option);
}
function loop(option: Option) {
if (!option.children) {
return;
}
option.children.forEach((item: Option) => {
if (index !== -1) {
// 删除选中节点及其子节点
selectedOptions = selectedOptions.filter(
(sItem: Option) => sItem[valueField] !== item[valueField]
);
} else {
// 添加节点及其子节点
selectedOptions.push(item);
}
loop(item);
});
}
loop(option);
return selectedOptions;
}
getParentTree = (option: Option, arr: Options): Options => {
const parentOption = this.getOptionParent(option);
if (parentOption) {
arr.push(parentOption);
return this.getParentTree(parentOption, arr);
}
return arr;
};
@autobind
onSelect(option: CascaderOption, tabIndex: number) {
const {multiple, valueField = 'value', cascade} = this.props;
let tabs = this.state.tabs.slice();
let {activeTab} = this.state;
let selectedOptions = this.state.selectedOptions;
const isDisable = option.disabled;
if (!isDisable) {
if (multiple) {
// 父子级分离
if (cascade) {
if (
option.isCheckAll ||
!option.children ||
!option.children.length
) {
let index = selectedOptions.findIndex(
(item: Option) => item[valueField] === option[valueField]
);
if (index !== -1) {
selectedOptions.splice(index, 1);
} else {
selectedOptions.push(option);
}
}
} else {
if (
option.isCheckAll ||
!option.children ||
!option.children.length
) {
selectedOptions = this.dealChildrenSelect(option, selectedOptions);
selectedOptions = this.dealParentSelect(option, selectedOptions);
}
}
} else {
// 单选
selectedOptions = this.getParentTree(option, [option]);
}
}
this.dealOptionDisable(selectedOptions);
if (tabs.length > tabIndex + 1) {
tabs = tabs.slice(0, tabIndex + 1);
}
requestAnimationFrame(() => {
const tabWidth = this.tabRef.current?.offsetWidth || 1;
const parentTree = this.getParentTree(option, [option]);
const scrollLeft = (parentTree.length - 2) * tabWidth;
if (scrollLeft !== 0) {
(this.tabsRef.current as HTMLElement).scrollTo(scrollLeft, 0);
}
});
if (option?.children && !option.isCheckAll) {
const nextTab = multiple
? {
options: [
{
...option,
isCheckAll: true
},
...option.children
]
}
: {
options: option.children
};
if (tabs[tabIndex + 1]) {
tabs[tabIndex + 1] = nextTab;
} else {
tabs.push(nextTab);
}
activeTab += 1;
}
this.setState({
tabs,
activeTab,
selectedOptions
});
}
@autobind
onNextClick(option: CascaderOption, tabIndex: number) {
let {activeTab} = this.state;
let tabs = this.state.tabs.slice();
if (option.c)
if (option?.children) {
const nextTab = {
options: option.children
};
if (tabs[tabIndex + 1]) {
tabs[tabIndex + 1] = nextTab;
} else {
tabs.push(nextTab);
}
activeTab += 1;
}
this.setState({
tabs,
activeTab
});
}
@autobind
getSubmitOptions(selectedOptions: Options): Options {
const _selectedOptions: Options = [];
const {
multiple,
options,
valueField = 'value',
cascade,
onlyChildren,
withChildren
} = this.props;
if (cascade || onlyChildren || withChildren || !multiple) {
return selectedOptions;
}
const selectedValues = selectedOptions.map(
(option: Option) => option[valueField]
);
function loop(options: Options) {
if (!options || !options.length) {
return;
}
options.forEach((option: Option) => {
if (selectedValues.includes(option[valueField])) {
_selectedOptions.push(option);
} else {
loop(option.children ? option.children : []);
}
});
}
loop(options);
return _selectedOptions;
}
@autobind
confirm() {
const {onChange, joinValues, delimiter, extractValue, valueField, onClose} =
this.props;
let {selectedOptions} = this.state;
let _selectedOptions = this.getSubmitOptions(selectedOptions);
_selectedOptions = uniqBy(_selectedOptions, valueField);
onChange(
joinValues
? _selectedOptions
.map(item => item[valueField as string])
.join(delimiter)
: extractValue
? _selectedOptions.map(item => item[valueField as string])
: _selectedOptions
);
onClose && onClose();
}
@autobind
renderOption(option: CascaderOption, tabIndex: number) {
const {
activeColor,
optionRender,
labelField,
valueField = 'value',
classnames: cx,
cascade,
multiple
} = this.props;
const {selectedOptions} = this.state;
const selectedValueArr = selectedOptions.map(item => item[valueField]);
let selfChecked = selectedValueArr.includes(option[valueField]);
const color = option.color || (selfChecked ? activeColor : undefined);
const Text = optionRender ? (
optionRender({option, selected: selfChecked})
) : (
<span>{option[labelField]}</span>
);
return (
<li
className={cx(
'Cascader-option',
{
selected: selfChecked,
disabled: option.disabled
},
option.className
)}
style={{color}}
onClick={() => this.onSelect(option, tabIndex)}
key={tabIndex + '-' + option[valueField]}
>
<span className={cx('Cascader-option--text')}>{Text}</span>
</li>
);
}
@autobind
renderOptions(options: Options, tabIndex: number) {
const {classnames: cx} = this.props;
return (
<ul key={tabIndex} className={cx('Cascader-options')}>
{options.map(option => this.renderOption(option, tabIndex))}
</ul>
);
}
@autobind
renderTabs() {
const {classnames: cx, options} = this.props;
const {tabs} = this.state;
const depth = getTreeDepth(options);
return (
<div
className={cx(`Cascader-tabs`, depth > 3 ? 'scrollable' : '')}
ref={this.tabsRef}
>
{tabs.map((tab: CascaderTab, tabIndex: number) => {
const {options} = tab;
return (
<div
className={cx(`Cascader-tab`)}
ref={this.tabRef}
key={tabIndex}
>
{this.renderOptions(options, tabIndex)}
</div>
);
})}
{depth <= 3 && options.length
? Array(getTreeDepth(options) - tabs.length)
.fill(1)
.map((item: number, index: number) => (
<div className={cx(`Cascader-tab`)} key={index}></div>
))
: null}
</div>
);
}
render() {
const {
classPrefix: ns,
classnames: cx,
className,
onClose,
translate: __
} = this.props;
return (
<div className={cx(`Cascader`, className)}>
<div className={cx(`Cascader-btnGroup`)}>
<Button
className={cx(`Cascader-btnCancel`)}
level="default"
onClick={onClose}
>
{__('cancel')}
</Button>
<Button
className={cx(`Cascader-btnConfirm`)}
level="primary"
onClick={this.confirm}
>
{__('confirm')}
</Button>
</div>
{this.renderTabs()}
</div>
);
}
}
export default themeable(Cascader);

View File

@ -1,7 +1,8 @@
import React from 'react';
import {autobind} from '../utils/helper';
import {autobind, isMobile} from '../utils/helper';
import Overlay from './Overlay';
import PopOver from './PopOver';
import PopUp from './PopUp';
import {findDOMNode} from 'react-dom';
export interface PopOverContainerProps {
@ -13,6 +14,7 @@ export interface PopOverContainerProps {
popOverRender: (props: {onClose: () => void}) => JSX.Element;
popOverContainer?: any;
popOverClassName?: string;
useMobileUI?: boolean;
}
export interface PopOverContainerState {
@ -60,11 +62,13 @@ export class PopOverContainer extends React.Component<
render() {
const {
useMobileUI,
children,
popOverContainer,
popOverClassName,
popOverRender: dropdownRender
} = this.props;
const mobileUI = useMobileUI && isMobile();
return (
<>
{children({
@ -72,26 +76,35 @@ export class PopOverContainer extends React.Component<
onClick: this.handleClick,
ref: this.targetRef
})}
<Overlay
container={popOverContainer || this.getParent}
target={this.getTarget}
placement={'auto'}
show={this.state.isOpened}
>
<PopOver
overlay
{mobileUI ? (
<PopUp
isShow={this.state.isOpened}
className={popOverClassName}
style={{
minWidth: this.target
? Math.max(this.target.offsetWidth, 100)
: 'auto'
}}
onHide={this.close}
>
{dropdownRender({onClose: this.close})}
</PopOver>
</Overlay>
</PopUp>
) : (
<Overlay
container={popOverContainer || this.getParent}
target={this.getTarget}
placement={'auto'}
show={this.state.isOpened}
>
<PopOver
overlay
className={popOverClassName}
style={{
minWidth: this.target
? Math.max(this.target.offsetWidth, 100)
: 'auto'
}}
onHide={this.close}
>
{dropdownRender({onClose: this.close})}
</PopOver>
</Overlay>
)}
</>
);
}

View File

@ -5,7 +5,8 @@
*/
import React from 'react';
import {ClassNamesFn, themeable} from '../theme';
import {themeable, ThemeProps} from '../theme';
import {localeable, LocaleProps} from '../locale';
import Transition, {
ENTERED,
EXITING,
@ -14,20 +15,21 @@ import Transition, {
} from 'react-transition-group/Transition';
import Portal from 'react-overlays/Portal';
import {Icon} from './icons';
import Button from './Button';
export interface PopUpPorps {
export interface PopUpPorps extends ThemeProps, LocaleProps {
title?: string;
className?: string;
style?: {
[styleName: string]: string;
};
overlay?: boolean;
onHide?: () => void;
classPrefix: string;
classnames: ClassNamesFn;
[propName: string]: any;
isShow?: boolean;
container?: any;
hideClose?: boolean;
showConfirm?: boolean;
onConfirm?: (value: any) => void;
showClose?: boolean;
placement?: 'left' | 'center' | 'right';
header?: JSX.Element;
}
@ -41,15 +43,33 @@ const fadeStyles: {
[ENTERING]: 'in'
};
export class PopUp extends React.PureComponent<PopUpPorps> {
scrollTop: number = 0;
static defaultProps = {
className: '',
overlay: true,
isShow: false,
container: document.body,
hideClose: false
showClose: true,
onConfirm: () => {}
};
componentDidMount() {}
componentDidUpdate() {
if (this.props.isShow) {
this.scrollTop =
document.body.scrollTop || document.documentElement.scrollTop;
document.body.style.overflow =
'hidden';
} else {
document.body.style.overflow =
'auto';
document.body.scrollTop =
this.scrollTop;
}
}
componentWillUnmount() {
document.body.style.overflow = 'auto';
document.body.scrollTop =
this.scrollTop;
}
handleClick(e: React.MouseEvent) {
e.stopPropagation();
}
@ -57,15 +77,19 @@ export class PopUp extends React.PureComponent<PopUpPorps> {
render() {
const {
style,
title,
children,
overlay,
onHide,
onConfirm,
classPrefix: ns,
classnames: cx,
className,
isShow,
container,
hideClose,
showConfirm,
translate: __,
showClose,
header,
placement = 'center',
...rest
@ -90,7 +114,7 @@ export class PopUp extends React.PureComponent<PopUpPorps> {
<div className={`${ns}PopUp-overlay`} onClick={onHide} />
)}
<div className={cx(`${ns}PopUp-inner`)}>
{!hideClose && (
{!showConfirm && showClose ? (
<div className={cx(`${ns}PopUp-closeWrap`)}>
{header}
<Icon
@ -99,12 +123,34 @@ export class PopUp extends React.PureComponent<PopUpPorps> {
onClick={onHide}
/>
</div>
) : null}
{showConfirm && (
<div className={cx(`${ns}PopUp-toolbar`)}>
<Button
className={cx(`${ns}PopUp-cancel`)}
level="default"
onClick={onHide}
>
{__('cancel')}
</Button>
{title && (
<span className={cx(`${ns}PopUp-title`)}>{title}</span>
)}
<Button
className={cx(`${ns}PopUp-confirm`)}
level="primary"
onClick={onConfirm}
>
{__('confirm')}
</Button>
</div>
)}
<div
className={cx(`${ns}PopUp-content`, `justify-${placement}`)}
>
{children}
{isShow ? children : null}
</div>
<div className={cx(`PopUp-safearea`)}></div>
</div>
</div>
);
@ -115,4 +161,4 @@ export class PopUp extends React.PureComponent<PopUpPorps> {
}
}
export default themeable(PopUp);
export default themeable(localeable(PopUp));

View File

@ -4,7 +4,7 @@ import {InputBoxProps} from './InputBox';
import {uncontrollable} from 'uncontrollable';
import {Icon} from './icons';
import Input from './Input';
import {autobind, ucFirst} from '../utils/helper';
import {autobind, isMobile, ucFirst} from '../utils/helper';
import {LocaleProps, localeable} from '../locale';
import isPlainObject = require('lodash/isPlainObject');
@ -19,6 +19,7 @@ export interface ResultBoxProps
onResultChange?: (value: Array<any>) => void;
allowInput?: boolean;
inputPlaceholder: string;
useMobileUI?: boolean;
}
export class ResultBox extends React.Component<ResultBoxProps> {
@ -115,9 +116,11 @@ export class ResultBox extends React.Component<ResultBoxProps> {
onFocus,
onBlur,
borderMode,
useMobileUI,
...rest
} = this.props;
const isFocused = this.state.isFocused;
const mobileUI = useMobileUI && isMobile();
return (
<div
@ -126,6 +129,7 @@ export class ResultBox extends React.Component<ResultBoxProps> {
'is-disabled': disabled,
'is-error': hasError,
'is-clickable': onResultClick,
'is-mobile': mobileUI,
[`ResultBox--border${ucFirst(borderMode)}`]: borderMode
})}
onClick={onResultClick}
@ -183,6 +187,11 @@ export class ResultBox extends React.Component<ResultBoxProps> {
<Icon icon="close" className="icon" />
</a>
) : null}
{!allowInput && mobileUI ? (
<span className={cx('ResultBox-arrow')}>
<Icon icon="caret" className="icon" />
</span>
) : null}
</div>
);
}

View File

@ -37,6 +37,7 @@ import Spinner from './Spinner';
import {Option, Options} from '../Schema';
import {RemoteOptionsProps, withRemoteConfig} from './WithRemoteConfig';
import Picker from './Picker';
import PopUp from './PopUp';
export {Option, Options};
@ -923,23 +924,13 @@ export class Select extends React.Component<SelectProps, SelectState> {
labelField: 'label',
options: filtedOptions
};
const menu = mobileUI ? (
<Picker
className={cx('PickerColumns-column', mobileClassName)}
labelField="label"
value={value[0]}
translate={this.props.translate}
locale={this.props.locale}
columns={[column]}
onChange={checkAll ? noop : this.handlePickerChange}
onClose={this.close}
onConfirm={this.confirm}
/>
) : (
const menu = (
<div
ref={this.menu}
className={cx('Select-menu', {
'Select--longlist': filtedOptions.length && filtedOptions.length > 100
'Select--longlist':
filtedOptions.length && filtedOptions.length > 100,
'is-mobile': mobileUI
})}
>
{searchable ? (
@ -1021,8 +1012,15 @@ export class Select extends React.Component<SelectProps, SelectState> {
)}
</div>
);
return (
return mobileUI ? (
<PopUp
className={cx(`Select-popup`)}
isShow={this.state.isOpen}
onHide={this.close}
>
{menu}
</PopUp>
) : (
<Overlay
container={popOverContainer || this.getTarget}
target={this.getTarget}
@ -1031,11 +1029,7 @@ export class Select extends React.Component<SelectProps, SelectState> {
>
<PopOver
overlay
className={cx(
'Select-popover',
popoverClassName,
mobileUI ? 'PopOver-isMobile' : ''
)}
className={cx('Select-popover')}
style={{
minWidth: this.target ? this.target.offsetWidth : 'auto'
}}
@ -1061,13 +1055,14 @@ export class Select extends React.Component<SelectProps, SelectState> {
labelField,
disabled,
checkAll,
borderMode
borderMode,
useMobileUI
} = this.props;
const selection = this.state.selection;
const inputValue = this.state.inputValue;
const resetValue = this.props.resetValue;
const mobileUI = useMobileUI && isMobile();
return (
<Downshift
selectedItem={selection}
@ -1101,6 +1096,7 @@ export class Select extends React.Component<SelectProps, SelectState> {
'is-opened': isOpen,
'is-focused': this.state.isFocused,
'is-disabled': disabled,
'is-mobile': mobileUI,
[`Select--border${ucFirst(borderMode)}`]: borderMode
},
className

View File

@ -7,11 +7,13 @@ import ResultBox from './ResultBox';
import {Icon} from './icons';
import InputBox from './InputBox';
import PopOverContainer from './PopOverContainer';
import {isMobile} from '../utils/helper';
export interface TransferDropDownProps extends TransferProps {
// 新的属性?
multiple?: boolean;
borderMode?: 'full' | 'half' | 'none';
useMobileUI?: boolean;
}
export class TransferDropDown extends Transfer<TransferDropDownProps> {
@ -25,15 +27,22 @@ export class TransferDropDown extends Transfer<TransferDropDownProps> {
onChange,
onSearch,
multiple,
borderMode
borderMode,
useMobileUI
} = this.props;
const {inputValue, searchResult} = this.state;
const mobileUI = useMobileUI && isMobile();
return (
<PopOverContainer
useMobileUI={useMobileUI}
popOverClassName={cx('TransferDropDown-popover')}
popOverRender={({onClose}) => (
<div className={cx('TransferDropDown-content')}>
<div
className={cx('TransferDropDown-content', {
'is-mobile': mobileUI
})}
>
{onSearch ? (
<div className={cx('Transfer-search')}>
<InputBox
@ -94,10 +103,15 @@ export class TransferDropDown extends Transfer<TransferDropDownProps> {
placeholder={__('Select.placeholder')}
disabled={disabled}
ref={ref}
useMobileUI={useMobileUI}
>
<span className={cx('TransferDropDown-icon')}>
<Icon icon="caret" className="icon" />
</span>
{!mobileUI ? (
<span className={cx('TransferDropDown-icon')}>
<Icon icon="caret" className="icon" />
</span>
) : (
<></>
)}
</ResultBox>
)}
</PopOverContainer>

View File

@ -2,6 +2,7 @@ import React from 'react';
import Overlay from '../../components/Overlay';
import Checkbox from '../../components/Checkbox';
import PopOver from '../../components/PopOver';
import PopUp from '../../components/PopUp';
import {Icon} from '../../components/icons';
import {
autobind,
@ -10,7 +11,8 @@ import {
string2regExp,
getTreeAncestors,
getTreeParent,
ucFirst
ucFirst,
isMobile
} from '../../utils/helper';
import {
FormOptionsControl,
@ -24,6 +26,7 @@ import xor from 'lodash/xor';
import union from 'lodash/union';
import compact from 'lodash/compact';
import {RootClose} from '../../utils/RootClose';
import Cascader from '../../components/Cascader';
/**
* Nested Select
@ -68,6 +71,7 @@ export interface NestedSelectProps extends OptionsControlProps {
withChildren?: boolean;
onlyChildren?: boolean;
hideNodePathLabel?: boolean;
useMobileUI?: boolean;
}
export interface NestedSelectState {
@ -625,12 +629,15 @@ export default class NestedSelectControl extends React.Component<
selectedOptions,
clearable,
loading,
borderMode
borderMode,
useMobileUI
} = this.props;
const mobileUI = useMobileUI && isMobile();
return (
<div className={cx('NestedSelectControl', className)}>
<ResultBox
useMobileUI={useMobileUI}
disabled={disabled}
ref={this.domRef}
placeholder={__(placeholder || '空')}
@ -665,7 +672,24 @@ export default class NestedSelectControl extends React.Component<
>
{loading ? <Spinner size="sm" /> : undefined}
</ResultBox>
{this.state.isOpened ? this.renderOuter() : null}
{mobileUI ? (
<PopUp
className={cx(`NestedSelect-popup`)}
isShow={this.state.isOpened}
onHide={this.close}
showConfirm={false}
showClose={false}
>
<Cascader
onClose={this.close}
{...this.props}
options={this.props.options.slice()}
value={selectedOptions}
/>
</PopUp>
) : this.state.isOpened ? (
this.renderOuter()
) : null}
</div>
);
}

View File

@ -91,6 +91,7 @@ export interface SelectProps extends OptionsControlProps {
autoComplete?: Api;
searchable?: boolean;
defaultOpen?: boolean;
useMobileUI?: boolean;
}
export default class SelectControl extends React.Component<SelectProps, any> {
@ -297,7 +298,6 @@ export default class SelectControl extends React.Component<SelectProps, any> {
menuTpl,
borderMode,
selectMode,
env,
...rest
} = this.props;
@ -347,6 +347,7 @@ export interface TransferDropDownProps
| 'descriptionClassName'
> {
borderMode?: 'full' | 'half' | 'none';
useMobileUI?: boolean;
}
class TransferDropdownRenderer extends BaseTransferRenderer<TransferDropDownProps> {
@ -367,7 +368,8 @@ class TransferDropdownRenderer extends BaseTransferRenderer<TransferDropDownProp
multiple,
columns,
leftMode,
borderMode
borderMode,
useMobileUI
} = this.props;
// 目前 LeftOptions 没有接口可以动态加载
@ -407,6 +409,7 @@ class TransferDropdownRenderer extends BaseTransferRenderer<TransferDropDownProp
leftMode={leftMode}
leftOptions={leftOptions}
borderMode={borderMode}
useMobileUI={useMobileUI}
/>
<Spinner overlay key="info" show={loading} />