diff --git a/examples/mobile.jsx b/examples/mobile.jsx index 6c5c8fd9e..d7394d249 100644 --- a/examples/mobile.jsx +++ b/examples/mobile.jsx @@ -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) { diff --git a/scss/components/_cascader.scss b/scss/components/_cascader.scss new file mode 100644 index 000000000..6c557ccd4 --- /dev/null +++ b/scss/components/_cascader.scss @@ -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; + } +} diff --git a/scss/components/_popup.scss b/scss/components/_popup.scss index f4d814cd1..7fb834cb4 100644 --- a/scss/components/_popup.scss +++ b/scss/components/_popup.scss @@ -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); + } } diff --git a/scss/components/_result-box.scss b/scss/components/_result-box.scss index 287c09aaf..ccd33da7d 100644 --- a/scss/components/_result-box.scss +++ b/scss/components/_result-box.scss @@ -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); + } + } + } } diff --git a/scss/components/form/_checks.scss b/scss/components/form/_checks.scss index e57a032cc..672cba059 100644 --- a/scss/components/form/_checks.scss +++ b/scss/components/form/_checks.scss @@ -29,6 +29,8 @@ pointer-events: all; margin-left: var(--Checkbox-gap); cursor: pointer; + display: inline-block; + vertical-align: middle; > a { // float: right; diff --git a/scss/components/form/_nested-select.scss b/scss/components/form/_nested-select.scss index b94f48974..ececbda74 100644 --- a/scss/components/form/_nested-select.scss +++ b/scss/components/form/_nested-select.scss @@ -87,4 +87,7 @@ } } } + &-popup { + height: px2rem(460px); + } } diff --git a/scss/components/form/_select.scss b/scss/components/form/_select.scss index 4695d1929..976a58af5 100644 --- a/scss/components/form/_select.scss +++ b/scss/components/form/_select.scss @@ -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 { diff --git a/scss/components/form/_transfer.scss b/scss/components/form/_transfer.scss index 51cbb9610..bdf6c205c 100644 --- a/scss/components/form/_transfer.scss +++ b/scss/components/form/_transfer.scss @@ -235,6 +235,9 @@ display: flex; flex-direction: column; + &.is-mobile { + width: 100%; + } & > .#{$ns}Transfer-selection { flex-grow: 1; max-height: 100%; diff --git a/scss/themes/_common.scss b/scss/themes/_common.scss index e1dde5551..c7c956d54 100644 --- a/scss/themes/_common.scss +++ b/scss/themes/_common.scss @@ -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'; diff --git a/src/components/Cascader.tsx b/src/components/Cascader.tsx new file mode 100644 index 000000000..033c81184 --- /dev/null +++ b/src/components/Cascader.tsx @@ -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 { + static defaultProps = { + labelField: 'label', + valueField: 'value' + }; + tabsRef: React.RefObject = React.createRef(); + tabRef: React.RefObject = 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}) + ) : ( + {option[labelField]} + ); + return ( +
  • this.onSelect(option, tabIndex)} + key={tabIndex + '-' + option[valueField]} + > + {Text} +
  • + ); + } + + @autobind + renderOptions(options: Options, tabIndex: number) { + const {classnames: cx} = this.props; + return ( +
      + {options.map(option => this.renderOption(option, tabIndex))} +
    + ); + } + + @autobind + renderTabs() { + const {classnames: cx, options} = this.props; + const {tabs} = this.state; + const depth = getTreeDepth(options); + return ( +
    3 ? 'scrollable' : '')} + ref={this.tabsRef} + > + {tabs.map((tab: CascaderTab, tabIndex: number) => { + const {options} = tab; + return ( +
    + {this.renderOptions(options, tabIndex)} +
    + ); + })} + {depth <= 3 && options.length + ? Array(getTreeDepth(options) - tabs.length) + .fill(1) + .map((item: number, index: number) => ( +
    + )) + : null} +
    + ); + } + + render() { + const { + classPrefix: ns, + classnames: cx, + className, + onClose, + translate: __ + } = this.props; + + return ( +
    +
    + + +
    + {this.renderTabs()} +
    + ); + } +} + +export default themeable(Cascader); diff --git a/src/components/PopOverContainer.tsx b/src/components/PopOverContainer.tsx index b7a926c98..a729521d8 100644 --- a/src/components/PopOverContainer.tsx +++ b/src/components/PopOverContainer.tsx @@ -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 })} - - - {dropdownRender({onClose: this.close})} - - + + ) : ( + + + {dropdownRender({onClose: this.close})} + + + )} ); } diff --git a/src/components/PopUp.tsx b/src/components/PopUp.tsx index 88b5d1202..b0c230138 100644 --- a/src/components/PopUp.tsx +++ b/src/components/PopUp.tsx @@ -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 { + 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 { 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 {
    )}
    - {!hideClose && ( + {!showConfirm && showClose ? (
    {header} { onClick={onHide} />
    + ) : null} + {showConfirm && ( +
    + + {title && ( + {title} + )} + +
    )}
    - {children} + {isShow ? children : null}
    +
    ); @@ -115,4 +161,4 @@ export class PopUp extends React.PureComponent { } } -export default themeable(PopUp); +export default themeable(localeable(PopUp)); diff --git a/src/components/ResultBox.tsx b/src/components/ResultBox.tsx index 2ab43eea4..8dbf3d407 100644 --- a/src/components/ResultBox.tsx +++ b/src/components/ResultBox.tsx @@ -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) => void; allowInput?: boolean; inputPlaceholder: string; + useMobileUI?: boolean; } export class ResultBox extends React.Component { @@ -115,9 +116,11 @@ export class ResultBox extends React.Component { onFocus, onBlur, borderMode, + useMobileUI, ...rest } = this.props; const isFocused = this.state.isFocused; + const mobileUI = useMobileUI && isMobile(); return (
    { '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 { ) : null} + {!allowInput && mobileUI ? ( + + + + ) : null}
    ); } diff --git a/src/components/Select.tsx b/src/components/Select.tsx index e117d7354..22929da2e 100644 --- a/src/components/Select.tsx +++ b/src/components/Select.tsx @@ -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 { labelField: 'label', options: filtedOptions }; - const menu = mobileUI ? ( - - ) : ( + const menu = (
    100 + 'Select--longlist': + filtedOptions.length && filtedOptions.length > 100, + 'is-mobile': mobileUI })} > {searchable ? ( @@ -1021,8 +1012,15 @@ export class Select extends React.Component { )}
    ); - - return ( + return mobileUI ? ( + + {menu} + + ) : ( { > { 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 ( { 'is-opened': isOpen, 'is-focused': this.state.isFocused, 'is-disabled': disabled, + 'is-mobile': mobileUI, [`Select--border${ucFirst(borderMode)}`]: borderMode }, className diff --git a/src/components/TransferDropDown.tsx b/src/components/TransferDropDown.tsx index 1923d7ec9..715d2c875 100644 --- a/src/components/TransferDropDown.tsx +++ b/src/components/TransferDropDown.tsx @@ -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 { @@ -25,15 +27,22 @@ export class TransferDropDown extends Transfer { onChange, onSearch, multiple, - borderMode + borderMode, + useMobileUI } = this.props; const {inputValue, searchResult} = this.state; + const mobileUI = useMobileUI && isMobile(); return ( ( -
    +
    {onSearch ? (
    { placeholder={__('Select.placeholder')} disabled={disabled} ref={ref} + useMobileUI={useMobileUI} > - - - + {!mobileUI ? ( + + + + ) : ( + <> + )} )} diff --git a/src/renderers/Form/NestedSelect.tsx b/src/renderers/Form/NestedSelect.tsx index 292d6f324..057b9a969 100644 --- a/src/renderers/Form/NestedSelect.tsx +++ b/src/renderers/Form/NestedSelect.tsx @@ -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 (
    {loading ? : undefined} - {this.state.isOpened ? this.renderOuter() : null} + {mobileUI ? ( + + + + ) : this.state.isOpened ? ( + this.renderOuter() + ) : null}
    ); } diff --git a/src/renderers/Form/Select.tsx b/src/renderers/Form/Select.tsx index 2a086393e..86b418921 100644 --- a/src/renderers/Form/Select.tsx +++ b/src/renderers/Form/Select.tsx @@ -91,6 +91,7 @@ export interface SelectProps extends OptionsControlProps { autoComplete?: Api; searchable?: boolean; defaultOpen?: boolean; + useMobileUI?: boolean; } export default class SelectControl extends React.Component { @@ -297,7 +298,6 @@ export default class SelectControl extends React.Component { 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 { @@ -367,7 +368,8 @@ class TransferDropdownRenderer extends BaseTransferRenderer