Skip to content

Commit 027535d

Browse files
committed
feat: add basic Select component
1 parent b9a3116 commit 027535d

File tree

7 files changed

+435
-0
lines changed

7 files changed

+435
-0
lines changed

src/components/select/index.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './select';
2+
export * from './option';

src/components/select/option.tsx

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import './style.scss';
2+
import * as React from 'react';
3+
import { ComponentProps } from 'react';
4+
import { classNames, getBEMElement, getBEMModifier } from 'mo/common/className';
5+
6+
import { selectClassName } from './select';
7+
8+
export interface ISelectOption extends ComponentProps<'option'> {
9+
value?: string;
10+
title?: string;
11+
description?: string;
12+
disabled?: boolean;
13+
}
14+
15+
const selectOptionClassName = getBEMElement(selectClassName, 'option');
16+
const selectOptionDisabledClassName = getBEMModifier(
17+
selectOptionClassName,
18+
'disabled'
19+
);
20+
21+
export function Option(props: ISelectOption) {
22+
const {
23+
className,
24+
value,
25+
title,
26+
description,
27+
disabled,
28+
children,
29+
...custom
30+
} = props;
31+
32+
const claNames = classNames(
33+
selectOptionClassName,
34+
className,
35+
disabled ? selectOptionDisabledClassName : ''
36+
);
37+
const content = children || title;
38+
return (
39+
<div
40+
className={claNames}
41+
title={content}
42+
data-value={value}
43+
data-desc={description}
44+
{...(custom as any)}
45+
>
46+
{content}
47+
</div>
48+
);
49+
}

src/components/select/select.tsx

+167
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import './style.scss';
2+
import * as React from 'react';
3+
import { useRef, useState, Children, isValidElement, useEffect } from 'react';
4+
import {
5+
prefixClaName,
6+
classNames,
7+
getBEMElement,
8+
getBEMModifier,
9+
} from 'mo/common/className';
10+
import { cloneReactChildren } from 'mo/react';
11+
import { useContextView } from 'mo/components/contextview';
12+
13+
import { ISelectOption } from './option';
14+
import { Icon } from '../icon';
15+
16+
export interface ISelect {
17+
value?: string;
18+
title?: string;
19+
style?: React.CSSProperties;
20+
className?: string;
21+
defaultValue?: string;
22+
placeholder?: string;
23+
prefix?: ReactNode;
24+
showArrow?: boolean;
25+
children?: ReactNode;
26+
onSelect?(e: React.MouseEvent, selectedOption?: ISelectOption): void;
27+
}
28+
29+
export const selectClassName = prefixClaName('select');
30+
const containerClassName = getBEMElement(selectClassName, 'container');
31+
const selectOptionsClassName = getBEMElement(selectClassName, 'options');
32+
const selectDescriptorClassName = getBEMElement(selectClassName, 'descriptor');
33+
const inputClassName = getBEMElement(selectClassName, 'input');
34+
const selectActiveClassName = getBEMModifier(selectClassName, 'active');
35+
const selectArrowClassName = getBEMElement(selectClassName, 'arrow');
36+
37+
export function Select(props: ISelect) {
38+
const {
39+
className,
40+
children,
41+
defaultValue = '',
42+
placeholder,
43+
value,
44+
title,
45+
onSelect,
46+
...custom
47+
} = props;
48+
49+
const contextView = useContextView({
50+
shadowOutline: false,
51+
});
52+
53+
const defaultSelectedOption: ISelectOption = {};
54+
const options = Children.toArray(children);
55+
for (const option of options) {
56+
if (isValidElement(option)) {
57+
const optionProps = option.props as ISelectOption;
58+
if (optionProps.value && optionProps.value === defaultValue) {
59+
defaultSelectedOption.title = optionProps.children as string;
60+
defaultSelectedOption.value = optionProps.value;
61+
break;
62+
}
63+
}
64+
}
65+
66+
const selectElm = useRef<HTMLDivElement>(null);
67+
const selectInput = useRef<HTMLInputElement>(null);
68+
const [isOpen, setIsOpen] = useState(false);
69+
const [inputValue, setInputValue] = useState(defaultSelectedOption);
70+
71+
const handleOnClickOption = (e: React.MouseEvent) => {
72+
const option = e.target as HTMLDivElement;
73+
const value = option.getAttribute('data-value');
74+
const title = option.getAttribute('title');
75+
const desc = option.getAttribute('data-desc');
76+
const optionItem = {
77+
value: value!,
78+
title: title!,
79+
description: desc!,
80+
};
81+
82+
setInputValue(optionItem);
83+
onSelect?.(e, optionItem);
84+
setIsOpen(false);
85+
contextView.hide();
86+
};
87+
88+
const handOnHoverOption = (e: React.MouseEvent) => {
89+
const option = e.target as HTMLDivElement;
90+
const desc = option.getAttribute('data-desc');
91+
const descriptor = contextView.view!.querySelector(
92+
'.' + selectDescriptorClassName
93+
);
94+
if (descriptor) {
95+
const content = desc || 'None';
96+
descriptor.innerHTML = content;
97+
descriptor.setAttribute('title', content);
98+
}
99+
};
100+
101+
const events = {
102+
onClick: (e: React.MouseEvent) => {
103+
const select = selectElm.current;
104+
if (select) {
105+
const selectRect = select?.getBoundingClientRect();
106+
selectRect.y = selectRect.y + selectRect.height;
107+
setIsOpen(true);
108+
109+
contextView.show(selectRect, () => {
110+
return (
111+
<div
112+
style={{
113+
width: selectRect.width,
114+
}}
115+
className={classNames(
116+
containerClassName,
117+
selectActiveClassName
118+
)}
119+
onMouseOver={handOnHoverOption}
120+
>
121+
<div className={selectOptionsClassName}>
122+
{cloneReactChildren<ISelectOption>(children, {
123+
onClick: handleOnClickOption,
124+
})}
125+
</div>
126+
<div className={selectDescriptorClassName}>
127+
None
128+
</div>
129+
</div>
130+
);
131+
});
132+
}
133+
},
134+
};
135+
136+
const selectActive = isOpen ? selectActiveClassName : '';
137+
const claNames = classNames(selectClassName, className, selectActive);
138+
139+
useEffect(() => {
140+
contextView.onHide(() => {
141+
setIsOpen(false);
142+
});
143+
144+
return () => {
145+
contextView.dispose();
146+
};
147+
}, [isOpen, inputValue]);
148+
149+
return (
150+
<div ref={selectElm} className={claNames} {...(custom as any)}>
151+
<input
152+
{...events}
153+
ref={selectInput}
154+
autoComplete="off"
155+
placeholder={placeholder}
156+
className={inputClassName}
157+
value={inputValue.title}
158+
readOnly
159+
>
160+
{title}
161+
</input>
162+
<span className={selectArrowClassName}>
163+
<Icon type={'chevron-down'} />
164+
</span>
165+
</div>
166+
);
167+
}

