feat:新增移动端PopUp组件 (#3142)

* 新增移动端PopUp组件

* Update DatePicker.tsx

Co-authored-by: zhangxulong <zhangxulong@baidu.com>
Co-authored-by: 吴多益 <wuduoyi@baidu.com>
This commit is contained in:
龙少 2021-12-20 15:43:38 +08:00 committed by GitHub
parent 089494452c
commit a03a4bbbde
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 479 additions and 79 deletions

123
scss/components/_popup.scss Normal file
View File

@ -0,0 +1,123 @@
@keyframes PopUpIn {
from {
height: 0;
}
}
@keyframes PopUpOut {
to {
height: 0;
}
}
@keyframes PopUpOpacityIn {
from {
opacity: 0;
}
}
@keyframes PopUpOpacityOut {
to {
opacity: 0;
}
}
.#{$ns}PopUp {
width: 100%;
position: fixed;
background: var(--PopOver-bg);
left: 0;
bottom: 0;
z-index: $zindex-popover;
padding: 0;
margin: 0;
font-weight: var(--fontWeightNormal);
letter-spacing: normal;
line-height: var(--lineHeightBase);
text-align: left;
text-align: start;
text-decoration: none;
text-shadow: none;
text-transform: none;
white-space: normal;
word-break: normal;
word-spacing: normal;
word-wrap: normal;
font-size: var(--fontSizeBase);
box-shadow: var(--boxShadow);
border: var(--borderWidth) solid var(--borderColor);
border-radius: var(--borderRadius);
overflow: hidden;
&.in,
&.out {
animation-duration: var(--animation-duration);
animation-fill-mode: both;
}
&.in {
animation-name: PopUpIn;
.#{$ns}PopUp-overlay{
animation-name: PopUpOpacityIn;
}
}
&.out {
animation-name: PopUpOut;
.#{$ns}PopUp-overlay{
animation-name: PopUpOpacityOut;
}
}
&-inner{
position: relative;
overflow: hidden;
height: 100%;
box-sizing: border-box;
background: $white;
padding-top: px2rem(36px);
}
&-closeWrap{
width: 100%;
position: absolute !important;
left: 0;
top: 0;
}
&-close{
width: px2rem(34px) !important;
height: px2rem(34px) !important;
padding: px2rem(12px);
cursor: pointer;
}
&-content{
overflow-y: auto;
height: 100%;
display: flex;
}
& > * {
position: relative;
z-index: 2;
}
&-overlay {
position: fixed !important;
top: 0;
left: 0;
right: 0;
z-index: 1;
bottom: 0;
background: rgba(0, 0, 0, 0.3);;
opacity: 1;
animation-duration: var(--animation-duration);
animation-fill-mode: both;
}
&--leftBottomLeftTop {
margin-top: px2rem(4px);
}
&--leftTopLeftBottom {
margin-top: px2rem(-4px);
}
}

View File

@ -10,6 +10,10 @@
color: var(--ColorPicker-color);
border-radius: var(--borderRadius);
&-popup{
height: 80vh;
}
&:not(.is-disabled) {
cursor: pointer;

View File

@ -127,6 +127,10 @@
}
}
.#{$ns}DateRangePicker-popup {
height: 90vh;
}
@include media-breakpoint-up(sm) {
.#{$ns}DateRangePicker-wrap {
white-space: nowrap;

View File

@ -122,6 +122,9 @@
}
}
.#{$ns}DatePicker-popup {
height: 80vh;
}
// override third-party styles
.rdt {
user-select: none;

View File

@ -15,6 +15,10 @@
box-shadow: var(--Form-input-boxShadow);
background: var(--Form-input-onFocused-bg);
}
&-popup {
height: 80vh;
}
}
.#{$ns}TreeSelect-popover {

View File

@ -18,6 +18,7 @@
@import '../components/tooltip';
@import '../components/tpl';
@import '../components/popover';
@import '../components/popup';
@import '../components/picker-columns';
@import '../components/toast';
@import '../components/alert';

