Skip to content

Commit

Permalink
feat: select searchPostion (80%)
Browse files Browse the repository at this point in the history
  • Loading branch information
pointhalo committed Jun 14, 2024
1 parent f68d8ad commit a0bae14
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 15 deletions.
45 changes: 44 additions & 1 deletion content/input/select/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,49 @@ import { Select } from '@douyinfe/semi-ui';
);
```
### 搜索框位置
默认搜索框展示于 Select 的 Trigger 触发器上。通过 `searchPosition` 可以指定不同的位置,可选 `dropdown``trigger`。 在 v2.61.0后提供
若希望定制位于 dropdown 中的 Input 搜索框的 placeholder,可以通过 `searchPlaceholder` 控制
```jsx live=true
import React from 'react';
import { Select } from '@douyinfe/semi-ui';

() => (
<>
<Select
filter
searchPosition='dropdown'
style={{ width: 180 }}
placeholder='我的搜索框在下拉菜单中'
searchPlaceholder="带搜索功能的单选"
>
<Select.Option value="douyin">抖音</Select.Option>
<Select.Option value="ulikecam">轻颜相机</Select.Option>
<Select.Option value="jianying">剪映</Select.Option>
<Select.Option value="xigua">西瓜视频</Select.Option>
</Select>
<br />
<br />
<Select
filter
searchPosition='dropdown'
multiple
style={{ width: 300 }}
placeholder='我的搜索框在下拉菜单中'
searchPlaceholder="带搜索功能的多选"
autoClearSearchValue={false}
>
<Select.Option value="semi-0">Semi-0</Select.Option>
<Select.Option value="semi-1">Semi-1</Select.Option>
<Select.Option value="semi-2">Semi-2</Select.Option>
<Select.Option value="semi-3">Semi-3</Select.Option>
<Select.Option value="semi-4">Semi-4</Select.Option>
</Select>
</>
);
```
### 远程搜索
带有远程搜索,防抖请求,加载状态的多选示例
Expand Down Expand Up @@ -1394,7 +1437,7 @@ import { Select, Checkbox, Highlight } from '@douyinfe/semi-ui';
| autoFocus | 初始渲染时是否自动 focus | boolean | false |
| borderless | 无边框模式 >=2.33.0 | boolean | |
| className | 类名 | string | |
| clearIcon | 可用于自定义清除按钮, showClear为true时有效 | ReactNode | 2.25.0 |
| clearIcon | 可用于自定义清除按钮, showClear为true时有效 | ReactNode | | 2.25.0
| clickToHide | 已展开时,点击选择框是否自动收起下拉列表 | boolean | false |
| defaultValue | 初始选中的值 | string\|number\|array | |
| defaultOpen | 是否默认展开下拉列表 | boolean | false |
Expand Down
2 changes: 2 additions & 0 deletions packages/semi-foundation/select/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ const strings = {
MODE_AUTOCOMPLETE: 'autoComplete',
// MODE_TAGS: 'tags',
STATUS: VALIDATE_STATUS,
SEARCH_POSITION_TRIGGER: 'trigger',
SEARCH_POSITION_DROPDOWN: 'dropdown'
} as const;

const numbers = { LIST_HEIGHT: 270 };
Expand Down
56 changes: 54 additions & 2 deletions packages/semi-foundation/select/foundation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import isNullOrUndefined from '../utils/isNullOrUndefined';
import { BasicOptionProps } from './optionFoundation';
import isEnterPress from '../utils/isEnterPress';
import { handlePrevent } from '../utils/a11y';
import { strings } from './constants';

export interface SelectAdapter<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S> {
getTriggerWidth(): number;
Expand All @@ -18,7 +19,7 @@ export interface SelectAdapter<P = Record<string, any>, S = Record<string, any>>
rePositionDropdown(): void;
updateFocusIndex(index: number): void;
updateSelection(selection: Map<any, any>): void;
openMenu(): void;
openMenu(cb?: () => void): void;
notifyDropdownVisibleChange(visible: boolean): void;
registerClickOutsideHandler(event: any): void;
toggleInputShow(show: boolean, cb: () => void): void;
Expand All @@ -30,6 +31,7 @@ export interface SelectAdapter<P = Record<string, any>, S = Record<string, any>>
notifyClear(): void;
updateInputValue(inputValue: string): void;
focusInput(): void;
focusDropdownInput(): void;
notifySearch(inputValue: string, event?: any): void;
registerKeyDown(handler: () => void): void;
unregisterKeyDown(): void;
Expand Down Expand Up @@ -367,7 +369,13 @@ export default class SelectFoundation extends BaseFoundation<SelectAdapter> {
// whether it is a filter or not, isFocus is guaranteed to be true when open
this._adapter.updateFocusState(true);
}
this._adapter.openMenu();
this._adapter.openMenu(() => {
const { searchPosition, autoFocus } = this.getProps();
// todo,需要放在 open的回调里,保证已展开
if (autoFocus && searchPosition === strings.SEARCH_POSITION_DROPDOWN) {
this._adapter.focusDropdownInput();
}
});
this._setDropdownWidth();
this._adapter.notifyDropdownVisibleChange(true);

Expand All @@ -378,6 +386,7 @@ export default class SelectFoundation extends BaseFoundation<SelectAdapter> {
this._notifyBlur(e);
this._adapter.updateFocusState(false);
});

}

toggle2SearchInput(isShow: boolean) {
Expand Down Expand Up @@ -714,6 +723,49 @@ export default class SelectFoundation extends BaseFoundation<SelectAdapter> {
}
}

_handleDropdownInputKeyDown(event: KeyboardEvent) {
const key = event.keyCode;
const { loading, filter, multiple, disabled } = this.getProps();
const { isOpen } = this.getStates();

if (loading || disabled) {
return;
}
switch (key) {
case KeyCode.UP:
// Prevent Input's cursor from following
// Prevent Input cursor from following
event.preventDefault();
this._handleArrowKeyDown(-1);
break;
case KeyCode.DOWN:
// Prevent Input's cursor from following
// Prevent Input cursor from following
event.preventDefault();
this._handleArrowKeyDown(1);
break;
case KeyCode.BACKSPACE:
this._handleBackspaceKeyDown();
break;
case KeyCode.ENTER:
// internal-issues:302
// prevent trigger form’s submit when use in form
handlePrevent(event);
this._handleEnterKeyDown(event);
break;
case KeyCode.ESC:
isOpen && this.close({ event: event });
filter && !multiple && this._focusTrigger();
break;
case KeyCode.TAB:
// check if slot have focusable element
this._handleTabKeyDown(event);
break;
default:
break;
}
}

_handleKeyDown(event: KeyboardEvent) {
const key = event.keyCode;
const { loading, filter, multiple, disabled } = this.getProps();
Expand Down
9 changes: 5 additions & 4 deletions packages/semi-ui/select/_story/select.stories.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3571,16 +3571,16 @@ export const UpdateOtherKeyNotInList = () => {

export const SearchPosition = () => {

return
(
<>
return (<>
<Select
filter
searchPosition='dropdown'
onChangeWithObject
placeholder={'single searchPosition=dropdown'}
optionList={optionList}
searchPlaceholder='dropdown input place'
showClear
autoFocus
style={{ width: 320 }}
/>
<Select
Expand All @@ -3589,10 +3589,11 @@ export const SearchPosition = () => {
placeholder={'multiple searchPosition=dropdown'}
searchPosition='dropdown'
onChangeWithObject
showClear
searchPlaceholder='dropdown input place'
optionList={optionList}
style={{ width: 320 }}
/>
</>
)
)
}
32 changes: 24 additions & 8 deletions packages/semi-ui/select/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ class Select extends BaseComponent<SelectProps, SelectState> {
defaultActiveFirstOption: true, // In order to meet the needs of A11y, change to true
showArrow: true,
showClear: false,
searchPosition: 'trigger',
searchPosition: strings.SEARCH_POSITION_TRIGGER,
remote: false,
autoAdjustOverflow: true,
autoClearSearchValue: true,
Expand Down Expand Up @@ -451,6 +451,12 @@ class Select extends BaseComponent<SelectProps, SelectState> {
this.inputRef.current.focus({ preventScroll });
}
},
focusDropdownInput: () => {
const { preventScroll } = this.props;
if (this.dropdownInputRef && this.dropdownInputRef.current) {
this.dropdownInputRef.current.focus({ preventScroll });
}
}
};
const multipleAdapter = {
notifyMaxLimit: (option: OptionProps) => this.props.onExceed(option),
Expand Down Expand Up @@ -522,8 +528,10 @@ class Select extends BaseComponent<SelectProps, SelectState> {
updateOptions: (options: OptionProps[]) => {
this.setState({ options });
},
openMenu: () => {
this.setState({ isOpen: true });
openMenu: (cb?: () => void) => {
this.setState({ isOpen: true }, () => {
cb?.();
});
},
closeMenu: () => {
this.setState({ isOpen: false });
Expand Down Expand Up @@ -730,6 +738,12 @@ class Select extends BaseComponent<SelectProps, SelectState> {
onChange: this.handleInputChange,
placeholder: searchPlaceholder,
...inputProps,
/**
* When searchPosition is trigger, the keyboard events are bound to the outer trigger div, so there is no need to listen in input.
* When searchPosition is dropdown, the popup and the outer trigger div are not parent- child relationships,
* and bubbles cannot occur, so onKeydown needs to be listened in input.
* */
onKeyDown: (e) => this.foundation._handleDropdownInputKeyDown(e)
};

return (
Expand Down Expand Up @@ -941,7 +955,8 @@ class Select extends BaseComponent<SelectProps, SelectState> {
virtualize,
multiple,
emptyContent,
searchPosition
searchPosition,
filter,
} = this.props;

// Do a filter first, instead of directly judging in forEach, so that the focusIndex can correspond to
Expand Down Expand Up @@ -974,7 +989,7 @@ class Select extends BaseComponent<SelectProps, SelectState> {
onKeyDown={e => this.foundation.handleContainerKeyDown(e)}
>
{outerTopSlot ? <div className={`${prefixcls}-option-list-outer-top-slot`} onMouseEnter={() => this.foundation.handleSlotMouseEnter()}>{outerTopSlot}</div> : null}
{searchPosition === 'dropdown' ? this.renderDropdownInput() : null}
{searchPosition === strings.SEARCH_POSITION_DROPDOWN && filter ? this.renderDropdownInput() : null}
<div
style={{ maxHeight: `${maxHeight}px` }}
className={optionListCls}
Expand Down Expand Up @@ -1008,7 +1023,7 @@ class Select extends BaseComponent<SelectProps, SelectState> {
renderText = (renderSelectedItem as RenderSingleSelectedItemFn)(selectedItem);
}

const showInputInTrigger = searchPosition === 'trigger';
const showInputInTrigger = searchPosition === strings.SEARCH_POSITION_TRIGGER;

const spanCls = cls({
[`${prefixcls}-selection-text`]: true,
Expand Down Expand Up @@ -1264,13 +1279,14 @@ class Select extends BaseComponent<SelectProps, SelectState> {
const tagContent = NotOneLine || (expandRestTagsOnClick && isOpen)
? selectedItems.map((item, i) => this.renderTag(item, i))
: oneLineTags;
const showTriggerInput = searchPosition === 'trigger';

const showTriggerInput = filterable && searchPosition === strings.SEARCH_POSITION_TRIGGER;

return (
<>
<div className={contentWrapperCls}>
{selectedItems && selectedItems.length ? tagContent : placeholderText}
{!filterable ? null : this.renderTriggerInput()}
{showTriggerInput ? this.renderTriggerInput() : null}
</div>
</>
);
Expand Down

0 comments on commit a0bae14

Please sign in to comment.