src/components/select/style.scss

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
@import 'mo/style/common';
2+
$select: prefix('select');
3+
4+
#{$select} {
5+
align-items: center;
6+
border: 1px solid;
7+
box-sizing: border-box;
8+
display: flex;
9+
font: inherit;
10+
height: 26px;
11+
justify-content: left;
12+
position: relative;
13+
width: initial;
14+
15+
&--active {
16+
border: 1px solid rgba(14, 99, 156, 0.8);
17+
}
18+
19+
&__input {
20+
appearance: none;
21+
background: inherit;
22+
border: 0;
23+
color: inherit;
24+
cursor: default;
25+
font: inherit;
26+
font-size: inherit;
27+
height: 100%;
28+
margin: 0;
29+
outline: 0;
30+
padding: 0 8px;
31+
width: 100%;
32+
33+
&:focus {
34+
outline: none;
35+
}
36+
}
37+
38+
&__arrow {
39+
bottom: 0;
40+
font-size: 14px;
41+
height: 14px;
42+
line-height: 14px;
43+
margin: auto;
44+
pointer-events: none;
45+
position: absolute;
46+
right: 6px;
47+
top: 0;
48+
width: 14px;
49+
}
50+
51+
&__container {
52+
align-items: center;
53+
appearance: none;
54+
box-sizing: border-box;
55+
display: flex;
56+
flex-direction: column;
57+
font-size: 13px;
58+
}
59+
60+
&__options,
61+
&__descriptor {
62+
text-indent: 6px;
63+
width: 100%;
64+
}
65+
66+
&__descriptor {
67+
border-top: 1px solid rgb(60, 60, 60);
68+
height: 26px;
69+
line-height: 26px;
70+
overflow: hidden;
71+
text-overflow: ellipsis;
72+
word-break: break-all;
73+
}
74+
75+
&__option {
76+
cursor: pointer;
77+
height: 20px;
78+
line-height: 20px;
79+
width: 100%;
80+
81+
&--disabled {
82+
cursor: not-allowed;
83+
}
84+
}
85+
}

src/style/common.scss

+10
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ $prefix: 'mo';
66
@return '.' + $prefix + '-' + $name;
77
}
88

9+
// The Naming of BEM Element
10+
@function bem-ele($block, $element) {
11+
@return $block + '__' + $element;
12+
}
13+
14+
// The Naming of BEM Modifier
15+
@function bem-mod($blockOrElement, $modifier) {
16+
@return $blockOrElement + '--' + $modifier;
17+
}
18+
919
.#{$prefix} {
1020
bottom: 0;
1121
font-size: 13px;

src/style/theme.scss

+16
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
@import 'mo/components/tabs/style';
2525
@import 'mo/components/button/style';
2626
@import 'mo/components/input/style';
27+
@import 'mo/components/select/style';
2728

2829
// =============== Workbench =============== //
2930
#{prefix($workbench)} {
@@ -224,5 +225,20 @@
224225
background-color: #f5f5f5;
225226
color: rgba(0, 0, 0, 0.25);
226227
opacity: 1;
228+
229+
// =============== Select =============== //
230+
#{$select} {
231+
&__container {
232+
background-color: rgb(60, 60, 60);
233+
// border-color: rgb(60, 60, 60);
234+
border-top: 0;
235+
color: rgb(240, 240, 240);
236+
}
237+
238+
&__option {
239+
&:hover {
240+
background-color: rgba(14, 99, 156, 0.8);
241+
color: rgb(240, 240, 240);
242+
}
227243
}
228244
}

0 commit comments

Comments
 (0)