View File

@ -11,8 +11,9 @@ import {Icon} from './icons';
import Overlay from './Overlay';
import {uncontrollable} from 'uncontrollable';
import PopOver from './PopOver';
import PopUp from './PopUp';
import {ClassNamesFn, themeable, ThemeProps} from '../theme';
import {autobind, isObject} from '../utils/helper';
import {autobind, isMobile, isObject} from '../utils/helper';
import {localeable, LocaleProps} from '../locale';
export type PresetColor = {color: string; title: string} | string;
@ -32,6 +33,7 @@ export interface ColorProps extends LocaleProps, ThemeProps {
presetColors?: PresetColor[];
resetValue?: string;
allowCustomColor?: boolean;
useMobileUI?: boolean;
}
export interface ColorControlState {
@ -218,7 +220,8 @@ export class ColorControl extends React.PureComponent<
placement,
classnames: cx,
presetColors,
allowCustomColor
allowCustomColor,
useMobileUI
} = this.props;
const __ = this.props.translate;
@ -270,7 +273,7 @@ export class ColorControl extends React.PureComponent<
<Icon icon="caret" className="icon" onClick={this.handleClick} />
</span>
{isOpened ? (
{!(useMobileUI && isMobile()) && isOpened ? (
<Overlay
placement={placement || 'auto'}
target={() => findDOMNode(this)}
@ -317,6 +320,45 @@ export class ColorControl extends React.PureComponent<
</PopOver>
</Overlay>
) : null}
{
useMobileUI && isMobile() && (
<PopUp
className={cx(`${ns}ColorPicker-popup`)}
isShow={isOpened}
onHide={this.handleClick}
>
{allowCustomColor ? (
<SketchPicker
styles={{}}
disableAlpha={!!~['rgb', 'hex'].indexOf(format as string)}
color={value}
presetColors={presetColors}
onChangeComplete={this.handleChange}
/>
) : (
<GithubPicker
color={value}
colors={
Array.isArray(presetColors)
? (presetColors
.filter(
item => typeof item === 'string' || isObject(item)
)
.map(item =>
typeof item === 'string'
? item
: isObject(item)
? item?.color
: item
) as string[])
: undefined
}
onChangeComplete={this.handleChange}
/>
)}
</PopUp>
)
}
</div>
);
}

View File

