Skip to content

Commit

Permalink
Merge branch 'feat/select' into 'dev'
Browse files Browse the repository at this point in the history
Add basic Select component

See merge request dt-insight-front/infrastructure/molecule!11
  • Loading branch information
wewoor committed Dec 8, 2020
2 parents b9a3116 + b28718b commit 7b9c813
Show file tree
Hide file tree
Showing 15 changed files with 587 additions and 14,339 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
node_modules
dist
coverage
.DS_Store
.DS_Store
.yarn-error.log
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
"stylelint-config-sass-guidelines": "^7.1.0",
"ts-jest": "^26.1.0",
"ts-loader": "^7.0.5",
"typescript": "^3.9.5",
"typescript": "^4.0.5",
"webpack-cli": "^4.0.0",
"webpack-dev-server": "^3.11.0",
"webpack-merge": "^5.2.0"
Expand Down
2 changes: 1 addition & 1 deletion src/common/className.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export function prefixClaName(name: string, prefix: string = APP_PREFIX) {

export function classNames(...args) {
if (isEmpty(args)) return;
let classList: string[] = [];
const classList: string[] = [];
for (const arg of args) {
if (!arg) continue;
const argType = typeof arg;
Expand Down
4 changes: 4 additions & 0 deletions src/common/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,7 @@ export function getPositionByPlacement(
console.log('getPositionByPlacement', x, y);
return { x, y };
}

export function getAttr(domElement: HTMLElement, attr) {
return domElement.getAttribute(attr) || '';
}
12 changes: 8 additions & 4 deletions src/components/contextview/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,16 @@ export interface IContextView {
dispose(): void;
}

enum ContextViewEvent {
onHide = 'onHide',
}

const contextViewClass = prefixClaName('context-view');
const contentClass = '.context-view-content';
const emitter = new EventEmitter();

export function useContextView(props?: IContextViewProps): IContextView {
const claName = classNames(contextViewClass, 'fade-in');
const emitter = new EventEmitter();
let contextView: HTMLElementType = select('.' + contextViewClass); // Singleton contextView dom

const show = (anchorPos: IPosition, render?: () => React.ReactNode) => {
Expand All @@ -58,12 +62,12 @@ export function useContextView(props?: IContextViewProps): IContextView {
if (contextView) {
contextView.style.visibility = 'hidden';
ReactDOM.unmountComponentAtNode(select(contentClass)!);
emitter.emit('onHide');
emitter.emit(ContextViewEvent.onHide);
}
};

const onHide = (callback: Function) => {
emitter.subscribe('onHide', callback);
emitter.subscribe(ContextViewEvent.onHide, callback);
};

const onMaskClick = (e: React.MouseEvent) => {
Expand All @@ -73,7 +77,7 @@ export function useContextView(props?: IContextViewProps): IContextView {
};

const dispose = () => {
emitter.unsubscribe('onHide');
emitter.unsubscribe(ContextViewEvent.onHide);
};

if (!contextView) {
Expand Down
2 changes: 2 additions & 0 deletions src/components/select/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './select';
export * from './option';
50 changes: 50 additions & 0 deletions src/components/select/option.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import './style.scss';
import * as React from 'react';
import { ComponentProps } from 'react';
import { classNames, getBEMElement, getBEMModifier } from 'mo/common/className';

import { selectClassName } from './select';

export interface ISelectOption extends ComponentProps<'option'> {
value?: string;
name?: string;
description?: string;
disabled?: boolean;
}

const selectOptionClassName = getBEMElement(selectClassName, 'option');
const selectOptionDisabledClassName = getBEMModifier(
selectOptionClassName,
'disabled'
);

export function Option(props: ISelectOption) {
const {
className,
value,
title,
name,
description,
disabled,
children,
...custom
} = props;

const claNames = classNames(
selectOptionClassName,
className,
disabled ? selectOptionDisabledClassName : ''
);
return (
<div
className={claNames}
title={title}
data-name={name || children}
data-value={value}
data-desc={description}
{...(custom as any)}
>
{children}
</div>
);
}
199 changes: 199 additions & 0 deletions src/components/select/select.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import './style.scss';
import * as React from 'react';
import {
Children,
PureComponent,
isValidElement,
RefObject,
ComponentProps,
} from 'react';
import {
prefixClaName,
classNames,
getBEMElement,
getBEMModifier,
} from 'mo/common/className';
import { cloneReactChildren } from 'mo/react';
import { getAttr } from 'mo/common/dom';
import { IContextView, useContextView } from 'mo/components/contextview';

import { ISelectOption } from './option';
import { Icon } from '../icon';

export interface ISelect extends ComponentProps<any> {
value?: string;
style?: React.CSSProperties;
className?: string;
defaultValue?: string;
placeholder?: string;
showArrow?: boolean;
children?: ReactNode;
onSelect?(e: React.MouseEvent, selectedOption?: ISelectOption): void;
}

const initialValue = {
isOpen: false,
option: {
name: '',
value: '',
description: '',
},
};

type IState = {
isOpen: boolean;
option: ISelectOption;
};

export const selectClassName = prefixClaName('select');
const containerClassName = getBEMElement(selectClassName, 'container');
const selectOptionsClassName = getBEMElement(selectClassName, 'options');
const selectDescriptorClassName = getBEMElement(selectClassName, 'descriptor');
const inputClassName = getBEMElement(selectClassName, 'input');
const selectActiveClassName = getBEMModifier(selectClassName, 'active');
const selectArrowClassName = getBEMElement(selectClassName, 'arrow');

export class Select extends PureComponent<ISelect, IState> {
private contextView: IContextView;
public state: IState;
private selectElm: RefObject<HTMLDivElement>;
private selectInput: RefObject<HTMLInputElement>;

constructor(props) {
super(props);
this.contextView = useContextView({
shadowOutline: false,
});
this.state = this.getDefaultState(this.props);
this.selectElm = React.createRef();
this.selectInput = React.createRef();
}

public componentDidMount() {
this.contextView.onHide(() => {
if (this.state.isOpen) {
this.setState({
isOpen: false,
});
}
});
}

public getDefaultState(props) {
let defaultSelectedOption: ISelectOption = {};
const defaultValue = props.value || props.defaultValue;
const options = Children.toArray(props.children);
for (const option of options) {
if (isValidElement(option)) {
const optionProps = option.props as ISelectOption;
if (optionProps.value && optionProps.value === defaultValue) {
defaultSelectedOption = {
...optionProps,
name:
optionProps.name ||
(optionProps.children as string),
};
break;
}
}
}
return {
...initialValue,
option: { ...defaultSelectedOption },
};
}

public handleOnClickOption = (e: React.MouseEvent) => {
const option = e.target as HTMLDivElement;
const value = getAttr(option, 'data-value');
const name = getAttr(option, 'data-name');
const desc = getAttr(option, 'data-desc');
if (name) {
const optionItem: ISelectOption = {
value: value,
name: name,
description: desc,
};

this.setState(
{
option: optionItem,
},
() => {
this.props.onSelect?.(e, optionItem);
this.contextView.hide();
}
);
}
};

public handleOnHoverOption = (e: React.MouseEvent) => {
const option = e.target as HTMLDivElement;
const desc = getAttr(option, 'data-desc');
const descriptor = this.contextView.view!.querySelector(
'.' + selectDescriptorClassName
);
if (descriptor) {
const content = desc || 'None';
descriptor.innerHTML = content;
descriptor.setAttribute('title', content);
}
};

public handleOnClickSelect = (e: React.MouseEvent) => {
const select = this.selectElm.current;
const { children } = this.props;
if (select) {
const selectRect = select?.getBoundingClientRect();
selectRect.y = selectRect.y + selectRect.height;
this.setState({ isOpen: true });

this.contextView.show(selectRect, () => {
return (
<div
style={{
width: selectRect.width,
}}
className={classNames(
containerClassName,
selectActiveClassName
)}
onMouseOver={this.handleOnHoverOption}
>
<div className={selectOptionsClassName}>
{cloneReactChildren<ISelectOption>(children, {
onClick: this.handleOnClickOption,
})}
</div>
<div className={selectDescriptorClassName}>None</div>
</div>
);
});
}
};

public render() {
const { option, isOpen } = this.state;
const { className, placeholder, ...custom } = this.props;

const selectActive = isOpen ? selectActiveClassName : '';
const claNames = classNames(selectClassName, className, selectActive);

return (
<div ref={this.selectElm} className={claNames} {...(custom as any)}>
<input
onClick={this.handleOnClickSelect}
ref={this.selectInput}
autoComplete="off"
placeholder={placeholder}
className={inputClassName}
value={option.name}
readOnly
/>
<span className={selectArrowClassName}>
<Icon type={'chevron-down'} />
</span>
</div>
);
}
}
Loading

0 comments on commit 7b9c813

Please sign in to comment.