@ -9,13 +9,14 @@ import moment from 'moment';
import 'moment/locale/zh-cn';
import {Icon} from './icons';
import PopOver from './PopOver';
import PopUp from './PopUp';
import Overlay from './Overlay';
import {ClassNamesFn, themeable, ThemeProps} from '../theme';
import {PlainObject} from '../types';
import Calendar from './calendar/Calendar';
import 'react-datetime/css/react-datetime.css';
import {localeable, LocaleProps, TranslateFn} from '../locale';
import {ucFirst} from '../utils/helper';
import { localeable, LocaleProps, TranslateFn } from '../locale';
import {isMobile, ucFirst} from '../utils/helper';
const availableShortcuts: {[propName: string]: any} = {
now: {
@ -559,6 +560,7 @@ export class DatePicker extends React.Component<DateProps, DatePickerState> {
borderMode,
embed,
minDate,
useMobileUI,
schedules,
largeMode,
scheduleClassNames,
@ -662,7 +664,7 @@ export class DatePicker extends React.Component<DateProps, DatePickerState> {
<Icon icon="clock" className="icon" />
</a>
{isOpened ? (
{!(useMobileUI && isMobile()) && isOpened ? (
<Overlay
target={this.getTarget}
container={popOverContainer || this.getParent}
@ -698,6 +700,34 @@ export class DatePicker extends React.Component<DateProps, DatePickerState> {
</PopOver>
</Overlay>
) : null}
{
useMobileUI && isMobile() ? (
<PopUp
className={cx(`${ns}DatePicker-popup`)}
isShow={isOpened}
onHide={this.handleClick}
>
{this.renderShortCuts(shortcuts)}
<Calendar
value={date}
onChange={this.handleChange}
requiredConfirm={!!(dateFormat && timeFormat)}
dateFormat={dateFormat}
inputFormat={inputFormat}
timeFormat={timeFormat}
isValidDate={this.checkIsValidDate}
viewMode={viewMode}
timeConstraints={timeConstraints}
input={false}
onClose={this.close}
locale={locale}
minDate={minDate}
// utc={utc}
/>
</PopUp>
) : null
}
</div>
);
}

View File

@ -13,9 +13,10 @@ import Overlay from './Overlay';
import {ShortCuts, ShortCutDateRange} from './DatePicker';
import Calendar from './calendar/Calendar';
import PopOver from './PopOver';
import PopUp from './PopUp';
import {ClassNamesFn, themeable, ThemeProps} from '../theme';
import {PlainObject} from '../types';
import {noop, ucFirst} from '../utils/helper';
import {isMobile, noop, ucFirst} from '../utils/helper';
import {LocaleProps, localeable} from '../locale';
export interface DateRangePickerProps extends ThemeProps, LocaleProps {
@ -47,6 +48,7 @@ export interface DateRangePickerProps extends ThemeProps, LocaleProps {
embed?: boolean;
viewMode?: 'days' | 'months' | 'years' | 'time' | 'quarters';
borderMode?: 'full' | 'half' | 'none';
useMobileUI?: boolean;
}
export interface DateRangePickerState {
@ -760,7 +762,8 @@ export class DateRangePicker extends React.Component<
disabled,
embed,
overlayPlacement,
borderMode
borderMode,
useMobileUI
} = this.props;
const {isOpened, isFocused} = this.state;
@ -836,7 +839,7 @@ export class DateRangePicker extends React.Component<
<Icon icon="clock" className="icon" />
</a>
{isOpened ? (
{!(useMobileUI && isMobile()) && isOpened ? (
<Overlay
target={() => this.dom.current}
onHide={this.close}
@ -856,6 +859,17 @@ export class DateRangePicker extends React.Component<
</PopOver>
</Overlay>
) : null}
{
useMobileUI && isMobile() && (
<PopUp
isShow={isOpened}
className={cx(`${ns}DateRangePicker-popup`)}
onHide={this.handleClick}
>
{this.renderCalendar()}
</PopUp>
)
}
</div>
);
}

View File

@ -12,14 +12,15 @@ import {Icon} from './icons';
import Overlay from './Overlay';
import Calendar from './calendar/Calendar';
import PopOver from './PopOver';
import PopUp from './PopUp';
import {themeable, ThemeProps} from '../theme';
import {PlainObject} from '../types';
import {noop} from '../utils/helper';
import {isMobile, noop} from '../utils/helper';
import {LocaleProps, localeable} from '../locale';
import {DateRangePicker} from './DateRangePicker';
import capitalize from 'lodash/capitalize';
import {ShortCuts, ShortCutDateRange} from './DatePicker';
import {availableRanges} from './DateRangePicker';
import { availableRanges } from './DateRangePicker';
export interface MonthRangePickerProps extends ThemeProps, LocaleProps {
className?: string;
@ -47,6 +48,7 @@ export interface MonthRangePickerProps extends ThemeProps, LocaleProps {
resetValue?: any;
popOverContainer?: any;
embed?: boolean;
useMobileUI?: boolean;
}
export interface MonthRangePickerState {
@ -528,7 +530,8 @@ export class MonthRangePicker extends React.Component<
clearable,
disabled,
embed,
overlayPlacement
overlayPlacement,
useMobileUI
} = this.props;
const {isOpened, isFocused} = this.state;
@ -603,7 +606,7 @@ export class MonthRangePicker extends React.Component<
<Icon icon="clock" className="icon" />
</a>
{isOpened ? (
{!(useMobileUI && isMobile()) && isOpened ? (
<Overlay
target={() => this.dom.current}
onHide={this.close}
@ -623,6 +626,17 @@ export class MonthRangePicker extends React.Component<
</PopOver>
</Overlay>
) : null}
{
useMobileUI && isMobile() && (
<PopUp
className={cx(`${ns}DateRangePicker-popup`)}
isShow={isOpened}
onHide={this.handleClick}
>
{this.renderCalendar()}
</PopUp>
)
}
</div>
);
}

133
src/components/PopUp.tsx Normal file
View File

@ -0,0 +1,133 @@
/**
* @file PopUp
* @description
* @author fex
*/
import React from 'react';
import {ClassNamesFn, themeable} from '../theme';
import Transition, {
ENTERED,
EXITING,
EXITED,
ENTERING
} from 'react-transition-group/Transition';
import Portal from 'react-overlays/Portal';
import { Icon } from './icons';
export interface PopUpPorps {
className?: string;
style?: {
[styleName: string]: string;
};
overlay?: boolean;
onHide?: () => void;
classPrefix: string;
classnames: ClassNamesFn;
[propName: string]: any;
isShow?: boolean;
container?: any;
hideClose?: boolean;
placement?: 'left' | 'center' | 'right';
}
const fadeStyles: {
[propName: string]: string;
} = {
[ENTERED]: '',
[EXITING]: 'out',
[EXITED]: '',
[ENTERING]: 'in'
};
export class PopUp extends React.PureComponent<PopUpPorps> {
static defaultProps = {
className: '',
overlay: true,
isShow: false,
container: document.body,
hideClose: false,
};
componentDidMount() {
}
handleClick(e: React.MouseEvent) {
e.stopPropagation();
}
render() {
const {
style,
children,
overlay,
onHide,
classPrefix: ns,
classnames: cx,
className,
isShow,
container,
hideClose,
placement='center',
...rest
} = this.props;
const outerStyle: any = {
...style,
};
delete outerStyle.top;
return (
<Portal container={container}>
<Transition
mountOnEnter
unmountOnExit
in={isShow}
timeout={500}
appear
>
{(status: string) => {
return (
<div
className={cx(
`${ns}PopUp`,
className,
fadeStyles[status]
)}
style={outerStyle}
{...rest}
onClick={this.handleClick}
>
{overlay && (
<div className={`${ns}PopUp-overlay`} onClick={onHide}/>
)}
<div className={cx(
`${ns}PopUp-inner`
)}
>
{
!hideClose && (
<div className={cx(`${ns}PopUp-closeWrap`, 'text-right')}>
<Icon
icon="close"
className={cx('icon', `${ns}PopUp-close`)}
onClick={onHide}
/>
</div>
)
}
<div
className={cx(`${ns}PopUp-content`, `justify-${placement}`)}
>
{children}
</div>
</div>
</div>
)
}}
</Transition>
</Portal>
)
}
}
export default themeable(PopUp);

View File

@ -68,12 +68,11 @@ export default class ColorControl extends React.PureComponent<
};
render() {
const {className, classPrefix: ns, value, ...rest} = this.props;
const {className, classPrefix: ns, value, env, ...rest} = this.props;
return (
<div className={cx(`${ns}ColorControl`, className)}>
<Suspense fallback={<div>...</div>}>
<ColorPicker classPrefix={ns} {...rest} value={value || ''} />
<ColorPicker classPrefix={ns} {...rest} value={value || ''} useMobileUI={env.useMobileUI}/>
</Suspense>
</div>
);

View File

@ -424,6 +424,7 @@ export default class DateControl extends React.PureComponent<
format,
timeFormat,
valueFormat,
env,
largeMode,
render,
...rest
@ -441,6 +442,7 @@ export default class DateControl extends React.PureComponent<
format={valueFormat || format}
{...this.state}
classnames={cx}
useMobileUI={env.useMobileUI}
schedules={this.state.schedules}
largeMode={largeMode}
onScheduleClick={this.onScheduleClick.bind(this)}

View File

@ -172,6 +172,7 @@ export default class DateRangeControl extends React.Component<DateRangeProps> {
maxDuration,
data,
format,
env,
...rest
} = this.props;
@ -186,6 +187,7 @@ export default class DateRangeControl extends React.Component<DateRangeProps> {
maxDate={maxDate ? filterDate(maxDate, data, format) : undefined}
minDuration={minDuration ? parseDuration(minDuration) : undefined}
maxDuration={maxDuration ? parseDuration(maxDuration) : undefined}
useMobileUI={env.useMobileUI}
/>
</div>
);

View File

@ -170,6 +170,7 @@ export default class MonthRangeControl extends React.Component<MonthRangeProps>
maxDuration,
data,
format,
env,
...rest
} = this.props;
@ -184,6 +185,7 @@ export default class MonthRangeControl extends React.Component<MonthRangeProps>
maxDate={maxDate ? filterDate(maxDate, data, format) : undefined}
minDuration={minDuration ? parseDuration(minDuration) : undefined}
maxDuration={maxDuration ? parseDuration(maxDuration) : undefined}
useMobileUI={env.useMobileUI}
/>
</div>
);

View File

@ -26,6 +26,7 @@ export default class QuarterRangeControl extends InputDateRange {
maxDuration,
data,
format,
env,
...rest
} = this.props;
@ -44,6 +45,7 @@ export default class QuarterRangeControl extends InputDateRange {
maxDate={maxDate ? filterDate(maxDate, data, format) : undefined}
minDuration={minDuration ? parseDuration(minDuration) : undefined}
maxDuration={maxDuration ? parseDuration(maxDuration) : undefined}
useMobileUI={env.useMobileUI}
/>
</div>
);

View File

@ -26,6 +26,7 @@ export default class YearRangeControl extends InputDateRange {
maxDuration,
data,
format,
env,
...rest
} = this.props;
@ -44,6 +45,7 @@ export default class YearRangeControl extends InputDateRange {
maxDate={maxDate ? filterDate(maxDate, data, format) : undefined}
minDuration={minDuration ? parseDuration(minDuration) : undefined}
maxDuration={maxDuration ? parseDuration(maxDuration) : undefined}
useMobileUI={env.useMobileUI}
/>
</div>
);

View File

@ -1,6 +1,7 @@
import React from 'react';
import Overlay from '../../components/Overlay';
import PopOver from '../../components/PopOver';
import PopUp from '../../components/PopUp';
import {
OptionsControl,
@ -17,7 +18,7 @@ import {Api} from '../../types';
import {isEffectiveApi} from '../../utils/api';
import Spinner from '../../components/Spinner';
import ResultBox from '../../components/ResultBox';
import {autobind, getTreeAncestors} from '../../utils/helper';
import {autobind, getTreeAncestors, isMobile} from '../../utils/helper';
import {findDOMNode} from 'react-dom';
import {normalizeOptions} from '../../components/Select';
@ -90,6 +91,7 @@ export interface TreeSelectProps extends OptionsControlProps {
hideNodePathLabel?: boolean;
enableNodePath?: boolean;
pathSeparator?: string;
useMobileUI?: boolean;
}
export interface TreeSelectState {
@ -502,67 +504,51 @@ export default class TreeSelectControl extends React.Component<
: options;
return (
<Overlay
container={popOverContainer || (() => this.container.current)}
target={() => this.target}
show
>
<PopOver
classPrefix={ns}
className={`${ns}TreeSelect-popover`}
style={{
minWidth: this.target ? this.target.offsetWidth : undefined
}}
onHide={this.close}
overlay
>
<TreeSelector
classPrefix={ns}
onlyChildren={onlyChildren}
labelField={labelField}
valueField={valueField}
disabled={disabled}
onChange={this.handleChange}
joinValues={joinValues}
extractValue={extractValue}
delimiter={delimiter}
placeholder={__(optionsPlaceholder)}
options={filtedOptions}
highlightTxt={this.state.inputValue}
multiple={multiple}
initiallyOpen={initiallyOpen}
unfoldedLevel={unfoldedLevel}
withChildren={withChildren}
rootLabel={__(rootLabel)}
rootValue={rootValue}
showIcon={showIcon}
showRadio={showRadio}
cascade={cascade}
foldedField="collapsed"
hideRoot
value={value || ''}
nodePath={nodePath}
enableNodePath={enableNodePath}
pathSeparator={pathSeparator}
maxLength={maxLength}
minLength={minLength}
onAdd={onAdd}
creatable={creatable}
createTip={createTip}
rootCreatable={rootCreatable}
rootCreateTip={rootCreateTip}
onEdit={onEdit}
editable={editable}
editTip={editTip}
removable={removable}
removeTip={removeTip}
onDelete={onDelete}
bultinCUD={!addControls && !editControls}
onDeferLoad={deferLoad}
onExpandTree={expandTreeOptions}
/>
</PopOver>
</Overlay>
<TreeSelector
classPrefix={ns}
onlyChildren={onlyChildren}
labelField={labelField}
valueField={valueField}
disabled={disabled}
onChange={this.handleChange}
joinValues={joinValues}
extractValue={extractValue}
delimiter={delimiter}
placeholder={__(optionsPlaceholder)}
options={filtedOptions}
highlightTxt={this.state.inputValue}
multiple={multiple}
initiallyOpen={initiallyOpen}
unfoldedLevel={unfoldedLevel}
withChildren={withChildren}
rootLabel={__(rootLabel)}
rootValue={rootValue}
showIcon={showIcon}
showRadio={showRadio}
cascade={cascade}
foldedField="collapsed"
hideRoot
value={value || ''}
nodePath={nodePath}
enableNodePath={enableNodePath}
pathSeparator={pathSeparator}
maxLength={maxLength}
minLength={minLength}
onAdd={onAdd}
creatable={creatable}
createTip={createTip}
rootCreatable={rootCreatable}
rootCreateTip={rootCreateTip}
onEdit={onEdit}
editable={editable}
editTip={editTip}
removable={removable}
removeTip={removeTip}
onDelete={onDelete}
bultinCUD={!addControls && !editControls}
onDeferLoad={deferLoad}
onExpandTree={expandTreeOptions}
/>
);
}
@ -581,9 +567,13 @@ export default class TreeSelectControl extends React.Component<
autoComplete,
selectedOptions,
placeholder,
popOverContainer,
env,
translate: __
} = this.props;
const { isOpened } = this.state;
const { useMobileUI } = env;
return (
<div ref={this.container} className={cx(`TreeSelectControl`, className)}>
<ResultBox
@ -622,7 +612,36 @@ export default class TreeSelectControl extends React.Component<
>
{loading ? <Spinner size="sm" /> : undefined}
</ResultBox>
{this.state.isOpened ? this.renderOuter() : null}
{ !(useMobileUI && isMobile()) && isOpened ? (
<Overlay
container={popOverContainer || (() => this.container.current)}
target={() => this.target}
show
>
<PopOver
classPrefix={ns}
className={`${ns}TreeSelect-popover`}
style={{
minWidth: this.target ? this.target.offsetWidth : undefined
}}
onHide={this.close}
overlay
>
{this.renderOuter()}
</PopOver>
</Overlay>
) : null}
{
useMobileUI && isMobile() && (
<PopUp
className={cx(`${ns}TreeSelect-popup`)}
isShow={isOpened}
onHide={this.close}
>
{this.renderOuter()}
</PopUp>
)
}
</div>
);
}